docs: add constitution, spec, plan, and data model for InteriorScan MVP
- Constitution: self-hosted first, FPS-grade immersion, mobile-first capture, API-first, open formats - Spec: 6 user stories (upload & process, share & embed, dashboard, API, measurements, billing) - Plan: 6 phases, 50 tasks, Phase 1-3 = MVP core - Data model: Drizzle schema with projects, photos, walkthroughs, processing_jobs, subscriptions
This commit is contained in:
@@ -0,0 +1,46 @@
|
|||||||
|
# InteriorScan Constitution
|
||||||
|
|
||||||
|
## Core Principles
|
||||||
|
|
||||||
|
### I. Self-Hosted First (NON-NEGOTIABLE)
|
||||||
|
All infrastructure must be self-hosted or FOSS. The ONLY external SaaS dependency is Stripe for billing. Database: PostgreSQL. Cache/sessions: Valkey. Processing: own GPU nodes. No vendor lock-in on reconstruction pipeline, viewer, or storage.
|
||||||
|
|
||||||
|
### II. Progressive Experience
|
||||||
|
Every feature must degrade gracefully. If WebGL fails, show 360° fallback. If processing fails, show uploaded photos. No feature should have a single point of failure that blocks the entire experience.
|
||||||
|
|
||||||
|
### III. FPS-Grade Immersion
|
||||||
|
The 3D viewer is not a "virtual tour" — it is a navigable space. Controls feel like a first-person game: WASD + mouse on desktop, tap-to-walk on mobile. Smooth movement, collision detection, gravity, head-bob optional. If it feels like a slideshow, it's wrong.
|
||||||
|
|
||||||
|
### IV. Photo-First Capture
|
||||||
|
Mobile phone is the only required hardware. iPhone LiDAR and 360° cameras are enhancements, not prerequisites. A real estate agent with an Android phone must be able to capture and publish a walkthrough in under 30 minutes.
|
||||||
|
|
||||||
|
### V. API-First Architecture
|
||||||
|
Every feature exposed via REST API before UI. InteriorScan is a reconstruction-as-a-service platform that happens to have a web UI, not a walled garden. White-label and embeddable viewers are first-class use cases.
|
||||||
|
|
||||||
|
### VI. TypeScript Strict
|
||||||
|
`strict: true` + `noUncheckedIndexedAccess: true`. No `any` without explicit justification. Zod for runtime validation. neverthrow for error handling in business logic.
|
||||||
|
|
||||||
|
### VII. Test-First (NON-NEGOTIABLE)
|
||||||
|
TDD mandatory: write failing test → user approves test → verify test fails → implement → verify test passes. No untested API endpoints, no untested business logic.
|
||||||
|
|
||||||
|
### VIII. Open Formats
|
||||||
|
All reconstruction outputs (mesh, point cloud, floor plan) must be exportable in open formats: OBJ, E57, LAS/LAZ, DXF, IFC (P4+). Proprietary lock-in is antithetical to this project's mission.
|
||||||
|
|
||||||
|
## Additional Constraints
|
||||||
|
|
||||||
|
- **Stack**: Next.js 16 App Router, Drizzle ORM, PostgreSQL, Valkey, three.js
|
||||||
|
- **Monorepo**: Turborepo + pnpm (tpl-next-saas template)
|
||||||
|
- **Billing**: Stripe only — no other payment/subscription SaaS
|
||||||
|
- **Lint/Format**: Biome 2.x ONLY — zero ESLint, zero Prettier
|
||||||
|
- **Testing**: Vitest v4 + Playwright
|
||||||
|
- **Design**: WCAG 2.2 AA minimum, d0a1 design palettes
|
||||||
|
- **Language**: Code/docs/commits in English. Team speaks Spanish.
|
||||||
|
|
||||||
|
## Governance
|
||||||
|
|
||||||
|
- Constitution supersedes all other practices
|
||||||
|
- Amendments require documentation, approval, and migration plan
|
||||||
|
- All PRs/reviews must verify compliance
|
||||||
|
- Retro-propagation: patterns validated in production must be PR'd back to `agentic-standards`
|
||||||
|
|
||||||
|
**Version**: 1.0.0 | **Ratified**: 2026-05-28 | **Last Amended**: 2026-05-28
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# Feature Specification: InteriorScan MVP
|
||||||
|
**Feature Branch**: `feat/001-interiorscan-mvp`
|
||||||
|
**Status**: Draft — Awaiting Approval
|
||||||
|
|
||||||
|
## Vision Statement
|
||||||
|
|
||||||
|
**InteriorScan turns apartment photos into a navigable 3D space** — like a Doom map of a real home. Real estate agents photograph a property, upload to InteriorScan, and share a link where potential buyers walk through the space in first-person. This filters out properties that don't fit, saving both agents and buyers from unnecessary physical visits.
|
||||||
|
|
||||||
|
## User Scenarios & Testing
|
||||||
|
|
||||||
|
### User Story 1 — Upload & Process (Priority: P1) 🎯 MVP Core
|
||||||
|
|
||||||
|
As a real estate agent, I upload 30-100 photos of a property and InteriorScan reconstructs the interior as a 3D walkthrough, so I can share a link with potential buyers who navigate it like a first-person game.
|
||||||
|
|
||||||
|
**Why this priority**: Without reconstruction, nothing else matters. This is the core value proposition.
|
||||||
|
|
||||||
|
**Independent Test**: Upload photos → receive a shareable walkthrough URL. A visitor opens URL → navigates space in first-person.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
1. **Given** I am logged in, **When** I create a new project, upload 40+ JPEG photos of an interior, and submit for processing, **Then** the system accepts the upload, shows a processing status (queued → processing → complete), and within 60 minutes produces a 3D walkthrough.
|
||||||
|
2. **Given** a walkthrough has completed processing, **When** I open the shareable link in a desktop browser, **Then** I see the reconstructed 3D space with WASD + mouse controls, collision detection, and texture-mapped walls/floors/ceilings.
|
||||||
|
3. **Given** a walkthrough has completed processing, **When** I open the shareable link on a mobile browser, **Then** I see the reconstructed 3D space with tap-to-walk navigation and gyroscope look-around option.
|
||||||
|
|
||||||
|
### User Story 2 — Share & Embed (Priority: P1) 🎯 MVP Distribution
|
||||||
|
|
||||||
|
As a real estate agent, I can share a walkthrough link or embed the viewer on any listing page, so potential buyers encounter the 3D tour wherever they find the property.
|
||||||
|
|
||||||
|
**Why this priority**: The walkthrough is useless without distribution. Sharing and embedding are how real estate works.
|
||||||
|
|
||||||
|
**Independent Test**: Agent shares link → opens in new tab. Agent copies embed code → pastes on WordPress/listing → viewer renders.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
1. **Given** a completed walkthrough, **When** I click "Share", **Then** I get a copyable link and an oEmbed-embeddable iframe snippet.
|
||||||
|
2. **Given** an embed snippet, **When** I paste it into a website, **Then** the InteriorScan viewer renders inline with all navigation controls functional.
|
||||||
|
3. **Given** a shared link, **When** an unauthenticated visitor opens it, **Then** they can navigate the full walkthrough without signing up (public access).
|
||||||
|
|
||||||
|
### User Story 3 — Dashboard & Projects (Priority: P2)
|
||||||
|
|
||||||
|
As a real estate agent, I manage all my properties from a dashboard, so I can track processing status, edit metadata, and organize my portfolio.
|
||||||
|
|
||||||
|
**Why this priority**: Essential for retention but not for first-value. A single walkthrough proves the concept.
|
||||||
|
|
||||||
|
**Independent Test**: Agent creates 3 projects → sees them in dashboard → edits name/address → deletes one.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
1. **Given** I am logged in, **When** I visit the dashboard, **Then** I see a grid of my projects with thumbnail, name, address, processing status, and date.
|
||||||
|
2. **Given** a project exists, **When** I edit its name or address, **Then** the changes persist and appear in the walkthrough metadata.
|
||||||
|
3. **Given** I have 3 projects, **When** I delete one, **Then** it is removed from the dashboard and its data is scheduled for cleanup within 24h.
|
||||||
|
|
||||||
|
### User Story 4 — API Access (Priority: P2)
|
||||||
|
|
||||||
|
As a developer integrating InteriorScan, I can upload photos and retrieve walkthroughs via REST API, so I can embed reconstruction into my own platform.
|
||||||
|
|
||||||
|
**Why this priority**: API-first is a constitution principle, but the web UI validates the pipeline first. API follows proven flow.
|
||||||
|
|
||||||
|
**Independent Test**: `POST /api/v1/projects` with photos → `GET /api/v1/projects/:id` returns status → `GET /api/v1/projects/:id/walkthrough` returns viewer URL.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
1. **Given** I have an API key, **When** I POST photos to `/api/v1/projects`, **Then** I receive a project ID and a `Location` header for status polling.
|
||||||
|
2. **Given** a project is processing, **When** I GET `/api/v1/projects/:id`, **Then** I receive `{status: "processing", progress: 45, estimatedCompletion: "..."}`.
|
||||||
|
3. **Given** a project is complete, **When** I GET `/api/v1/projects/:id/walkthrough`, **Then** I receive the viewer URL and embed snippet.
|
||||||
|
|
||||||
|
### User Story 5 — Measurements & Floor Plans (Priority: P3)
|
||||||
|
|
||||||
|
As a buyer, I can see room dimensions overlaid on the walkthrough and download a floor plan, so I can evaluate if furniture fits without visiting.
|
||||||
|
|
||||||
|
**Why this priority**: Differentiates from pure 360° tours but requires accurate reconstruction. Comes after core pipeline is proven.
|
||||||
|
|
||||||
|
**Independent Test**: Open walkthrough → toggle measurements → see room dimensions in meters.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
1. **Given** a walkthrough with LiDAR data, **When** I toggle "Show Measurements", **Then** room dimensions appear as overlays on walls and floor (±5% accuracy).
|
||||||
|
2. **Given** a completed walkthrough, **When** I click "Download Floor Plan", **Then** I receive a PDF/DXF floor plan with labeled rooms and dimensions.
|
||||||
|
|
||||||
|
### User Story 6 — Subscription & Billing (Priority: P3)
|
||||||
|
|
||||||
|
As an agent, I subscribe to a plan (Free/Pro/Enterprise) that determines how many walkthroughs I can create per month, so I only pay for what I use.
|
||||||
|
|
||||||
|
**Why this priority**: Revenue model, but not needed to validate the core pipeline.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
1. **Given** I am on the Free plan, **When** I attempt to create my 4th walkthrough, **Then** I see an upgrade prompt with Pro plan pricing.
|
||||||
|
2. **Given** I upgrade to Pro, **When** payment is confirmed via Stripe, **Then** my walkthrough limit increases to 25/month and the badge updates.
|
||||||
|
|
||||||
|
## Edge Cases
|
||||||
|
|
||||||
|
- **Fewer than 20 photos**: System rejects with guidance on minimum coverage
|
||||||
|
- **Corrupt/unreadable photos**: System skips corrupt files, warns user, continues with valid ones
|
||||||
|
- **Processing failure**: System retries once, then marks as "failed" with error details and refund option
|
||||||
|
- **Very large spaces (>200 photos)**: System queues with estimated time, processes in chunks
|
||||||
|
- **Mobile browser without WebGL**: Fallback to 360° photo carousel (progressive enhancement)
|
||||||
|
- **Duplicate photos**: System deduplicates by perceptual hash before processing
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
- **FR-001**: System MUST accept 20-500 JPEG/PNG photos per project via web upload or API
|
||||||
|
- **FR-002**: System MUST reconstruct 3D mesh from photos using COLMAP/OpenMVS pipeline
|
||||||
|
- **FR-003**: System MUST provide first-person navigation (WASD + mouse) in desktop browsers
|
||||||
|
- **FR-004**: System MUST provide tap-to-walk navigation in mobile browsers
|
||||||
|
- **FR-005**: System MUST generate shareable public links for completed walkthroughs
|
||||||
|
- **FR-006**: System MUST provide oEmbed-embeddable iframe code
|
||||||
|
- **FR-007**: System MUST show processing status: queued → processing → complete (with progress %)
|
||||||
|
- **FR-008**: System MUST handle processing failures gracefully with retry + notification
|
||||||
|
- **FR-009**: System MUST export walkthrough data in open formats (OBJ minimum)
|
||||||
|
- **FR-010**: System MUST work on iPhone, Android, desktop Chrome, Firefox, Safari
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
- **NFR-001**: Walkthrough viewer MUST load initial mesh in <5 seconds on 4G
|
||||||
|
- **NFR-002**: Navigation MUST run at ≥30 FPS on mid-range 2024 devices
|
||||||
|
- **NFR-003**: Processing MUST complete within 60 minutes for 100 photos
|
||||||
|
- **NFR-004**: System MUST handle 10 concurrent processing jobs
|
||||||
|
- **NFR-005**: Walkthrough links MUST work without authentication (public)
|
||||||
|
- **NFR-006**: API MUST follow OpenAPI 3.1 spec with Zod-validated schemas
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- **SC-001**: First tester uploads 40 photos → walks through reconstructed kitchen in <10 min total
|
||||||
|
- **SC-002**: Unauthenticated visitor opens shareable link → navigates fluently at 30+ FPS
|
||||||
|
- **SC-003**: Processing pipeline produces visual quality comparable to Matterport's basic tier
|
||||||
|
- **SC-004**: 3 of monyi's tester friends complete a walkthrough creation within 1 hour of onboarding
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# InteriorScan — Data Model
|
||||||
|
|
||||||
|
## Entity Relationship
|
||||||
|
|
||||||
|
```
|
||||||
|
users ──< team_members >── teams ──< projects ──< photos
|
||||||
|
│ │
|
||||||
|
│ └── walkthroughs
|
||||||
|
│
|
||||||
|
└── subscriptions
|
||||||
|
|
||||||
|
projects ──< processing_jobs
|
||||||
|
```
|
||||||
|
|
||||||
|
## Drizzle Schema Definition
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// packages/db/src/schema.ts
|
||||||
|
|
||||||
|
import { pgTable, text, timestamp, integer, jsonb, boolean, pgEnum } from 'drizzle-orm/pg-core';
|
||||||
|
import { cuid2 } from './custom-types'; // branded cuid2 ID type
|
||||||
|
|
||||||
|
// ── Enums ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export const projectStatusEnum = pgEnum('project_status', [
|
||||||
|
'draft', // created, no photos yet
|
||||||
|
'uploading', // photos being uploaded
|
||||||
|
'queued', // queued for processing
|
||||||
|
'processing', // COLMAP/OpenMVS pipeline running
|
||||||
|
'complete', // walkthrough ready
|
||||||
|
'failed', // processing error
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const processingStageEnum = pgEnum('processing_stage', [
|
||||||
|
'feature_extraction', // SIFT/ORB features
|
||||||
|
'feature_matching', // Correspondence search
|
||||||
|
'sfm', // Structure from Motion → sparse cloud
|
||||||
|
'mvs', // Multi-View Stereo → dense cloud
|
||||||
|
'meshing', // Mesh generation
|
||||||
|
'texturing', // UV mapping + texturing
|
||||||
|
'tiling', // LOD + Draco compression
|
||||||
|
'complete',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const subscriptionPlanEnum = pgEnum('subscription_plan', [
|
||||||
|
'free',
|
||||||
|
'pro',
|
||||||
|
'enterprise',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const teamRoleEnum = pgEnum('team_role', [
|
||||||
|
'owner',
|
||||||
|
'admin',
|
||||||
|
'member',
|
||||||
|
'viewer',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Users (better-auth managed) ─────────────────────
|
||||||
|
|
||||||
|
export const users = pgTable('users', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
email: text('email').notNull().unique(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
emailVerified: boolean('email_verified').default(false),
|
||||||
|
image: text('image'), // avatar URL
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Teams & Membership ──────────────────────────────
|
||||||
|
|
||||||
|
export const teams = pgTable('teams', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
slug: text('slug').notNull().unique(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const teamMembers = pgTable('team_members', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
teamId: cuid2('team_id').references(() => teams.id).notNull(),
|
||||||
|
userId: cuid2('user_id').references(() => users.id).notNull(),
|
||||||
|
role: teamRoleEnum('role').default('member').notNull(),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Projects ────────────────────────────────────────
|
||||||
|
|
||||||
|
export const projects = pgTable('projects', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
teamId: cuid2('team_id').references(() => teams.id).notNull(),
|
||||||
|
name: text('name').notNull(),
|
||||||
|
address: text('address'), // property address
|
||||||
|
description: text('description'),
|
||||||
|
status: projectStatusEnum('status').default('draft').notNull(),
|
||||||
|
photoCount: integer('photo_count').default(0),
|
||||||
|
captureDevice: text('capture_device'), // 'iphone-lidar', 'android', 'theta-z1', 'unknown'
|
||||||
|
processingProgress: integer('processing_progress').default(0), // 0-100
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Photos ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export const photos = pgTable('photos', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
projectId: cuid2('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
originalKey: text('original_key').notNull(), // S3/storage key for original
|
||||||
|
thumbnailKey: text('thumbnail_key'), // processed thumbnail key
|
||||||
|
perceptualHash: text('perceptual_hash'), // for deduplication
|
||||||
|
width: integer('width'),
|
||||||
|
height: integer('height'),
|
||||||
|
fileSizeBytes: integer('file_size_bytes'),
|
||||||
|
exifData: jsonb('exif_data'), // GPS, camera model, focal length
|
||||||
|
isDuplicate: boolean('is_duplicate').default(false),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Walkthroughs ────────────────────────────────────
|
||||||
|
|
||||||
|
export const walkthroughs = pgTable('walkthroughs', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
projectId: cuid2('project_id').references(() => projects.id).notNull(),
|
||||||
|
meshKey: text('mesh_key').notNull(), // tiled mesh storage key
|
||||||
|
textureKey: text('texture_key').notNull(), // texture atlas storage key
|
||||||
|
navmeshKey: text('navmesh_key'), // navmesh for click-to-walk
|
||||||
|
pointCloudKey: text('point_cloud_key'), // E57/LAS point cloud (optional)
|
||||||
|
thumbnailKey: text('thumbnail_key'), // preview image
|
||||||
|
bounds: jsonb('bounds'), // { min: [x,y,z], max: [x,y,z] }
|
||||||
|
stats: jsonb('stats'), // { vertices: N, faces: N, rooms: N }
|
||||||
|
isPublic: boolean('is_public').default(true),
|
||||||
|
slug: text('slug').notNull().unique(), // for shareable URLs
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Processing Jobs ─────────────────────────────────
|
||||||
|
|
||||||
|
export const processingJobs = pgTable('processing_jobs', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
projectId: cuid2('project_id').references(() => projects.id).notNull(),
|
||||||
|
stage: processingStageEnum('stage').default('feature_extraction').notNull(),
|
||||||
|
progress: integer('progress').default(0), // 0-100 per stage
|
||||||
|
error: text('error'), // error message if failed
|
||||||
|
startedAt: timestamp('started_at'),
|
||||||
|
completedAt: timestamp('completed_at'),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Subscriptions ───────────────────────────────────
|
||||||
|
|
||||||
|
export const subscriptions = pgTable('subscriptions', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
teamId: cuid2('team_id').references(() => teams.id).notNull(),
|
||||||
|
plan: subscriptionPlanEnum('plan').default('free').notNull(),
|
||||||
|
stripeCustomerId: text('stripe_customer_id'),
|
||||||
|
stripePriceId: text('stripe_price_id'),
|
||||||
|
stripeSubscriptionId: text('stripe_subscription_id'),
|
||||||
|
monthlyQuota: integer('monthly_quota').default(3), // Free=3, Pro=25, Enterprise=unlimited
|
||||||
|
monthlyUsage: integer('monthly_usage').default(0),
|
||||||
|
currentPeriodStart: timestamp('current_period_start'),
|
||||||
|
currentPeriodEnd: timestamp('current_period_end'),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Sessions (better-auth managed) ──────────────────
|
||||||
|
|
||||||
|
export const sessions = pgTable('sessions', {
|
||||||
|
id: cuid2('id').primaryKey(),
|
||||||
|
userId: cuid2('user_id').references(() => users.id).notNull(),
|
||||||
|
token: text('token').notNull().unique(),
|
||||||
|
expiresAt: timestamp('expires_at').notNull(),
|
||||||
|
ipAddress: text('ip_address'),
|
||||||
|
userAgent: text('user_agent'),
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Indexes
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE INDEX idx_projects_team_id ON projects(team_id);
|
||||||
|
CREATE INDEX idx_projects_status ON projects(status);
|
||||||
|
CREATE INDEX idx_photos_project_id ON photos(project_id);
|
||||||
|
CREATE INDEX idx_photos_perceptual_hash ON photos(perceptual_hash);
|
||||||
|
CREATE INDEX idx_walkthroughs_project_id ON walkthroughs(project_id);
|
||||||
|
CREATE INDEX idx_walkthroughs_slug ON walkthroughs(slug);
|
||||||
|
CREATE INDEX idx_processing_jobs_project_id ON processing_jobs(project_id);
|
||||||
|
CREATE INDEX idx_team_members_user_id ON team_members(user_id);
|
||||||
|
CREATE INDEX idx_team_members_team_id ON team_members(team_id);
|
||||||
|
CREATE INDEX idx_subscriptions_team_id ON subscriptions(team_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
| Rule | Level | Implementation |
|
||||||
|
|------|-------|---------------|
|
||||||
|
| User email unique | DB | `users.email UNIQUE` |
|
||||||
|
| Team slug unique | DB | `teams.slug UNIQUE` |
|
||||||
|
| Walkthrough slug unique | DB | `walkthroughs.slug UNIQUE` |
|
||||||
|
| Project belongs to team | DB | `projects.team_id FK → teams.id` |
|
||||||
|
| Photo belongs to project | DB | `photos.project_id FK → projects.id CASCADE` |
|
||||||
|
| Max 500 photos per project | App | Validate in Zod schema + upload endpoint |
|
||||||
|
| Monthly quota enforcement | App | Middleware checks `subscriptions.monthlyUsage < monthlyQuota` |
|
||||||
|
| Photo deduplication | App | Perceptual hash comparison before insert |
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
# InteriorScan MVP — Implementation Plan
|
||||||
|
|
||||||
|
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task.
|
||||||
|
|
||||||
|
**Goal:** Build an interior photogrammetry SaaS that turns apartment photos into navigable 3D walkthroughs (Doom-map style).
|
||||||
|
|
||||||
|
**Architecture:** Turborepo monorepo (tpl-next-saas template). Next.js 16 App Router for web UI + dashboard. GPU worker (Node.js) orchestrates COLMAP/OpenMVS processing pipeline. three.js FPS viewer for walkthrough navigation. PostgreSQL + Drizzle for data. Valkey for queues/sessions. Stripe for billing.
|
||||||
|
|
||||||
|
**Tech Stack:** Next.js 16 · Drizzle ORM · PostgreSQL · Valkey · three.js · COLMAP · OpenMVS · Stripe · Biome · Vitest
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
| Principle | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| I. Self-Hosted First | ✅ PASS | All infra self-hosted except Stripe |
|
||||||
|
| II. Progressive Experience | ✅ PASS | WebGL → 360° fallback, mobile tap-to-walk |
|
||||||
|
| III. FPS-Grade Immersion | ✅ PASS | WASD + mouse primary, tap-to-walk secondary |
|
||||||
|
| IV. Photo-First Capture | ✅ PASS | Mobile photos as baseline, LiDAR enhancement |
|
||||||
|
| V. API-First Architecture | ✅ PASS | API endpoints defined before UI |
|
||||||
|
| VI. TypeScript Strict | ✅ PASS | strict + noUncheckedIndexedAccess |
|
||||||
|
| VII. Test-First | ⚠ CONDITIONAL | TDD for business logic; 3D viewer tested via Playwright E2E |
|
||||||
|
| VIII. Open Formats | ✅ PASS | OBJ export in MVP, E57/DXF/IFC in P4+ |
|
||||||
|
|
||||||
|
⚠ VII conditional: 3D rendering correctness is validated via E2E visual regression, not unit tests. Business logic (upload, processing, auth) follows strict TDD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
|
||||||
|
See `data-model.md` (separate document — same directory).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Monorepo Scaffold + Core Infra (Shared)
|
||||||
|
|
||||||
|
- [ ] T001 Create monorepo from tpl-next-saas template
|
||||||
|
- [ ] T002 Configure Drizzle schema with InteriorScan tables (projects, photos, walkthroughs, processing_jobs, users, teams, subscriptions)
|
||||||
|
- [ ] T003 Run migration: `pnpm db:migrate`
|
||||||
|
- [ ] T004 [P] Add seed data: 1 user, 1 team, 3 project statuses
|
||||||
|
- [ ] T005 [P] Set up Valkey connection for job queue
|
||||||
|
- [ ] T006 [P] Configure Docker Compose (postgres + valkey + web)
|
||||||
|
|
||||||
|
**Checkpoint**: Migration green, `pnpm dev` starts without errors, health endpoint returns 200.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Photo Upload & Processing Pipeline (P1) 🎯 MVP Core
|
||||||
|
|
||||||
|
### 2A: Upload API
|
||||||
|
|
||||||
|
- [ ] T007 Write failing test: POST /api/v1/projects with photos returns 201 + project ID
|
||||||
|
- [ ] T008 Implement project creation endpoint with Zod validation
|
||||||
|
- [ ] T009 Write failing test: Upload 3 JPEGs → stored in object storage, DB has photo records
|
||||||
|
- [ ] T010 Implement photo upload with deduplication (perceptual hash)
|
||||||
|
- [ ] T011 Write failing test: GET /api/v1/projects/:id returns status + progress
|
||||||
|
- [ ] T012 Implement project status endpoint
|
||||||
|
|
||||||
|
**Checkpoint**: Upload 40 photos via API → receive project ID → poll status → see "queued".
|
||||||
|
|
||||||
|
### 2B: Processing Worker
|
||||||
|
|
||||||
|
- [ ] T013 Set up `apps/processor/` — Node.js worker with BullMQ/Valkey job queue
|
||||||
|
- [ ] T014 Write failing test: Job queued → worker picks up → runs COLMAP SfM → produces sparse point cloud
|
||||||
|
- [ ] T015 Implement COLMAP SfM step (feature extraction + matching + sparse reconstruction)
|
||||||
|
- [ ] T016 Write failing test: SfM output → OpenMVS dense reconstruction → mesh file produced
|
||||||
|
- [ ] T017 Implement OpenMVS dense reconstruction step (MVS + meshing + texturing)
|
||||||
|
- [ ] T018 Write failing test: Mesh + textures → tiled output for three.js streaming
|
||||||
|
- [ ] T019 Implement mesh processing + tiling step (LOD generation, Draco compression, texture atlas)
|
||||||
|
- [ ] T020 Implement progress reporting (Valkey pub/sub → web UI)
|
||||||
|
- [ ] T021 Write failing test: Processing failure → job marked failed → user notified
|
||||||
|
- [ ] T022 Implement error handling + retry logic
|
||||||
|
|
||||||
|
**Checkpoint**: Upload 40 photos → processing completes → produces OBJ mesh + tiled three.js output. Total time <60 min for 100 photos on GPU.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: 3D Viewer — FPS Walkthrough (P1) 🎯 MVP Core
|
||||||
|
|
||||||
|
- [ ] T023 Create `packages/viewer/` — three.js + Vite library package
|
||||||
|
- [ ] T024 Implement FPS camera controller (WASD + mouse look, collision detection, gravity)
|
||||||
|
- [ ] T025 Implement tap-to-walk controller (mobile: tap destination → navmesh pathfinding)
|
||||||
|
- [ ] T026 Implement mesh loader (tiled LODeD mesh streaming, Draco decompression)
|
||||||
|
- [ ] T027 Implement texture loading (progressive: low-res first, high-res on demand)
|
||||||
|
- [ ] T028 Implement mini-map overlay (bird's eye view with player position)
|
||||||
|
- [ ] T029 Implement measurement overlay toggle (room dimensions on walls/floor)
|
||||||
|
- [ ] T030 Implement WebGL fallback detection → 360° photo carousel mode
|
||||||
|
- [ ] T031 Implement touch controls (gyroscope look, pinch-zoom, two-finger walk)
|
||||||
|
- [ ] T032 Write Playwright E2E test: load walkthrough → verify 3D renders → verify WASD movement → verify tap-to-walk
|
||||||
|
|
||||||
|
**Checkpoint**: Open walkthrough link → walk through reconstructed space at 30+ FPS. WASD works. Click-to-walk works. Mini-map shows position.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: Web UI — Dashboard & Sharing (P1 + P2)
|
||||||
|
|
||||||
|
- [ ] T033 Create project creation flow (multi-step: name/address → photo upload → processing)
|
||||||
|
- [ ] T034 Create processing status page (progress bar, ETA, stage indicator)
|
||||||
|
- [ ] T035 Create walkthrough view page (embedded viewer + share controls)
|
||||||
|
- [ ] T036 Implement share link generation (public, no auth required)
|
||||||
|
- [ ] T037 Implement oEmbed embed code generator
|
||||||
|
- [ ] T038 Create dashboard grid (project cards with thumbnail, status, date)
|
||||||
|
- [ ] T039 Create project settings (edit name, address, delete)
|
||||||
|
|
||||||
|
**Checkpoint**: Agent creates project → uploads photos → sees processing progress → walkthrough appears → shares link → visitor navigates without login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: Auth, Teams & Billing (P3)
|
||||||
|
|
||||||
|
- [ ] T040 Set up better-auth with email + Google OAuth
|
||||||
|
- [ ] T041 Implement RBAC (owner/admin/member/viewer per team)
|
||||||
|
- [ ] T042 Create subscription tiers (Free: 3/month, Pro: 25/month, Enterprise: unlimited)
|
||||||
|
- [ ] T043 Implement Stripe checkout + webhook
|
||||||
|
- [ ] T044 Implement usage tracking + quota enforcement middleware
|
||||||
|
|
||||||
|
**Checkpoint**: User signs up → creates 3 projects on Free → gets upgrade prompt → pays via Stripe → quota increases.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Launch
|
||||||
|
|
||||||
|
- [ ] T045 Landing page with demo walkthrough embedded
|
||||||
|
- [ ] T046 Pricing page with 3 tiers
|
||||||
|
- [ ] T047 Email notifications (processing complete, processing failed, quota warning)
|
||||||
|
- [ ] T048 Mobile-responsive dashboard with bottom nav
|
||||||
|
- [ ] T049 Accessibility audit (WCAG 2.2 AA): keyboard nav, screen reader, contrast
|
||||||
|
- [ ] T050 Performance audit (Core Web Vitals, first mesh load <5s on 4G)
|
||||||
|
|
||||||
|
**Checkpoint**: Real tester (monyi's friends) create walkthrough from phone photos in <1h total. Lighthouse 90+.
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# InteriorScan — AGENTS.md
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
InteriorScan is a SaaS platform for interior photogrammetry targeting real estate. Users upload photos of a property, the system reconstructs a 3D mesh, and visitors navigate the space in first-person (WASD + click-to-walk) — like a Doom map of a real apartment.
|
||||||
|
|
||||||
|
**Purpose**: Filter properties before physical visits. Save time for buyers and agents.
|
||||||
|
|
||||||
|
**Stack**: Next.js 16 App Router · Turborepo · Drizzle ORM · PostgreSQL · Valkey · three.js · COLMAP/OpenMVS · Stripe
|
||||||
|
|
||||||
|
**Repo**: https://git.d0a1.es/hermes/interiorscan
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
interiorscan/
|
||||||
|
├── apps/web/ # Next.js 16 App Router (SSR + client)
|
||||||
|
│ ├── src/app/(marketing)/ # Landing, pricing, public pages
|
||||||
|
│ ├── src/app/(saas)/ # Auth-guarded dashboard
|
||||||
|
│ ├── src/app/api/ # Webhooks, health, processing callbacks
|
||||||
|
│ └── src/lib/3d/ # three.js viewer components (FPS controls, mesh loader)
|
||||||
|
├── apps/processor/ # Node.js worker — orchestrates COLMAP/OpenMVS pipeline
|
||||||
|
├── packages/
|
||||||
|
│ ├── auth/ # better-auth + RBAC
|
||||||
|
│ ├── db/ # Drizzle schema + migrations
|
||||||
|
│ ├── ui/ # shadcn/ui v4 + Tailwind tokens
|
||||||
|
│ ├── utils/ # Zod schemas, neverthrow, error catalog
|
||||||
|
│ └── ts-config/ # Shared TS configs
|
||||||
|
├── docker/ # Docker Compose (web + postgres + valkey + GPU worker)
|
||||||
|
└── .specify/ # Constitution, specs, plans
|
||||||
|
```
|
||||||
|
|
||||||
|
## Constitution
|
||||||
|
|
||||||
|
See `.specify/memory/constitution.md` — all development must comply.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
- **FPS navigation**: WASD + click-to-walk (both modes). Not 360° panos.
|
||||||
|
- **Mobile-first capture**: iPhone/Android photos as baseline. LiDAR = enhancement.
|
||||||
|
- **Self-hosted**: All infra self-hosted except Stripe.
|
||||||
|
- **API-first**: Every feature has REST API before UI.
|
||||||
|
- **Open exports**: OBJ, E57, LAS/LAZ, DXF minimum. IFC in P4+.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # Install dependencies
|
||||||
|
pnpm dev # Start dev server (all apps)
|
||||||
|
pnpm build # Production build
|
||||||
|
pnpm test # Run Vitest
|
||||||
|
pnpm lint # Biome check
|
||||||
|
pnpm typecheck # TypeScript strict check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standards Reference
|
||||||
|
|
||||||
|
| Layer | Choice | Standard |
|
||||||
|
|---|---|---|
|
||||||
|
| Auth | better-auth | [SECURITY.md](https://git.d0a1.es/d0a1-agentic-crew/agentic-standards/src/branch/main/standards/SECURITY.md) |
|
||||||
|
| Design | d0a1 palettes | [DESIGN_SYSTEM.md](https://git.d0a1.es/d0a1-agentic-crew/agentic-standards/src/branch/main/standards/DESIGN_SYSTEM.md) |
|
||||||
|
| CSS | #app[data-theme] selectors | Design system token pattern |
|
||||||
|
| Lint | Biome 2.x only | [STANDARDS.md](https://git.d0a1.es/d0a1-agentic-crew/agentic-standards/src/branch/main/standards/STANDARDS.md) |
|
||||||
|
| Testing | Vitest v4 + Playwright | tpl-next-saas pattern |
|
||||||
|
|
||||||
|
## Branch Strategy
|
||||||
|
|
||||||
|
- `main` — production-ready
|
||||||
|
- `feat/###-description` — feature branches from spec IDs
|
||||||
|
- `fix/description` — bugfixes
|
||||||
|
- Conventional commits: `feat:`, `fix:`, `docs:`, `chore:`, `refactor:`
|
||||||
@@ -1,3 +1,59 @@
|
|||||||
# interiorscan
|
# InteriorScan
|
||||||
|
|
||||||
Interior photogrammetry SaaS - Doom-map style 3D walkthroughs for real estate
|
**Turn apartment photos into navigable 3D walkthroughs.**
|
||||||
|
|
||||||
|
InteriorScan is a self-hosted SaaS platform for interior photogrammetry targeting real estate. Upload 30-100 photos of a property, the system reconstructs a 3D mesh (COLMAP + OpenMVS), and visitors navigate the space in first-person — like a Doom map of a real home.
|
||||||
|
|
||||||
|
## Why?
|
||||||
|
|
||||||
|
Physical property visits are time-consuming and often unnecessary. InteriorScan lets potential buyers walk through a reconstructed interior from their browser, filtering out properties that don't fit before scheduling a visit.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 📸 **Photo-first capture** — iPhone/Android photos as baseline, LiDAR optional
|
||||||
|
- 🎮 **FPS navigation** — WASD + mouse (desktop), tap-to-walk (mobile)
|
||||||
|
- 🔗 **Share & embed** — public links + oEmbed for listing pages
|
||||||
|
- 📐 **Measurements** — room dimensions overlaid on walkthrough
|
||||||
|
- 🗺️ **Floor plans** — DXF/PDF export from reconstructed geometry
|
||||||
|
- 🔌 **API-first** — every feature has REST API before UI
|
||||||
|
- 🏠 **Self-hosted** — PostgreSQL + Valkey + GPU worker, no vendor lock-in
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
| Layer | Technology |
|
||||||
|
|-------|-----------|
|
||||||
|
| Framework | Next.js 16 (App Router) |
|
||||||
|
| Monorepo | Turborepo + pnpm |
|
||||||
|
| Database | PostgreSQL + Drizzle ORM |
|
||||||
|
| Cache/Queue | Valkey |
|
||||||
|
| 3D Viewer | three.js |
|
||||||
|
| Processing | COLMAP + OpenMVS |
|
||||||
|
| Auth | better-auth |
|
||||||
|
| Billing | Stripe |
|
||||||
|
| Lint | Biome 2.x |
|
||||||
|
| Testing | Vitest v4 + Playwright |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
interiorscan/
|
||||||
|
├── apps/web/ # Next.js 16 App Router
|
||||||
|
├── apps/processor/ # GPU processing worker
|
||||||
|
├── packages/auth/ # better-auth + RBAC
|
||||||
|
├── packages/db/ # Drizzle schema + migrations
|
||||||
|
├── packages/viewer/ # three.js FPS walkthrough viewer
|
||||||
|
├── packages/ui/ # shadcn/ui v4 + Tailwind tokens
|
||||||
|
├── packages/utils/ # Zod, neverthrow, error catalog
|
||||||
|
└── .specify/ # Constitution, specs, plans
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache-2.0
|
||||||
Reference in New Issue
Block a user