kalei/docs/technical/kalei-claude-code-build-fra...

973 lines
76 KiB
Markdown
Raw Normal View History

# Kalei — Claude Code Build Framework
Version: 2.0
Date: 2026-02-20
Status: Implementation-ready technical framework
Author: Matt + Claude
This document is the operational build bible for constructing Kalei from zero to production using Claude Code as the primary development tool. It translates all existing architecture, design, and phase documents into a practical, session-by-session execution framework.
This is not a summary of what exists. This is the missing piece: how you actually sit down and build it.
---
## 1. How This Framework Works
Every section in this document maps to a concrete Claude Code session. Each session has a clear input (what you tell Claude Code), a clear output (what gets committed), and a definition of done (how you know it worked). You work through them in order. If a session fails validation, you fix it before moving on.
The framework is organized into build tracks that run roughly in sequence but with deliberate parallelism where safe. Each track contains numbered sessions. Each session is designed to be completable in a single focused Claude Code interaction (30-90 minutes of wall time).
### 1.1 Session Anatomy
Every session follows this structure:
1. Context: what Claude Code needs to know before starting.
2. Prompt: what you paste or describe to Claude Code.
3. Deliverables: what files get created or modified.
4. Validation: how you confirm it works.
5. Commit: what the git message should look like.
### 1.2 Working Principles
These rules apply to every session:
- Never skip validation. A session is not done until validation passes.
- Commit after every successful session. Small, clean commits.
- If Claude Code generates something that does not match the architecture docs, the architecture docs win.
- Every AI call goes through the AI gateway. No direct provider SDK imports in feature code. Ever.
- Every user-facing AI path goes through the safety gate first. No exceptions.
- TypeScript strict mode everywhere. No `any` unless explicitly justified.
- Tests are not optional. Every endpoint gets at least one happy-path and one failure-path test.
---
## 2. Toolchain Decisions (Locked)
These are final. Do not revisit during build.
### Backend
| Layer | Tool | Version Target | Why |
|---|---|---|---|
| Language | TypeScript | 5.x strict | Type safety across full stack, shared types |
| Runtime | Node.js | 22 LTS | Current LTS, native fetch, native test runner fallback |
| API framework | Fastify | 5.x | Fastest Node framework, plugin encapsulation model maps perfectly to our modular monolith. Note: v5 prohibits decorating request/reply with reference types — use onRequest hooks or getter decorators instead. |
| ORM / query | Drizzle ORM | Latest stable | Type-safe schemas as code, zero-overhead SQL, migration generation via drizzle-kit. Use prepared statements for hot-path queries (auth lookups, usage checks). |
| Database | PostgreSQL | 16 | JSONB, RLS, pgcrypto, mature ecosystem |
| Cache / counters | Redis | 7.x | Rate limiting, usage counters, session cache, idempotency store |
| AI SDK | openai (OpenAI-compatible SDK) | Latest | OpenRouter uses OpenAI-compatible API. Native streaming, structured output. Single SDK works for DeepSeek V3.2 (primary) and Claude Haiku (fallback) via OpenRouter gateway. |
| Password hashing | @node-rs/argon2 | Latest | Argon2id via native Rust binding, fastest safe option |
| JWT | jose | Latest | Standards-compliant, no native deps, supports all JWT operations |
### Mobile
| Layer | Tool | Version Target | Why |
|---|---|---|---|
| Framework | React Native + Expo | SDK 54+ (New Architecture enabled by default) | Fabric renderer + TurboModules = 60fps UI, precompiled iOS builds (10x faster), EAS builds + OTA updates |
| Navigation | Expo Router | v4+ | File-system routing, typed routes, deep linking, shared element transitions |
| Server state | TanStack Query (React Query) | v5 | Industry standard for data fetching in production React Native apps. Provides caching (staleTime/gcTime), background refetching, optimistic mutations, offline persistence via `networkMode: 'offlineFirst'`, and paused mutation resume. Replaces raw HTTP clients as the data layer. |
| Client state | Zustand | Latest | Minimal boilerplate, persist middleware with MMKV adapter for offline-first state |
| Local storage | react-native-mmkv | v4 | 30x faster than AsyncStorage, built-in AES encryption, synchronous reads. Used for auth tokens, user preferences, offline cache. Premium apps universally use MMKV over AsyncStorage. |
| Animations | react-native-reanimated | v4+ | UI-thread animations via shared values + useAnimatedStyle. Spring physics, layout animations, entering/exiting transitions, shared element transitions. Required for premium-feeling interactions. |
| Gestures | react-native-gesture-handler | Latest | Native gesture system — pan, pinch, rotation, tap. Composes with Reanimated for gesture-driven animations (swipe-to-dismiss, pull-to-refresh, card interactions). |
| Haptics | expo-haptics | Latest | Tactile feedback on key interactions: Turn save (success notification), action completion (light impact), fragment tap (selection), onboarding progression (soft impact). Standard in premium wellness apps. |
| Images | expo-image | Latest | Faster than React Native Image, aggressive caching, blurhash placeholders, animated image support. |
| Lists | @shopify/flash-list | Latest | 60fps scrolling for long lists, view recycling, superior to FlatList. Used for Gallery timeline, Turn history, and any scrollable content with 50+ items. |
### Shared / Tooling
| Layer | Tool | Version Target | Why |
|---|---|---|---|
| Validation | Zod | 3.x | Shared schemas between API and mobile, runtime + compile-time safety |
| Linting | Biome | Latest | Single tool for lint + format, faster than ESLint + Prettier combined |
| Unit testing | Vitest | Latest | Vite-powered, native TypeScript, compatible with Fastify test patterns |
| E2E testing | Maestro | Latest | Industry standard for mobile E2E testing. YAML-based flow definitions, visual regression, CI-friendly. Tests critical paths: onboarding, Turn flow, Mirror session, purchase. |
| CI | Gitea Actions (self-hosted, free) or GitHub Actions (2,000 free mins/month private, unlimited public) | N/A | Lint + test + type-check on every PR. Gitea is fully self-hosted on the VPS at zero cost. |
| Monorepo | pnpm workspaces | Native | Strict dependency resolution, disk-efficient, prevents phantom deps |
### 2.1 Package Manager
Use `pnpm` as the package manager. It is faster, more disk-efficient, and has strict dependency resolution that prevents phantom dependencies. Install globally:
```bash
npm install -g pnpm
```
### 2.2 What We Are NOT Using (And Why)
| Rejected | Reason |
|---|---|
| Express | Slower, no schema validation, no encapsulation model |
| Prisma | Heavy runtime, slower queries, migration lock-in |
| Supabase Cloud | 3x cost of self-hosting, unnecessary service overhead |
| RevenueCat | Third-party dependency in billing critical path |
| Redux / MobX | Overkill for this app's state complexity. Zustand + TanStack Query covers both client and server state |
| AsyncStorage | 30x slower than MMKV, no encryption, async-only reads. Not used in premium apps |
| Axios / ky / ofetch | TanStack Query is the data layer now. Raw HTTP clients are only used inside query/mutation functions, not directly in components. Use native fetch inside TanStack Query's queryFn. |
| React Navigation (standalone) | Expo Router wraps React Navigation with file-based routing — no reason to use React Navigation directly |
| Animated (built-in) | React Native's built-in Animated API runs on the JS thread. Reanimated runs on the UI thread — mandatory for 60fps premium animations. |
| Jest | Slower, heavier config, Vitest is drop-in compatible |
| ESLint + Prettier | Two tools where Biome does both faster |
| tRPC | Over-engineering for a mobile + API monorepo with Zod already shared |
| Detox | Slower, more complex setup than Maestro for E2E testing. Maestro's YAML flows are faster to write and maintain |
### 2.3 Zero-Cost Development Guarantee
Every tool and dependency in this stack can be used at zero cost during development. This section maps each component to its free path.
**All npm packages are free and open source.** Every library in the Backend, Mobile, and Shared toolchain tables is MIT, Apache 2.0, or BSD licensed. This includes Fastify, Drizzle ORM, TanStack Query, Zustand, MMKV, Reanimated, FlashList, Zod, Biome, Vitest, Pino, jose, @node-rs/argon2, and ioredis. Zero license costs, ever.
**Local infrastructure is free.** PostgreSQL and Redis run locally via Docker Compose (Docker Engine is free and open source). No cloud database or managed Redis required during development.
**Mobile builds are free locally.** Use `npx expo run:ios` and `npx expo run:android` for development builds — these compile using your local Xcode and Android Studio (both free). For production builds, use `eas build --local --platform ios` and `eas build --local --platform android` to build .ipa and .aab files on your machine at zero cost. EAS Cloud Build's free tier (30 builds/month) is available but optional. OTA updates via EAS Update are free for up to 1,000 active users.
**AI API costs are zero during development.** The mock AI provider (`mock.provider.ts`) returns deterministic, schema-valid responses for all unit and integration tests. You never hit real AI APIs during normal development. For manual testing against real AI, use OpenRouter credits — DeepSeek V3.2 via DeepInfra is so inexpensive ($0.26/$0.38 per MTok) that even extensive manual testing costs pennies. During development, you will spend effectively $0 on AI.
**Testing is free.** Vitest (MIT) for unit/integration tests. Maestro CLI (Apache 2.0) runs locally against simulators/emulators at no cost — do not use Maestro Cloud. k6 (Apache 2.0) for load testing runs locally.
**Error tracking is free.** GlitchTip is self-hosted via Docker Compose on your dev machine (or later on the VPS). It is Sentry-compatible and 100% open source. No SaaS subscription needed.
**Analytics is free.** PostHog can be self-hosted via Docker for up to ~100k events/month, or use their cloud free tier (1M events/month, no credit card required). For development, self-hosted PostHog in Docker Compose alongside Postgres and Redis costs nothing.
**Push notifications are free.** Expo Push Notification API is free with a 600 notifications/second/project rate limit. No paid tier required.
**CI/CD is free.** Gitea Actions is self-hosted and free. GitHub Actions offers 2,000 minutes/month free for private repos, unlimited for public repos.
**SSL/HTTPS is free.** Caddy auto-provisions Let's Encrypt certificates. Let's Encrypt is a free certificate authority.
**Costs that only apply at launch (not during development):**
| Item | Cost | When |
|---|---|---|
| Netcup VPS 1000 G12 | €8.45/month | Production deployment |
| Apple Developer Program | $99/year | App Store submission |
| Google Play Developer | $25 one-time | Play Store submission |
| OpenRouter API — DeepSeek V3.2 via DeepInfra (production traffic) | ~$2-8/month at launch scale | Live users hitting AI endpoints |
| Domain name | ~$10-15/year | Production domain |
Total development cost: **$0.** Total launch cost: approximately **€16/month** + one-time store fees.
---
## 3. Repository Architecture
This is the exact folder structure. Claude Code should scaffold this in the first session.
```
kalei/
package.json # Workspace root
pnpm-workspace.yaml # Workspace config
tsconfig.base.json # Shared TS config
biome.json # Lint + format config
.env.example # Root env template
.gitignore
apps/
mobile/ # Expo React Native app
app/ # Expo Router file-based routes
(tabs)/ # Tab navigator group
_layout.tsx # Tab layout with 5 tabs
index.tsx # Home / daily focus
mirror.tsx # Mirror entry
turn.tsx # Kaleidoscope entry
lens.tsx # Lens goals
gallery.tsx # Gallery / history
(auth)/ # Auth flow group
_layout.tsx
login.tsx
register.tsx
onboarding.tsx
(spectrum)/ # Spectrum (ships in v1)
_layout.tsx
dashboard.tsx
weekly.tsx
monthly.tsx
_layout.tsx # Root layout (auth gate)
src/
api/ # API layer
client.ts # Base fetch wrapper with auth header injection
query-client.ts # TanStack Query client config (staleTime, gcTime, offline persistence)
queries/ # TanStack Query hooks organized by feature
use-auth.ts # useLogin, useRegister, useRefresh mutations
use-mirror.ts # useMirrorSession, useSendMessage, useReframe queries/mutations
use-turn.ts # useCreateTurn, useTurns, useSaveTurn
use-lens.ts # useGoals, useCreateGoal, useCompleteAction, useDailyAffirmation
use-spectrum.ts # useWeeklyInsight, useMonthlyInsight
use-billing.ts # useEntitlements
keys.ts # Query key factory for organized cache invalidation
stores/ # Zustand stores (client state only — NOT server state)
auth.store.ts # Token state, persisted to MMKV
ui.store.ts # UI preferences, theme mode, onboarding state
offline-queue.store.ts # Queued mutations for offline-first
lib/ # Core utilities
storage.ts # MMKV instance setup with encryption
haptics.ts # Centralized haptic feedback helpers (tapHaptic, successHaptic, etc.)
analytics.ts # PostHog event tracking (self-hosted or cloud free tier — 1M events/month free)
components/ # Reusable UI components
ui/ # Primitives (Button, Card, Input, TextArea, etc.)
animations/ # Reanimated animation components (FadeIn, SlideUp, PulseGlow, KaleidoscopeSpinner)
mirror/ # Mirror-specific components
turn/ # Turn-specific components
lens/ # Lens-specific components
spectrum/ # Spectrum-specific components
hooks/ # Custom React hooks
use-keyboard-aware.ts # Keyboard avoidance for writing screens
use-network-state.ts # Online/offline detection
use-app-state.ts # Foreground/background lifecycle
utils/ # Helpers, formatters, constants
theme/ # Colors, typography, spacing tokens, dark mode support
types/ # Mobile-specific types
assets/ # Images, fonts, Lottie animations
e2e/ # Maestro E2E test flows
onboarding.yaml
turn-flow.yaml
mirror-session.yaml
purchase-flow.yaml
app.config.ts # Expo config (newArchEnabled: true is default in SDK 54)
package.json
tsconfig.json
.env.example
services/
api/ # Fastify API server
src/
server.ts # Fastify app factory
config.ts # Environment config with Zod validation
modules/ # Feature modules (Fastify plugins)
auth/
auth.routes.ts
auth.service.ts
auth.schemas.ts
auth.test.ts
mirror/
mirror.routes.ts
mirror.service.ts
mirror.schemas.ts
mirror.test.ts
turn/
turn.routes.ts
turn.service.ts
turn.schemas.ts
turn.test.ts
lens/
lens.routes.ts
lens.service.ts
lens.schemas.ts
lens.test.ts
spectrum/
spectrum.routes.ts
spectrum.service.ts
spectrum.schemas.ts
spectrum.test.ts
billing/
billing.routes.ts
billing.service.ts
billing.schemas.ts
billing.test.ts
plugins/ # Cross-cutting Fastify plugins
auth.plugin.ts # JWT verification decorator
entitlement.plugin.ts # Plan gating middleware
rate-limit.plugin.ts # Redis-backed rate limiting
request-id.plugin.ts # Request ID injection
error-handler.plugin.ts
gateway/ # AI Gateway abstraction
ai-gateway.ts # Main gateway interface
providers/
openrouter.provider.ts # Primary: DeepSeek V3.2 via DeepInfra/Fireworks, Fallback: Claude Haiku
prompts/
mirror-detect.prompt.ts
mirror-reframe.prompt.ts
turn-reframe.prompt.ts
lens-affirmation.prompt.ts
spectrum-weekly.prompt.ts
spectrum-monthly.prompt.ts
output-schemas/ # Zod schemas for AI output validation
mirror-fragments.schema.ts
turn-perspectives.schema.ts
lens-output.schema.ts
safety/ # Safety service
safety.service.ts # Multi-stage crisis filter
crisis-keywords.ts # Deterministic keyword sets
crisis-resources.ts # Regional crisis hotline data
safety.test.ts
db/ # Database layer
client.ts # Drizzle client setup
schema/ # Drizzle schema definitions
users.ts
auth.ts
mirror.ts
turn.ts
lens.ts
spectrum.ts
billing.ts
safety.ts
usage.ts
index.ts # Re-exports all schemas
migrations/ # Generated by drizzle-kit
seed.ts # Dev seed data
redis/
client.ts # Redis connection
usage-counter.ts # Per-user usage tracking
rate-limiter.ts # Sliding window rate limiter
idempotency.ts # Idempotency key store
workers/ # Background job processors
spectrum-weekly.worker.ts
spectrum-monthly.worker.ts
push-notification.worker.ts
billing-reconciliation.worker.ts
utils/
logger.ts # Pino structured logger
crypto.ts # Column-level encryption helpers
errors.ts # Custom error classes
drizzle.config.ts # Drizzle Kit configuration
package.json
tsconfig.json
vitest.config.ts
.env.example
packages/
shared/ # Shared types and schemas
src/
schemas/ # Zod schemas shared between API and mobile
auth.schema.ts
mirror.schema.ts
turn.schema.ts
lens.schema.ts
spectrum.schema.ts
billing.schema.ts
types/ # TypeScript type exports
index.ts
constants/ # Shared constants
plans.ts # Plan definitions and limits
cognitive-distortions.ts # Fragment type taxonomy
crisis-patterns.ts # Shared crisis detection patterns
package.json
tsconfig.json
infra/
docker/
docker-compose.yml # Postgres + Redis for local dev
docker-compose.prod.yml # Production compose (VPS deployment)
scripts/
setup-local.sh # One-command local environment setup
deploy.sh # Production deployment script
backup-db.sh # PostgreSQL backup script
restore-db.sh # PostgreSQL restore script
nginx/
kalei.conf # Nginx reverse proxy config
caddy/
Caddyfile # Alternative: Caddy auto-HTTPS config
docs/ # Existing documentation (unchanged)
```
### 3.1 Why This Structure
**Backend: Fastify plugin encapsulation.** Each module in `services/api/src/modules/` is a self-contained Fastify plugin that registers its own routes, schemas, and services. Fastify v5 enforces this cleanly — you cannot decorate request/reply with reference types directly (use onRequest hooks or getter decorators instead). Each module can be tested in isolation, and extraction to a separate service later is a matter of moving the plugin to its own process.
**Mobile: Separation of server state and client state.** This is the pattern every premium React Native app follows in 2026. TanStack Query owns all server state (API data, caching, background refetching, optimistic updates). Zustand owns only client state (auth tokens, UI preferences, offline queue). Components never call fetch directly — they use query hooks from `src/api/queries/`. This separation makes offline-first behavior trivial: TanStack Query's `networkMode: 'offlineFirst'` plus MMKV-backed Zustand persistence means the app works without network.
**Shared schemas.** The `packages/shared` workspace ensures that Zod schemas defined once are used identically for API request validation, TanStack Query response typing, and mobile form validation. When the API says a turn response has exactly 3 perspectives, the mobile app knows this at compile time.
**AI gateway isolation.** The `gateway/` directory is intentionally separate from `modules/`. Feature code calls the gateway; the gateway calls providers. Feature code never imports provider SDKs. This is the single most important architectural boundary in the entire codebase.
### 3.2 Premium UX Architecture Principles
These patterns separate a forgettable app from a premium one:
**60fps everywhere.** All animations run on the UI thread via Reanimated shared values. Never animate with React state or the built-in Animated API. Screen transitions use Reanimated's layout animations (FadeIn, SlideInRight). The Turn kaleidoscope animation uses withRepeat + withSpring for organic-feeling motion.
**Haptic vocabulary.** Define a consistent haptic language: `Haptics.selectionAsync()` for tapping fragments, `Haptics.impactAsync(Light)` for completing actions, `Haptics.notificationAsync(Success)` for saving turns, `Haptics.impactAsync(Soft)` for onboarding progression. Centralize in `src/lib/haptics.ts` so the haptic vocabulary stays consistent.
**Offline-first by default.** Every screen must work without network. MMKV persists Zustand state synchronously. TanStack Query caches API responses with configurable stale times. Mutations queue locally and resume when connectivity returns (via `resumePausedMutations()`). The Mirror writing experience must never be interrupted by network issues.
**Optimistic UI.** When the user saves a Turn, the UI updates immediately via TanStack Query's optimistic mutation pattern — the save icon fills instantly, and the server sync happens in the background. If it fails, it rolls back. The user never waits.
**Image optimization.** Use `expo-image` with blurhash placeholders for any loaded images. Profile avatars, onboarding illustrations, and Spectrum chart images all load with a color-blur placeholder that resolves to the full image.
**Accessibility from day one.** All interactive elements have accessibility labels. The Mirror writing area supports dynamic type sizes. Color contrast meets WCAG AA. VoiceOver/TalkBack navigation works for all core flows.
---
## 4. Build Track A — Foundation (Weeks 1-2)
Duration: 3-5 days
Goal: Everything runs. Nothing is broken. You can develop features.
### Session A1: Repository Scaffold
Context: Starting from empty repo.
Prompt to Claude Code:
> Initialize a pnpm monorepo with three workspaces: apps/mobile, services/api, and packages/shared. Set up tsconfig.base.json with strict mode. Add biome.json with reasonable defaults. Add .gitignore covering node_modules, .env, dist, .expo, and drizzle migration artifacts. Create package.json files for each workspace. The API workspace should use Fastify 5, Drizzle ORM with postgres-js driver, zod, pino, ioredis, jose, and @node-rs/argon2. The mobile workspace should use Expo SDK 54 with expo-router, and these critical dependencies: @tanstack/react-query for server state, zustand for client state, react-native-mmkv v4 for encrypted local storage (use createMMKV API), react-native-reanimated v4 for UI-thread animations, react-native-gesture-handler for native gestures, expo-haptics for tactile feedback, expo-image for optimized image loading, and zod for shared validation. The shared workspace should export Zod schemas and TypeScript types. Add a pnpm-workspace.yaml. Make the whole thing build and type-check cleanly.
Deliverables:
- All package.json files with correct dependencies
- tsconfig files with project references
- biome.json
- pnpm-workspace.yaml
- Clean `pnpm install` and `pnpm -r run type-check`
Validation:
```bash
pnpm install
pnpm -r run type-check # Zero errors
pnpm biome check . # Zero errors
```
### Session A2: Docker Infrastructure
Context: Repo scaffold exists.
Prompt to Claude Code:
> Create infra/docker/docker-compose.yml with PostgreSQL 16 and Redis 7. Postgres should use kalei/kalei credentials with a kalei database. Add health checks for both services. Create a root-level Makefile with targets: up (start docker), down (stop docker), reset-db (drop and recreate), and logs. Also create .env.example files for both API and mobile workspaces with all expected environment variables documented with comments.
Deliverables:
- docker-compose.yml with health checks
- Makefile
- .env.example files
Validation:
```bash
make up
docker compose -f infra/docker/docker-compose.yml ps # Both healthy
make down
```
### Session A3: API Server Skeleton
Context: Docker running, dependencies installed.
Prompt to Claude Code:
> Create the Fastify server factory in services/api/src/server.ts. It should: load config from environment using Zod validation (src/config.ts), register CORS, helmet, and sensible plugins, add a request ID plugin that generates UUIDs and injects them into pino logs, add a global error handler that logs errors and returns structured JSON errors, register a GET /health endpoint that checks Postgres and Redis connectivity and returns status/uptime/version. Add the database client (src/db/client.ts) using Drizzle ORM with postgres-js as the driver. Add the Redis client (src/redis/client.ts) using ioredis with reconnection strategy. Wire it all up in an index.ts that starts the server. Use Fastify's plugin encapsulation model — each capability should be a registered plugin.
Deliverables:
- server.ts (app factory)
- config.ts (Zod-validated env)
- db/client.ts (Drizzle + postgres-js)
- redis/client.ts (ioredis)
- plugins/request-id.plugin.ts
- plugins/error-handler.plugin.ts
- Health endpoint returning JSON
Validation:
```bash
make up
cd services/api && pnpm dev
curl http://localhost:8080/health # Returns JSON with status: ok
```
### Session A4: Mobile App Skeleton
Context: API server running.
Prompt to Claude Code:
> Create the Expo app in apps/mobile using Expo Router with file-based routing and the New Architecture (enabled by default in SDK 54). Set up the root layout (_layout.tsx) with: a TanStack Query provider (QueryClientProvider with defaults: staleTime 5 minutes, gcTime 10 minutes, networkMode offlineFirst), a GestureHandlerRootView wrapper, and an auth gate that checks for a stored token in MMKV. Set up MMKV storage in src/lib/storage.ts using createMMKV with encryption enabled. Set up the TanStack Query client in src/api/query-client.ts. Create a Zustand auth store in src/stores/auth.store.ts that persists tokens to MMKV (using zustand/middleware persist with a custom MMKV storage adapter). Create the (auth) group with login and register screens (just UI shells with Reanimated FadeIn entering animations). Create the (tabs) group with 5 tabs: Home, Mirror, Turn, Lens, Gallery. Each tab should show its name and a placeholder with a subtle FadeIn.duration(300) entering animation. Set up the theme directory with Kalei's color tokens (primary deep teal #1A3A3A, accent amber #D4A574, background warm off-white #FAF8F5, text near-black #1A1A1A) and typography scale. Create a TanStack Query hook in src/api/queries/use-health.ts that fetches /health. Create src/lib/haptics.ts with centralized haptic helpers (tapHaptic, successHaptic, errorHaptic, selectionHaptic) wrapping expo-haptics. Display the health status on the Home tab using the useHealth query hook.
Deliverables:
- Expo Router file structure with all route groups
- Root layout with TanStack Query provider, GestureHandlerRootView, auth gate
- MMKV storage setup with encryption
- TanStack Query client with offline-first defaults
- Zustand auth store persisted to MMKV
- Haptics helper library
- 5 tab screens with Reanimated entering animations
- Theme tokens and typography scale
- useHealth query hook calling /health
- Query key factory in src/api/queries/keys.ts
Validation:
```bash
cd apps/mobile && npx expo start
# Open on device (not Expo Go — need dev build for MMKV/Reanimated native modules)
# See tabs with fade-in animations, see health status from API
# Kill app and reopen — auth state persists from MMKV
```
### Session A5: Database Schema v1
Context: API server and Drizzle client working.
Prompt to Claude Code:
> Create the full Phase 1 database schema using Drizzle ORM for PostgreSQL. Define all tables in separate files under services/api/src/db/schema/. Tables needed: users (id uuid, email, password_hash, display_name, role, created_at, updated_at), profiles (user_id FK, onboarding_complete, timezone, preferred_reframe_style, created_at, updated_at), auth_sessions (id uuid, user_id FK, device_info jsonb, ip_address, created_at, expires_at), refresh_tokens (id uuid, user_id FK, token_hash, session_id FK, created_at, expires_at, revoked_at), subscriptions (id uuid, user_id FK, plan enum free/prism/prism_plus, store enum apple/google, store_subscription_id, status enum, current_period_start, current_period_end, created_at, updated_at), entitlement_snapshots (id uuid, user_id FK, plan, features jsonb, valid_from, valid_until), turns (id uuid, user_id FK, input_text encrypted, perspectives jsonb, micro_action text, reframe_style, saved boolean, created_at), mirror_sessions (id uuid, user_id FK, status enum, started_at, ended_at, reflection_text), mirror_messages (id uuid, session_id FK, user_id FK, content encrypted, sequence_number, created_at), mirror_fragments (id uuid, message_id FK, user_id FK, fragment_type, start_offset, end_offset, confidence, reframe_text, created_at), lens_goals (id uuid, user_id FK, title, description, status, created_at, updated_at), lens_actions (id uuid, goal_id FK, user_id FK, title, completed boolean, completed_at, due_date, created_at), ai_usage_events (id uuid, user_id FK, feature, model, provider, input_tokens, output_tokens, cost_usd, latency_ms, created_at), safety_events (id uuid, user_id FK, trigger_type, trigger_content_hash, action_taken, created_at). Use proper indexes on user_id and created_at for all tables. Use Drizzle relations for type-safe joins. Create an index.ts that re-exports everything. Then configure drizzle.config.ts and generate the initial migration with drizzle-kit generate.
Deliverables:
- Schema files for all tables
- Drizzle relations defined
- drizzle.config.ts
- Generated SQL migration
- Clean migration apply
Validation:
```bash
cd services/api
pnpm drizzle-kit generate # Generates migration SQL
pnpm drizzle-kit migrate # Applies to local Postgres
pnpm drizzle-kit studio # Opens Drizzle Studio, verify all tables exist
```
### Session A6: Shared Schemas and Quality Gates
Context: Schema exists, both apps scaffold complete.
Prompt to Claude Code:
> Create the shared Zod schemas in packages/shared/src/schemas/ for all API request/response contracts. Auth schemas: RegisterInput, LoginInput, TokenResponse, ProfileResponse. Mirror schemas: CreateSessionInput, SendMessageInput, FragmentResponse, ReframeResponse, ReflectionResponse. Turn schemas: CreateTurnInput (input_text, reframe_style enum), TurnResponse (3 perspectives array + micro_action). Lens schemas: CreateGoalInput, GoalResponse, CreateActionInput, DailyAffirmationResponse. Also create packages/shared/src/constants/plans.ts with plan definitions including feature limits (free: 3 turns/day, 2 mirror/week; prism: unlimited; prism_plus: unlimited + spectrum). Add packages/shared/src/constants/cognitive-distortions.ts with the 10 cognitive distortion types used for fragment detection. Then set up Vitest config for the API workspace with a test that imports the shared schemas. Set up Biome and add scripts to root package.json: lint, format, test, type-check, e2e (runs `maestro test apps/mobile/e2e/`). Add a CI-ready check script that runs lint + format + type-check + test (unit/integration). E2E tests via Maestro run separately in the release pipeline since they require a running device/emulator.
Deliverables:
- All shared Zod schemas
- Plan constants with limits
- Cognitive distortion taxonomy
- Vitest configuration
- Root-level quality scripts
- At least one passing test
Validation:
```bash
pnpm run check # Runs lint + format + type-check + test — all pass
```
---
## 5. Build Track B — Platform Core (Weeks 1-3)
Duration: 2-3 weeks
Goal: Auth, entitlements, AI gateway, safety, and rate limiting are production-grade.
### Session B1: Auth — Registration and Login
Prompt to Claude Code:
> Implement the auth module in services/api/src/modules/auth/. Create auth.routes.ts as a Fastify plugin that registers POST /auth/register and POST /auth/login. Registration should: validate input with shared Zod schema, check email uniqueness, hash password with Argon2id, create user + profile, generate JWT access token (15 min TTL) and refresh token (7 day TTL), store refresh token hash in DB, return both tokens. Login should: validate credentials, verify password, create new session with device info from user-agent, generate token pair, return tokens. Use jose for JWT signing with RS256 or HS256 based on config. Create auth.service.ts for business logic and use Drizzle for data access. Add auth.schemas.ts importing from shared schemas. Write tests for: successful registration, duplicate email rejection, successful login, wrong password rejection.
### Session B2: Auth — Token Refresh and Sessions
Prompt to Claude Code:
> Add POST /auth/refresh and POST /auth/logout to the auth module. Refresh should: accept refresh token, verify it exists and is not expired or revoked, rotate it (revoke old, issue new pair), return new tokens. This implements refresh token rotation — if a revoked token is reused, revoke ALL tokens for that user (compromise detection). Logout should: revoke the current refresh token and mark the session as ended. Add GET /me that returns the current user profile. Add PATCH /me/profile for updating display_name, timezone, and preferred_reframe_style. Create the auth.plugin.ts in plugins/ that adds a Fastify decorator `authenticate` — a preHandler hook that verifies the JWT access token, extracts user ID, and decorates the request with `request.user`. Test: full token lifecycle (register, use access token, refresh, use new token, logout, verify old refresh fails).
### Session B3: Entitlement and Plan Gating
Prompt to Claude Code:
> Implement the entitlement system. Create plugins/entitlement.plugin.ts as a Fastify plugin that adds a `requirePlan` decorator. This decorator takes a minimum plan level and returns a preHandler that: loads the user's current entitlement snapshot, checks if their plan meets the minimum, returns 403 with a clear upgrade message if not. Also implement feature-specific gates: a `checkTurnLimit` preHandler that reads today's turn count from Redis and enforces the daily cap for free users (3/day), and a `checkMirrorLimit` preHandler that reads this week's mirror session count and enforces the weekly cap for free users (2/week). Create billing.routes.ts with webhook endpoints for Apple and Google (POST /billing/webhooks/apple and /billing/webhooks/google) — these should parse notification payloads, update subscriptions table, and write new entitlement snapshots. For now, implement the webhook signature verification as a placeholder. Add GET /billing/entitlements that returns the user's current plan and feature limits with usage counts. Test: free user hitting turn limit, prism user bypassing limit, entitlement check after plan change.
### Session B4: AI Gateway — Core Abstraction
Prompt to Claude Code:
> Build the AI gateway in services/api/src/gateway/. Create ai-gateway.ts with a TypeScript interface: AIGateway with methods `generate(request: AIRequest): Promise<AIResponse>` and `stream(request: AIRequest): AsyncGenerator<AIChunk>`. The AIRequest type should include: feature (mirror_detect | mirror_reframe | turn_reframe | lens_affirmation | spectrum_weekly | spectrum_monthly), messages array, model override (optional), temperature, max_tokens, output_schema (Zod schema for structured output validation). Create providers/anthropic.provider.ts that implements this interface using @anthropic-ai/sdk. It should: use prompt caching by marking system message content blocks with `cache_control: { type: "ephemeral" }` (this tells Anthropic to cache the system prompt — subsequent calls within a 5-minute window pay only 10% of base input rate, saving 40-50% on input costs), support streaming via the SDK's stream method, extract token usage from response metadata, validate output against the provided Zod schema and retry once on validation failure. Create providers/openai-compatible.provider.ts as the Venice/Groq/OpenRouter adapter using the OpenAI-compatible API format. The gateway factory should read config to determine which provider to use per feature, with fallback chains. Log every call to ai_usage_events with feature, model, provider, token counts, cost estimate, and latency. Test: mock provider returns valid structured output, mock provider returns invalid output and retry succeeds, token usage logging works.
### Session B5: AI Gateway — Prompt Templates
Prompt to Claude Code:
> Create all prompt templates in services/api/src/gateway/prompts/. Each template exports a function that takes context and returns the messages array for the AI gateway. Mirror fragment detection (mirror-detect.prompt.ts): system prompt instructs the model to analyze freeform writing and identify cognitive distortions from our taxonomy (all-or-nothing, catastrophizing, emotional reasoning, fortune telling, labeling, magnification, mental filtering, mind reading, overgeneralization, should statements). Output must be a JSON array of fragments with: type, start_offset, end_offset, confidence (0-1), and a brief explanation. Mirror inline reframe (mirror-reframe.prompt.ts): takes the original text and a specific fragment, generates a gentle compassionate reframe (2-3 sentences). Turn reframe (turn-reframe.prompt.ts): system prompt generates exactly 3 perspective reframes (compassionate, rational, growth-oriented) plus one micro-action as an if-then implementation intention. Output is strictly structured JSON. Lens affirmation (lens-affirmation.prompt.ts): takes user's active goals and generates a personalized daily affirmation. Create corresponding output validation schemas in output-schemas/ using Zod. Each prompt template should include a version string (e.g., "mirror-detect-v1") for tracking prompt revisions. Test: each template produces valid messages arrays, output schemas validate correct and reject incorrect shapes.
### Session B6: Safety Service
Prompt to Claude Code:
> Build the safety service in services/api/src/safety/. Create crisis-keywords.ts with deterministic keyword and phrase sets for crisis detection: explicit self-harm language, suicidal ideation phrases, immediate danger phrases, and severe distress indicators. These must be comprehensive but avoid false positives on common expressions. Create safety.service.ts with a multi-stage crisis filter: Stage 1 is deterministic keyword/regex matching (instant, no AI call). If Stage 1 flags, immediately return crisis response — no further processing. Stage 2 (optional, for ambiguous cases): send flagged text to AI gateway for confirmation with a safety-specific prompt that returns a confidence score. If confirmed, return crisis response. Create crisis-resources.ts with structured data for crisis hotlines by region (start with US: 988 Suicide and Crisis Lifeline, Crisis Text Line). The crisis response payload should include: is_crisis boolean, resources array with name/phone/text/url per resource, a compassionate message, and explicit instruction that the content was NOT reframed. Wire the safety service as a preHandler on all AI-facing routes (mirror messages, turn creation, lens if accepting user input). Log every safety event to safety_events table. Test: known crisis phrases trigger immediate response, safe text passes through, ambiguous text reaches Stage 2, crisis response never contains reframed content.
### Session B7: Rate Limiting and Usage Metering
Prompt to Claude Code:
> Build Redis-backed rate limiting and usage tracking. Create redis/rate-limiter.ts implementing a sliding window rate limiter. It should support: per-IP rate limiting (general API protection, 100 req/min), per-user rate limiting (feature-specific, configurable), and burst allowance. Create redis/usage-counter.ts for tracking: daily turn count per user, weekly mirror session count per user, monthly token usage per user. Create redis/idempotency.ts that stores idempotency keys with TTL — if a request includes an X-Idempotency-Key header and we've seen it before, return the cached response. Create plugins/rate-limit.plugin.ts that registers the general rate limiter on all routes and exposes decorators for feature-specific limits. Create a usage cost tracking function that estimates USD cost from token counts based on current model pricing (configurable). Add middleware that records every AI-backed request to ai_usage_events. Test: rate limit kicks in at threshold, idempotency key returns cached response, usage counters increment correctly.
### Session B8: Push Notification Infrastructure
Prompt to Claude Code:
> Set up push notification infrastructure. Install expo-notifications in the mobile app and configure it to request permissions and register for push tokens on login. Create a notification service in the API (services/api/src/modules/notifications/) that: stores device push tokens per user session, sends push notifications via Expo Push API (which handles both APNs and FCM routing), supports notification types: daily_affirmation_reminder, mirror_nudge, weekly_spectrum_ready, turn_streak_reminder. Create a notification preferences table in the schema so users can toggle notification types on/off. Create a worker (workers/push-notification.worker.ts) that runs scheduled notifications: daily affirmation reminder at user's preferred time (default 8am local), weekly spectrum summary notification on Monday morning. For development, log notifications to console instead of sending. Test: push token registration, notification send via Expo Push API, user preference toggle, scheduled notification timing.
### Session B9: Observability Baseline
Prompt to Claude Code:
> Set up structured logging and error tracking. Configure Pino logger in utils/logger.ts with: JSON output in production, pretty output in development, request ID correlation, user ID hash (never log raw user ID), redaction of sensitive fields (authorization headers, passwords, tokens). Create a Fastify hook that logs request start (method, url, request_id) and response end (status, duration_ms, request_id). Add GlitchTip integration for error tracking — GlitchTip is free and open source, self-hosted via Docker Compose alongside our existing Postgres and Redis (add it to docker-compose.yml). Install @sentry/node (GlitchTip is Sentry-compatible, same SDK) and create an initialization plugin that points to the local GlitchTip instance. For development, GlitchTip runs on your machine; in production, it runs on the VPS. Zero SaaS cost. Create a /metrics endpoint that returns: request count by endpoint, error count by endpoint, AI token usage by feature (daily/monthly), AI cost by feature, active sessions count, rate limit denial count. For now this can be a simple JSON endpoint — we'll add Prometheus format later if needed. Test: verify request logs contain request_id, verify error tracking captures unhandled errors, verify metrics endpoint returns non-zero data after some requests.
---
## 6. Build Track C — Core Experience (Weeks 4-8)
Duration: 3-5 weeks
Goal: Users can complete the Mirror to Turn to Lens flow end-to-end.
### Session C1: Mirror — Session Lifecycle API
Prompt to Claude Code:
> Implement the Mirror module backend. POST /mirror/sessions creates a new session (status: active). POST /mirror/messages accepts message content, runs it through the safety gate, then through the AI gateway for fragment detection. The response includes the original message with an array of detected fragments (type, offsets, confidence). Each fragment above the confidence threshold (0.7) is stored in mirror_fragments. POST /mirror/fragments/:id/reframe triggers an inline reframe for a specific fragment — calls AI gateway with the original context and fragment, returns the reframed perspective. POST /mirror/sessions/:id/close ends the session and triggers a reflection generation — the AI summarizes the session's themes, patterns noticed, and a gentle closing thought. GET /mirror/sessions lists sessions with pagination. DELETE /mirror/sessions/:id soft-deletes a session. All endpoints enforce authentication, safety precheck, and entitlement limits. The message content should be encrypted at rest using the column-level encryption helper. Test: full session lifecycle, fragment detection returns valid offsets, reframe returns compassionate text, reflection summarizes themes, safety gate blocks crisis content.
### Session C2: Mirror — Mobile UI
Prompt to Claude Code:
> Build the Mirror screen in the mobile app as a premium writing experience. The compose UI should feel like a peaceful writing space — warm off-white background (#FAF8F5), gentle typography (system serif or Inter at 18px), generous padding, no distracting UI elements. Use keyboard-aware scrolling so the text input stays visible above the keyboard. As the user types and pauses (debounce 2 seconds after last keystroke), use a TanStack Query mutation (useSendMessage from src/api/queries/use-mirror.ts) to send content to the API for fragment detection. When fragments come back, render subtle highlights on the detected text — use a warm amber (#D4A574) underline with a Reanimated FadeIn.duration(400) animation, not aggressive red highlighting. Add Haptics.selectionAsync() when the user taps a highlighted fragment. Show a bottom sheet card (animated with Reanimated SlideInDown.springify()) with the fragment type name (e.g., "Catastrophizing"), a brief explanation, and a "See another perspective" button that triggers the reframe mutation. The reframe slides in as an animated card with FadeInUp. When the user closes the session, show the Reflection as a full-screen gentle summary with Reanimated FadeIn. All API calls should go through TanStack Query hooks — useMirrorSession, useSendMessage, useReframe, useCloseSession. Configure the useSendMessage mutation with optimistic updates so fragment highlights appear immediately from cached data while the server processes. Handle offline gracefully using TanStack Query's offline mutation persistence — queue messages locally in the mutation cache and resume when connectivity returns via resumePausedMutations.
### Session C3: Turn — Reframe Engine API
Prompt to Claude Code:
> Implement the Turn module backend. POST /turns accepts input_text and optional reframe_style (compassionate, rational, growth, or all). Runs safety precheck. Calls AI gateway with the turn-reframe prompt. Returns exactly 3 perspectives, each with: style label, reframed text (2-3 sentences), and a brief explanation of the cognitive shift. Also returns one micro_action as an if-then implementation intention (e.g., "If I notice myself catastrophizing about work, then I will take 3 breaths and name one thing I can control"). POST /turns/:id/save toggles the saved state. GET /turns returns paginated history. GET /turns/:id returns a single turn with full details. Support streaming for the reframe generation — the API should use Server-Sent Events so the mobile app can show perspectives appearing in real-time. Enforce daily turn limits for free users. Test: valid turn creates 3 perspectives, streaming delivers chunks, save toggle works, daily limit enforced, crisis content blocked.
### Session C4: Turn — Mobile UI
Prompt to Claude Code:
> Build the Turn (Kaleidoscope) screen as the premium signature experience. The input should be a focused single-purpose screen: a large text input with placeholder "What thought is weighing on you?" and a "Turn" button that pulses gently using Reanimated withRepeat(withTiming()) when the input has text. Add Haptics.impactAsync(Medium) when the Turn button is pressed. When processing, show a kaleidoscope animation built with Reanimated — rotate a geometric SVG pattern using useSharedValue + withRepeat(withSpring()) for organic, non-mechanical motion. This is the core product moment and must feel magical. As perspectives stream in via SSE, render them as cards that appear one at a time using Reanimated's layout animation FadeInDown.springify().delay(index * 200) — each card shows the perspective style icon, the reframed text, and the cognitive shift explanation. The micro-action appears last with a distinct card style and FadeInUp animation. The save button (bookmark icon) should use Reanimated withSpring for a satisfying scale bounce on tap, with Haptics.notificationAsync(Success) feedback. Use TanStack Query for all data: useCreateTurn mutation (with streaming SSE handling), useTurns query for history, useSaveTurn mutation with optimistic update (the save icon fills immediately, server syncs in background — rollback on failure). The Gallery tab should show saved turns in a timeline view using FlashList for performant scrolling. Implement pull-to-refresh with Reanimated-powered custom refresh indicator. All loading states must use skeleton placeholders with a Reanimated shimmer effect, never a spinner.
### Session C5: Lens — Goals and Actions API
Prompt to Claude Code:
> Implement the Lens module backend. POST /lens/goals creates a goal with title and optional description. GET /lens/goals returns active goals. PATCH /lens/goals/:id updates goal status. POST /lens/goals/:id/actions creates an action item. POST /lens/actions/:id/complete marks it done with timestamp. GET /lens/affirmation/today generates or returns cached daily affirmation — it calls the AI gateway with the user's active goals as context and generates a personalized affirmation. Cache the affirmation in Redis with a TTL that expires at midnight in the user's timezone. If the AI budget is constrained, fall back to a template-based affirmation system using pre-written affirmations matched to goal themes. Test: goal CRUD, action completion, affirmation generation, affirmation caching, template fallback.
### Session C6: Lens — Mobile UI
Prompt to Claude Code:
> Build the Lens screen as a calm, empowering daily direction hub. Top section shows the daily affirmation in a prominent card with gentle serif typography and a Reanimated FadeIn.delay(200).duration(600) entering animation — this card should feel like the first thing you notice. Use TanStack Query's useDailyAffirmation hook (from src/api/queries/use-lens.ts) with staleTime set to Infinity (the affirmation is cached for the full day). Below the affirmation, show active goals as expandable cards using Reanimated Layout animations (LayoutAnimation.springify()) for smooth expand/collapse. Each goal card shows its title and an animated circular progress indicator — use Reanimated useAnimatedProps to animate the SVG circle stroke-dashoffset as actions are completed. Tapping a goal expands it with a spring animation to show action items as a checklist. Completing an action triggers: Haptics.impactAsync(ImpactFeedbackStyle.Light), a Reanimated withSpring scale bounce on the checkbox (1 → 1.3 → 1), and an optimistic update via TanStack Query's useCompleteAction mutation (checkbox fills immediately, server syncs in background, rollback on failure). When all actions in a goal are complete, trigger Haptics.notificationAsync(NotificationFeedbackType.Success) and play a subtle confetti-like Reanimated animation. Add a floating action button with a Reanimated withSpring scale entrance animation. The create goal flow opens as a bottom sheet (animated with Reanimated SlideInDown.springify()) with title and optional description fields. All data flows through TanStack Query hooks: useGoals, useCreateGoal, useCompleteAction, useDailyAffirmation. Goals and actions persist offline via TanStack Query's cache + MMKV persistence — creating a goal while offline queues the mutation. Keep the UI minimal and focused — this is the "direction" pillar, it should feel empowering and clear, not like a corporate task manager.
### Session C7: Gallery and History Views
Prompt to Claude Code:
> Build the Gallery tab as a premium history experience using FlashList for buttery-smooth scrolling. Use @shopify/flash-list instead of FlatList — it recycles views for 60fps scrolling even with hundreds of items. The gallery shows a unified timeline with three content types: saved turns (with perspective previews), mirror session summaries (with fragment count and date), and lens achievements (completed actions and goals). Each type should have a distinct but cohesive card design using Reanimated entering animations — cards use FadeInUp.delay(index * 80) for staggered appearance as they scroll into view. Add filtering by content type and date range with an animated filter bar (Reanimated layout animations for smooth filter chip transitions). Add search across saved turn content and mirror reflections — the search bar slides in with Reanimated SlideInDown and uses debounced TanStack Query with the search term as a query key. For free users, gallery shows last 30 days; for prism users, full history. Implement infinite scroll with cursor-based pagination using TanStack Query's useInfiniteQuery — configure getNextPageParam for cursor-based pagination and flatMap pages for FlashList data. Add pull-to-refresh with a custom Reanimated-powered refresh indicator (not the default system spinner). Add a detail view that opens when tapping any gallery item with a shared element transition (Reanimated's SharedTransition) — the card expands into the full detail view. Turns show full perspectives, mirror sessions show the reflection, lens items show the goal with all actions. Add Haptics.selectionAsync() when tapping gallery items. All data through TanStack Query hooks: useGalleryItems (infinite query), useGallerySearch, useTurnDetail, useMirrorDetail. Stale time 2 minutes for gallery list, 5 minutes for detail views. Gallery works offline with cached data from TanStack Query's persisted cache.
### Session C8: Onboarding Flow
Prompt to Claude Code:
> Build the onboarding experience in the (auth) group as a premium first impression. After registration, guide the user through 3-4 screens using a horizontal pager with Reanimated-powered page transitions — each page slides in with a spring physics animation using withSpring({ damping: 20, stiffness: 90 }) for a soft, organic feel (not the stiff default transitions). Screen 1 explains the kaleidoscope metaphor — "Your thoughts are like light. Sometimes they get stuck in one pattern. Kalei helps you turn them and see new colors." Animate the kaleidoscope illustration with a gentle Reanimated rotation (withRepeat + withTiming over 8 seconds) so it feels alive. Screen 2 asks the user to set their preferred reframe style (compassionate, rational, growth) with brief descriptions of each — use Reanimated scale animations on the selection cards (withSpring scale 1 → 1.05 on selection) and Haptics.selectionAsync() on tap. Screen 3 invites the user to try their first Turn — embed a mini version of the Turn input with a pulsing "Turn" button (Reanimated withRepeat opacity animation). Screen 4 shows the result and introduces the daily rhythm (morning affirmation, journaling, reframing) — use staggered FadeInUp animations for each rhythm item. Add a progress indicator using Reanimated interpolation — animated dots that fill as the user progresses, with Haptics.impactAsync(ImpactFeedbackStyle.Soft) on each page transition. Store onboarding completion in the profile via a TanStack Query mutation (useCompleteOnboarding). Skip onboarding for returning users by checking the auth store's onboarding_complete flag (persisted in MMKV). The entire onboarding should feel like a guided meditation — slow, intentional, beautiful.
---
## 7. Build Track D — Launch Hardening (Weeks 9-10)
Duration: 2-4 weeks
Goal: Production-safe, store-ready, monitored.
### Session D1: Safety Hardening
> Expand crisis keyword sets with comprehensive coverage. Add regional crisis resources for all launch regions. Add a safety dashboard endpoint that returns: total safety events by type, false positive rate (manually tagged), response time percentiles. Add an admin-only endpoint to review and tag safety events for quality improvement. Verify that no code path can return reframed content when crisis is detected — write an integration test that traces every AI-facing route with crisis input and asserts the response is always the crisis resource payload.
### Session D2: Billing Integration — Apple
> Implement full App Store Server Notifications v2 handling. Verify notification signatures using Apple's public key. Parse all notification types: SUBSCRIBED, DID_RENEW, EXPIRED, DID_FAIL_TO_RENEW, REFUND, REVOKE, GRACE_PERIOD_EXPIRED. Update subscription status and entitlement snapshots accordingly. Implement a reconciliation job that periodically verifies subscription status directly with Apple's API to catch any missed webhooks. Test with Apple's sandbox environment.
### Session D3: Billing Integration — Google
> Implement Google Play Real-Time Developer Notifications. Verify Pub/Sub message authenticity. Handle all notification types: SUBSCRIPTION_RECOVERED, SUBSCRIPTION_RENEWED, SUBSCRIPTION_CANCELED, SUBSCRIPTION_PURCHASED, SUBSCRIPTION_ON_HOLD, SUBSCRIPTION_IN_GRACE_PERIOD, SUBSCRIPTION_EXPIRED, SUBSCRIPTION_REVOKED. Update subscription and entitlement records. Add reconciliation job for Google Play API verification.
### Session D4: Security Audit Pass
> Review and harden: verify all endpoints require authentication (except /health, /auth/register, /auth/login, /auth/refresh, billing webhooks). Verify password hashing uses Argon2id with secure parameters. Verify JWT tokens use appropriate algorithms and key sizes. Verify refresh token rotation and compromise detection work. Add secure HTTP headers (HSTS, CSP, X-Frame-Options). Verify no PII in logs. Verify column-level encryption on mirror message content and turn input text. Run a dependency audit (pnpm audit). Add rate limiting on auth endpoints (5 attempts per minute per IP for login). Add PostgreSQL Row-Level Security (RLS) policies as defense-in-depth: enable RLS on all user-data tables, create policies that restrict SELECT/UPDATE/DELETE to rows where user_id matches the authenticated user's ID from the JWT claim. This ensures that even if application-level ownership checks are bypassed, the database itself prevents cross-user data access. Create a migration that enables RLS and adds the policies. Test: verify a query with the wrong user_id returns zero rows when RLS is active.
### Session D5: Performance and Load Testing
> Create a load test script using k6 or autocannon that simulates: 50 concurrent users performing the Turn flow (the most AI-intensive path), 20 concurrent Mirror sessions with message submission, 100 concurrent health checks. Measure: p50/p95/p99 latency for each endpoint, error rate under load, AI gateway response time distribution, database query time distribution. Identify and fix any bottlenecks. Target: p95 under 3.5s for AI-backed endpoints, p95 under 200ms for non-AI endpoints. Additionally, run the full Maestro E2E suite (apps/mobile/e2e/) on both iOS simulator and Android emulator to verify all critical flows pass under realistic conditions. Measure: screen transition times (target under 300ms), animation frame drops (target zero dropped frames in Reanimated animations), TanStack Query cache hit rates, and offline-to-online mutation resume success rate.
### Session D6: Deployment Pipeline
> Create the production deployment setup. Docker Compose for the VPS (Fastify API, Postgres 16, Redis 7, Caddy reverse proxy with auto-HTTPS via Let's Encrypt — all free). Create deploy.sh that: builds the API, runs migrations, restarts the service with zero-downtime (using Fastify's graceful shutdown). Create backup-db.sh for automated PostgreSQL backups. Set up the mobile build pipeline using local builds as the primary path: configure eas.json with development, preview, and production profiles, then build locally using `eas build --local --platform ios --profile production` and `eas build --local --platform android --profile production`. This avoids EAS Cloud Build costs entirely — builds run on your machine using Xcode and Android Studio (both free). For development builds during day-to-day work, use `npx expo run:ios` and `npx expo run:android` which compile directly against local toolchains. EAS Cloud Build (30 free builds/month) is available as a convenience but not required. Create the app store metadata: screenshots, description, privacy policy URL, support URL.
### Session D7: Beta Testing Pipeline
> Configure TestFlight for iOS internal testing and Google Play internal testing track. Set up EAS Update for over-the-air updates during beta. Create a beta feedback mechanism — either in-app feedback button or a simple form. Add Maestro E2E tests to the release gate: configure the CI pipeline so that all Maestro flows (onboarding, turn-flow, mirror-session, purchase-flow) must pass on both iOS and Android before a TestFlight or Play Store upload proceeds. Run through the complete user journey manually: register, onboard, complete a Mirror session, do a Turn, set a Lens goal, complete an action, view Gallery. Verify that offline-first behavior works: enable airplane mode, create a turn, re-enable network, verify the turn syncs. Verify haptic feedback fires on all premium touch points (fragment tap, turn save, action complete, onboarding page transition). Document any issues. Fix launch-blocking bugs.
---
## 8. Build Track E — Spectrum Intelligence (Weeks 11-12)
Duration: 3-6 weeks
Goal: Weekly and monthly AI-powered insights from accumulated usage data.
### Session E1: Spectrum Schema and Data Pipeline
> Create Drizzle schema for spectrum tables: spectrum_session_analysis (session_id, emotional_vectors jsonb, fragment_distribution jsonb, created_at), spectrum_turn_analysis (turn_id, pre_emotional_state jsonb, post_emotional_state jsonb, impact_score float, created_at), spectrum_weekly (user_id, week_start, emotional_landscape jsonb, top_fragments jsonb, turn_impact_summary jsonb, rhythm_patterns jsonb, narrative text, created_at), spectrum_monthly (user_id, month_start, growth_trajectory jsonb, theme_evolution jsonb, narrative text, created_at). Add event hooks in Mirror and Turn services that write to spectrum analysis tables after each session/turn completes. Add exclusion flags so users can opt out specific sessions from analysis.
### Session E2: Aggregation Workers
> Build background workers for Spectrum aggregation. The weekly worker runs every Sunday night: loads all non-excluded sessions and turns from the past week, computes emotional vectors using the AI gateway (batch mode for cost savings), aggregates fragment distributions, calculates turn impact scores, generates a narrative summary of the week's patterns. The monthly worker runs on the 1st of each month: aggregates weekly data into monthly trends, identifies growth trajectory, generates a deeper narrative. Both workers must be idempotent (safe to re-run), use retry with backoff, and log failures to a dead-letter table. Use BullMQ or a simple PostgreSQL-based job queue for scheduling.
### Session E3: Spectrum API and Mobile Dashboard
> Create Spectrum API endpoints: GET /spectrum/weekly returns the latest weekly insight (or 404 if not enough data), GET /spectrum/monthly returns the latest monthly summary, POST /spectrum/reset clears all spectrum data for the user, POST /spectrum/exclusions manages session exclusions. Build the Spectrum mobile dashboard as the (spectrum) route group. The dashboard should show: an emotional landscape visualization (could be a simple radar chart or heatmap of the 6 emotional dimensions), fragment pattern distribution (which cognitive distortions appear most), turn impact summary (how much emotional shift the reframes produce), and the AI-generated narrative insight. Gate all Spectrum features behind the prism_plus entitlement.
---
## 9. AI Cost Control Strategy
This section defines the exact implementation of cost guardrails.
### 9.1 Budget Hierarchy
```
Global monthly cap ($50 Phase 1, scales with revenue)
└─ Per-feature daily budget
├─ Turn: 40% of daily budget (core product)
├─ Mirror: 35% of daily budget (core product)
├─ Lens: 10% of daily budget (can fall back to templates)
└─ Spectrum: 15% of daily budget (batch, can defer)
└─ Per-user daily token cap
├─ Free: 100k tokens/day
└─ Prism: 500k tokens/day
```
### 9.2 Degradation Order
When budget pressure hits (80% of daily budget consumed):
1. Lens affirmations switch to template-based (no AI cost).
2. Spectrum narrative generation deferred to next day.
3. Mirror fragment detection reduces max fragments per message from 10 to 5.
4. Turn reframes reduce from 3 perspectives to 2.
5. If 95% consumed: all non-critical AI paused, only safety detection continues.
### 9.3 Infrastructure Scaling Tiers
The build timeline is sequential but culminates in a single v1 launch. Infrastructure scales in tiers:
| Tier | DAU | Infrastructure | Key Milestone |
|------|-----|-----------------|-----------|
| **Launch** | 50 | Single VPS, API + DB + Redis | Initial release |
| **Traction** | 200 | Split DB, keep API monolith | First paid subscribers |
| **Growth** | 1K | Separate workers, scale API replicas | Growing active base |
| **Scale** | 10K+ | Full microservice extraction | Mature platform |
### 9.3 Provider Routing
The AI gateway routes all features through OpenRouter with provider pinning:
| Feature | Primary Provider | Fallback (provider outage) | Cost Pressure Fallback |
|---|---|---|---|
| Turn reframe | DeepSeek V3.2 via DeepInfra | DeepSeek V3.2 via Fireworks | Template system |
| Mirror detect | DeepSeek V3.2 via DeepInfra | DeepSeek V3.2 via Fireworks | — (critical) |
| Mirror reframe | DeepSeek V3.2 via DeepInfra | DeepSeek V3.2 via Fireworks | Template system |
| Lens affirmation | DeepSeek V3.2 via DeepInfra | DeepSeek V3.2 via Fireworks | Template system (no AI) |
| Crisis detection | Keyword + DeepSeek V3.2 | Keyword + Claude Haiku 4.5 | Keywords only (never skip) |
| Guide coaching | DeepSeek V3.2 via DeepInfra | DeepSeek V3.2 via Fireworks | Deferred |
| Spectrum weekly | DeepSeek V3.2 via DeepInfra | Claude Haiku 4.5 | Deferred |
| Spectrum monthly | DeepSeek V3.2 via DeepInfra | Claude Haiku 4.5 | Deferred |
OpenRouter handles failover automatically via the `order` array in API requests. Provider pinning ensures no data flows through Chinese servers (DeepInfra/Fireworks host on US/EU infrastructure).
### 9.4 Prompt Caching Strategy
System prompts for each feature are designed to be identical across users and requests. With DeepInfra's prompt caching:
- System prompt tokens (600-800 per feature) are cached after first call.
- Cache hits receive ~20% discount on input pricing ($0.216/M vs $0.26/M).
- While less dramatic than Anthropic's 90% cache discount, the base pricing is already so low (~$0.26/M input, $0.38/M output) that effective per-user costs are minimal (~$0.034/month for free users).
- Implementation: OpenRouter's OpenAI-compatible API handles caching transparently at the provider level.
---
## 10. Testing Strategy
### 10.1 Test Pyramid
| Layer | Tool | What | Coverage Target |
|---|---|---|---|
| Unit | Vitest | Pure functions, schemas, validators, prompt builders | High (80%+) |
| Integration | Vitest + Supertest | API routes with real DB (test container) | All endpoints |
| Safety | Vitest | Crisis detection accuracy | 100% of known patterns |
| AI Output | Vitest | Schema validation of AI responses | All prompt templates |
| E2E (Mobile) | Maestro | Critical mobile user flows | All happy paths + key error paths |
| Load | k6 | Concurrency and latency under load | pre-launch only |
### 10.2 Test Database Strategy
Use a separate PostgreSQL database for tests. Before each test suite: run migrations on the test DB. After each test suite: truncate all tables. Never run tests against the development database.
### 10.3 AI Mock Strategy
For unit and integration tests, mock the AI gateway at the provider level. Create a `mock.provider.ts` that returns deterministic, schema-valid responses. This means tests never hit real AI APIs, are fast, free, and deterministic. This is the primary reason AI costs are zero during development — you develop and test against the mock, not real providers. Add a small set of "golden file" tests that run against the real AI provider (gated behind an environment variable `REAL_AI_TESTS=true`) to catch prompt regression. These golden file tests should only be run occasionally during prompt engineering, using OpenRouter credits (DeepSeek V3.2 via DeepInfra is so cheap at $0.26/$0.38 per MTok that golden file test costs are negligible).
### 10.4 Maestro E2E Testing Strategy
Maestro is the industry standard for mobile E2E testing in 2026. It uses YAML-based flow definitions that are faster to write and more readable than code-based alternatives.
**Flow files live in `apps/mobile/e2e/`:**
```yaml
# e2e/onboarding.yaml — Verify new user onboarding flow
appId: com.kalei.app
---
- launchApp
- assertVisible: "Your thoughts are like light"
- swipeLeft
- assertVisible: "How would you like your reframes?"
- tapOn: "Compassionate"
- swipeLeft
- assertVisible: "Try your first Turn"
- tapOn: "What thought is weighing on you?"
- inputText: "I feel overwhelmed with everything"
- tapOn: "Turn"
- assertVisible: "perspective"
- swipeLeft
- assertVisible: "Your daily rhythm"
```
```yaml
# e2e/turn-flow.yaml — Verify complete Turn lifecycle
appId: com.kalei.app
---
- launchApp
- tapOn: "Turn"
- tapOn: "What thought is weighing on you?"
- inputText: "Nothing ever works out for me"
- tapOn: "Turn"
- assertVisible: "Compassionate"
- assertVisible: "Rational"
- assertVisible: "Growth"
- assertVisible: "If I notice"
- tapOn:
id: "save-turn-button"
- assertVisible: "Saved"
```
```yaml
# e2e/mirror-session.yaml — Verify Mirror writing and fragment detection
appId: com.kalei.app
---
- launchApp
- tapOn: "Mirror"
- tapOn: "Begin writing"
- inputText: "Everything is terrible and nothing will get better"
- wait: 3000
- assertVisible: "Catastrophizing"
- tapOn: "Catastrophizing"
- assertVisible: "See another perspective"
- tapOn: "See another perspective"
- assertVisible: "perspective"
```
```yaml
# e2e/purchase-flow.yaml — Verify paywall and upgrade path
appId: com.kalei.app
---
- launchApp
- tapOn: "Turn"
# Exhaust free tier (3 turns)
- repeat:
times: 4
commands:
- tapOn: "What thought is weighing on you?"
- inputText: "Test thought"
- tapOn: "Turn"
- wait: 2000
- assertVisible: "Upgrade to Prism"
```
**Running Maestro tests:**
```bash
# Run a single flow
maestro test apps/mobile/e2e/onboarding.yaml
# Run all flows
maestro test apps/mobile/e2e/
# Run with screenshot capture on failure
maestro test --format junit --output e2e-results/ apps/mobile/e2e/
# Run in CI (headless)
maestro test --no-ansi apps/mobile/e2e/
```
**Maestro in CI (Session D7):** Add Maestro to the CI pipeline by running the Maestro CLI locally on a CI runner with an emulator (Maestro CLI is free and open source — do not use Maestro Cloud which is a paid service). For self-hosted CI via Gitea Actions, install Maestro and an Android emulator on the runner. Gate releases on E2E pass — no TestFlight or Play Store upload if Maestro flows fail. Target: all 4 core flows pass on both iOS and Android before every release.
---
## 11. Deployment and Operations
### 11.1 Launch Deployment (Single VPS)
```
Netcup VPS 1000 G12 (€8.45/month)
├── Caddy (reverse proxy, auto-HTTPS)
├── Node.js API (Fastify, PM2 process manager)
├── PostgreSQL 16 (direct install)
├── Redis 7 (direct install)
└── Background workers (same process or separate PM2 app)
```
### 11.2 Deployment Checklist
Before every production deploy:
1. All tests pass locally.
2. Migration is tested on a staging copy of the database.
3. Rollback migration exists and is tested.
4. Health endpoint returns ok after deploy.
5. Error rate does not spike in first 15 minutes.
6. AI cost telemetry is within expected range.
### 11.3 Backup Strategy
- PostgreSQL: daily pg_dump to encrypted offsite storage.
- Redis: RDB snapshots daily (Redis data is ephemeral and rebuildable, but snapshots prevent cold-start recalculation of usage counters).
- Verified restore drill: run monthly.
---
## 12. Session Execution Checklist
Use this checklist for every Claude Code session:
```
[ ] Read the session description and understand the deliverables
[ ] Verify prerequisites from previous sessions are met
[ ] Execute the session with Claude Code
[ ] Run the validation steps — all must pass
[ ] Run the full quality check: pnpm run check
[ ] Review generated code for architecture compliance:
[ ] AI calls go through gateway only
[ ] Safety precheck on all AI-facing routes
[ ] Entitlement checks where required
[ ] Structured logging with request IDs
[ ] Zod validation on all inputs
[ ] TypeScript strict mode — no any
[ ] Commit with descriptive message
[ ] Update this checklist with any issues or deviations
```
---
## 13. Timeline Summary
| Track | Sessions | Duration | Weeks | Outcome |
|---|---|---|---|---|
| A: Foundation | A1-A6 | 3-5 days | 1-2 | Repo, infra, schema, skeletons |
| B: Platform Core | B1-B9 | 2-3 weeks | 1-3 | Auth, billing, AI gateway, safety, push, observability |
| C: Core Experience | C1-C8 | 3-5 weeks | 4-8 | Mirror, Turn, Lens, Gallery, Onboarding |
| D: Launch Hardening | D1-D7 | 2-4 weeks | 9-10 | Safety, billing, security, performance, deployment |
| E: Spectrum | E1-E3 | 3-6 weeks | 11-12 | Analytics pipeline, insights, dashboard |
Total to v1 launch (all features, end of Track E): 12 weeks.
Note: All features ship in a single v1 release. The build timeline is continuous (weeks 1-12) with sequential milestones, not separate "phases."
---
## 14. Critical Path Dependencies
```
A1 (repo) → A2 (docker) → A3 (API) → A5 (schema)
A4 (mobile) ←─── A3 (API)
A6 (shared schemas) → B1 (auth) → B2 (tokens) → B3 (entitlements)
B4 (AI gateway) → B5 (prompts) → B6 (safety) ──────→ C1 (mirror API)
B7 (rate limits) → B8 (push) → B9 (observability) ──→ C3 (turn API)
C1 → C2 (mirror UI) ─────────────────────────────→ C7 (gallery)
C3 → C4 (turn UI) ─────────────────────────────→ C7 (gallery)
C5 → C6 (lens UI) ─────────────────────────────→ C7 (gallery)
C8 (onboarding) → D1-D7 (hardening) → E1-E3 (spectrum)
```
---
## 15. What This Framework Deliberately Excludes
These are not in scope for the build framework and should be handled separately:
- Pixel-level UI specs (refer to kalei-complete-design.md and kalei-brand-guidelines.md)
- Marketing copy and App Store optimization (refer to app-blueprint.md section on ASO)
- Legal review of privacy policy and terms of service
- Detailed threat modeling (should be commissioned separately before pre-launch)
- Community building and growth hacking strategy
- Investor materials or pitch decks
---
*This framework is the operational bridge between Kalei's architecture documents and actual code. Follow it session by session. Deviate only when reality demands it, and document every deviation.*