Status of the greenfield rebuild of the frozen 2019 iMassage Japanese B2C massage-booking platform — what's shipped, what's pending, and the Phase 2 plan now beginning execution.
| Area | State | Detail |
|---|---|---|
| Infrastructure foundation | SHIPPED | Bun + Hono + Supabase. /setup wizard. Runtime config out of Dropbox. Linode-only deploy target. |
| Database schema | SHIPPED | 12 migrations, RLS policies, role grants, reference-data import from legacy DDL. |
| Legacy-shim translation layer | SHIPPED | Spring-OAuth2 → Supabase Auth shim. AngularJS frontend talks to it unmodified. |
| Auth boundary (Phase 0) | SHIPPED | requireFinancialAccess + requireOperationalAccess on 5 protected endpoints. 12 CI tests. |
| Deferred security closure (Phase 1) | SHIPPED | All 18 D-list items closed (D1–D5, D7–D18 RESOLVED; D6 MITIGATED-MANUAL). |
| Phase 2 — Booking flows | STARTING | Spec at _specs/002-phase2-booking.md approved. Implementation begins this session. |
| D6 dashboard action | OPERATOR | Confirm Supabase JWT expiry ≤ 3600s in dashboard, then upgrade ledger to VERIFIED. |
| Phase 3 — Payments (Stripe / Paidy / PayPal) | PLANNED | Schema stubs in place. Wire-up scheduled after Phase 2. |
| Phase 4 — Mobile push (APNS / FCM) | PLANNED | Schema stubs in place. |
| Concern | Choice | Why |
|---|---|---|
| Runtime | Bun 1.3+ | Fast, native TS, single tool for install / build / test / run. |
| HTTP framework | Hono | 15 kB core, runs natively on Bun, typed context, no client bundle. |
| Templates | Eta | 2.5 kB, compiled + cached, server-side rendered HTML. |
| Database | Supabase Postgres | Auth + Storage + Vault + RLS in one place. |
| Auth | Supabase Auth | Email OTP + password + social (Twitter, Facebook). Spring-OAuth2 contract preserved via shim. |
| Storage | Supabase Storage | Private buckets for therapist + store imagery. |
| Secrets | Supabase Vault | pgsodium-encrypted, KMS-backed. |
Resend | Transactional sends; wire-up activates in Phase 2. | |
| Tests | bun test + Playwright | Unit + integration + E2E + visual regression. |
| Deploy target | Linode | Hard constraint — no AWS / GCP / Azure / Vercel / Cloudflare for new infra. |
No SPA framework. Server-rendered HTML with progressive enhancement is the default. The original 2019 app was AngularJS; the new app is server-first. The legacy AngularJS frontend continues to be served from _audit/legacy-sandbox/source-root/ during the rebuild, talking to the new backend through the OAuth2 → Supabase shim.
Bottom layer functional and verified before any integrations are wired. Closes the live cross-tenant exploit window the original AngularJS app left open.
src/handlers/legacy-shim.handler.ts, ~1,700 LOC)requireFinancialAccess + requireOperationalAccess + resolveAccess helpers/companies/:id/{bankdetails, payments, revenue}, /companies/:id, /companies/:id/storestests/flows/legacy-shim-phase0.spec.ts) — unauth → 401, superadmin → 200, path-param escape, ownership checksThe original 3-QC security audit produced 18 deferred items (D1 – D18). All 18 are now closed (one with operator action remaining). Methodology: iterative QC ratchet — see § QC methodology.
/oauth/token; global per-IP bucket on all /iMassage_*/* prefixesLEGACY_CLIENT_ID / SECRET / PRE_AUTH_HMAC missing or sentinelpre_auth.<nonce>.<hmac> format with explicit prefix-rejection in requireBeareraudit_logs via insert_audit_log RPC + advisory lock; writeAuditSafe wrapper at 21+ hook sitesseed-tenant-b.ts + 3 new test cases verifying customer / therapist / store_admin all 403 across tenantsscripts/check-jwt-ttl.ts signs in, decodes exp - iat, asserts ≤ 3600s + ledger match"database error" body; truncated raw error in audit metadata onlyTRUSTED_PROXY=true; safeRemoteIp falls back to socket peerscripts/preview.sh dual-port: legacy 8765 + rebuild 3737)seed-ready-state.ts: 1 company, 5 stores, 8 therapists, 15 services, 7-day availability).runtime-config.json out of Dropbox-synced folderThe 3-QC security audit during Phase 1 produced 18 deferred items, tracked in imassage-rebuild/SECURITY.md. Below is the closure register.
| ID | Item | Status |
|---|---|---|
| D1 | Rate limit on /oauth/token password grant | RESOLVED |
| D2 | Production env hard-fail (missing client secrets) | RESOLVED |
| D3 | pre_auth.* HMAC token + explicit prefix-rejection | RESOLVED |
| D4 | Audit-log on auth-fail and PII-read | RESOLVED |
| D5 | Regression tests — Phase 0 + non-linked role coverage | RESOLVED |
| D6 | JWT TTL — live decode shipped; dashboard tighten remaining | MITIGATED-MANUAL |
| D7 | Tenant authorization — cross-tenant 403 verified for 3 roles | RESOLVED |
| D8 | Logout 503 audit-logged | RESOLVED |
| D9 | Account-enumeration oracle closed for credential paths | RESOLVED |
| D10 | Audit-write capacity DoS — global IP rate-limit middleware | RESOLVED |
| D11 | 500-path Supabase error.message leaks | RESOLVED |
| D12 | Audit-coverage expansion — 5 new pii.read.* hooks | RESOLVED |
| D13 | safeRemoteIp middleware refactor | RESOLVED |
| D14 | HMAC contract enforcement — CI grep guard | RESOLVED |
| D15 | _phase1TestHelpers dead export deleted | RESOLVED |
| D16 | companyList integer-alias deterministic ordering | RESOLVED |
| D17 | resolveAccess customer-branch explicit forbidden | RESOLVED |
| D18 | Logout-on-expired-token forensic distinction (4 outcomes) | RESOLVED |
scripts/check-jwt-ttl.ts verifies the project's effective TTL on every gate run. Final closure requires the operator (Kyle) to confirm the dashboard's JWT expiry is ≤ 3600s and append a GATE_JWT_TTL: VERIFIED YYYY-MM-DD @ <seconds> line to PHASE1-SCOPE.md — at which point D6 upgrades to RESOLVED.
| Suite | Cases | Coverage |
|---|---|---|
tests/flows/legacy-shim-phase0.spec.ts | 12 / 12 | Auth-boundary on 5 protected endpoints — unauth, superadmin, customer ownership, path-param escape |
tests/flows/legacy-shim-phase1.spec.ts | 18 / 18 | Rate limits, pre-auth tokens, audit logs, prod hard-fail, logout audit, cross-tenant 403, audit-coverage expansion |
scripts/check-pre-auth-contract.sh | PASS | D14 CI grep guard — verifies pre_auth prefix-rejection contract |
scripts/check-jwt-ttl.ts | PASS | D6 ledger + live JWT decode probe (TTL = 3600s confirmed) |
scripts/seed-tenant-b.ts | idempotent | 2nd-company seed for D5/D7 cross-tenant verification — re-run produces no errors |
tests/lib/availability.test.ts | PASS | Pure-domain slot resolution algorithm (Phase 2A foundation) |
tests/lib/runtime-config.test.ts | PASS | Runtime-config loader path resolution + cache TTL |
tests/lib/csrf.test.ts | PASS | Double-submit CSRF token validator |
tests/lib/migrations.test.ts | PASS | Migration-runner ordering + idempotency |
tests/visual/auth-pages.spec.ts | baseline | Visual regression on /login, /signup, /reset |
tests/flows/booking.spec.ts | WIP | Phase 2A — customer happy path + cancel (begins this session) |
| # | File | Purpose |
|---|---|---|
| 0001 | 0001_legacy_user_map.sql | Bridge table for legacy user IDs during cutover |
| 0002 | 0002_core_schema.sql | Core domain: profiles, stores, therapists, services, appointments, junction tables, role triggers |
| 0003 | 0003_rls_policies.sql | Row-Level Security policies per role |
| 0004 | 0004_storage_buckets.sql | Supabase Storage buckets for therapist + store imagery |
| 0005 | 0005_app_migrations_tracking.sql | Migration ledger table |
| 0006 | 0006_app_settings_seed.sql | Seed for app_settings (locale, timezone defaults) |
| 0007 | 0007_schema_discrepancies.sql | Reconciliation against legacy DDL gaps |
| 0008 | 0008_supabase_role_grants.sql | service_role / authenticated / anon GRANTs |
| 0009 | 0009_tighten_anon_grants.sql | Lock down anon to read-only on public catalog tables |
| 0010 | 0010_reference_data.sql | Reference-data import: prefectures, payment modes, company types, languages, statuses |
| 0011 | 0011_companies_bank_payments.sql | companies, bank_accounts, company_payments — financial entities |
| 0012 | 0012_ref_city.sql | City reference table import |
Phase 2 will add migration 0013_phase2_schema.sql: therapist_availability + EXCLUDE USING gist double-booking guard + tightened RLS for store_admin role.
Every non-trivial plan and implementation passes the Iterative QC Ratchet (codified in CLAUDE.md). The rule:
| Agent | Focus |
|---|---|
senior-evaluator | Goal alignment, scope control, what's missing that others overlook |
senior-architect-reviewer | Architecture, hidden coupling, scalability, foundational soundness |
security-auditor | Auth, RLS, data exposure, secrets, injection, abuse paths |
break-it-expert | Edge cases, invalid states, concurrency, hostile inputs |
vibe-code-guardian | AI-coding failure modes: fake completion, hallucinated APIs, plan drift |
| Round | Agents | Findings | Result |
|---|---|---|---|
| QC1 | senior-evaluator + break-it-expert (2) | 6 genuine | v2 written |
| QC2 | add security-auditor (3) | 6 genuine | v3 written |
| QC3 | add vibe-code-guardian (4) | 4 genuine | v4 written |
| QC4 | add architect (5) | 7 genuine | v5 written |
| QC5 | add senior-architect-reviewer (6) | 4 genuine | v6 written |
| QC6 | focused 3-agent re-verification | 0 genuine | CLEAN — implement |
Across the project so far, the ratchet has run for: Phase 0 plan (10 rounds) + impl (5 rounds), Phase 1 deferred-security plan (5 rounds) + impl (6 rounds), D11 (2+1), D14–D17 (2+1), D10–D18 (2+1), D5–D7 (6+1). Total: ~40 iterative QC passes with documented convergence trails in _audit/.
Branch phase0-phase1-security-closure contains 6 commits closing Phase 0 + Phase 1 + all 18 D-list items. View on GitHub →
Plus pre-PR commits on main: setup-wizard QC pass · /setup wizard build · Phase 2A.code (paused) · Phase 2A foundation (schema + availability algorithm) · Phase 2 spec + deploy automation · Phase 1 foundation (Bun + Hono + Supabase + Playwright).
Spec at imassage-rebuild/_specs/002-phase2-booking.md. Senior-architect approved with 3 P1 fixes (R1–R3) and 4 P2 fixes (R4–R7) already applied to the spec.
| # | Decision | Why |
|---|---|---|
| R1 | Migration renamed 0005 → 0006 | Avoid clash with already-shipped 0005_security_hardening.sql |
| R2 | appointments.time_range generated column + EXCLUDE USING gist constraint scoped to non-cancelled rows; booking service catches Postgres 23P01 and returns "slot no longer available" | Application-layer availability check is racy. DB-level prevention is the only correct guard against double-booking. |
| R3 | Migration drops stores_superadmin_write only (not stores_admin_write) | Wrong DROP target silently no-ops via IF EXISTS, leaving stale policies alive — auditable footgun closed at spec stage |
| R4 | Cancellation explicitly sets cancellation_policy = 'none' and refund_eligible = true for Phase 2A | Real policy enforcement deferred to Phase 3 payments — explicit defaults prevent ad-hoc invention by implementers |
| R5 | booking-success.eta accepts emailStatus: 'sent' \| 'failed' flag and surfaces failure to user | Telling the customer "email sent" when it actually failed is a support-ticket factory |
| R6 | Store-admin handler validates cross-store availability overlap before INSERT | Two stores showing the same therapist as available at the same time is a real bug, not a "physically impossible" hand-wave |
| R7 | RLS smoke test requires BOTH a Playwright (HTTP-layer) check AND a SQL/bun test (RLS-policy-layer) check | The two layers can have independent bugs; one test does not cover the other |
| Path | Purpose |
|---|---|
migrations/0013_phase2_schema.sql | Therapist availability windows + double-booking exclusion constraint + store-admin RLS |
src/handlers/booking.handler.ts | Customer browse + slot resolution + create/cancel |
src/handlers/store-admin.handler.ts | CRUD stores, therapists, services, availability |
src/handlers/therapist.handler.ts | Day-view, status transitions |
src/services/availability.service.ts | Slot resolution algorithm |
src/services/booking.service.ts | Create, cancel, status change, audit emit |
src/services/email.service.ts | Resend wire-up (was stub in Phase 1) |
src/lib/availability.ts | Pure domain logic — already exists, already tested |
src/views/customer/* | browse · store · slots · confirm · booking-success · my-bookings |
src/views/therapist/day-view.eta | Today + tomorrow appointment list |
src/views/store-admin/* | Dashboard + CRUD forms |
tests/flows/booking.spec.ts | Customer happy path + cancel (Playwright) |
tests/flows/store-admin.spec.ts | CRUD flows |
tests/flows/therapist.spec.ts | Day view + transitions |
HTMX progressive enhancement on slot picker + booking confirm. Not in 2A scope. Two-way door — only opened if a real interaction needs it.
Stripe + Paidy + PayPal wire-up. Schema stubs already in place. Refund + cancellation-policy enforcement (R4 defaults replaced with real logic).
APNS + FCM. Schema stubs already in place.
| Item | Owner | Status |
|---|---|---|
Confirm Supabase JWT expiry ≤ 3600s in dashboard (Auth → Settings → JWT expiry); append GATE_JWT_TTL: VERIFIED YYYY-MM-DD @ <seconds> to PHASE1-SCOPE.md |
Kyle | PENDING |
| Enable refresh-token rotation in Supabase dashboard (Auth → Settings) | Kyle | PENDING |
| Disable email enumeration in Supabase dashboard (Auth → Settings) | Kyle | PENDING |
| Provision Linode deployment target (production runtime) | Kyle | SCHEDULED · post-Phase 2 |
| Stripe / Paidy / PayPal sandbox accounts | Kyle | SCHEDULED · Phase 3 |
| Resend production API key + domain verification | Kyle | NEEDED · Phase 2 wire-up |
| APNS team ID + key, FCM service account | Kyle | SCHEDULED · Phase 4 |
.env.local outside Dropbox or a vault — never in synced paths.