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:
2026-05-28 20:26:22 +02:00
parent f44b1543d5
commit 3db7e47994
6 changed files with 634 additions and 2 deletions
+46
View File
@@ -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
+121
View File
@@ -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 |
+131
View File
@@ -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+.