iMassage Rebuild · Engineering Report

Foundation, security boundary, and deferred-security closure complete.

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.

Report date: 2026-05-06 Phase: 0 + 1 shipped · 2 starting Active branch: phase0-phase1-security-closure Open PR: #1

At-a-glance snapshot

12
Migrations applied
30
Test cases passing
18 / 18
D-list items closed
~5,051
Lines of TypeScript
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.

Stack & architecture

ConcernChoiceWhy
RuntimeBun 1.3+Fast, native TS, single tool for install / build / test / run.
HTTP frameworkHono15 kB core, runs natively on Bun, typed context, no client bundle.
TemplatesEta2.5 kB, compiled + cached, server-side rendered HTML.
DatabaseSupabase PostgresAuth + Storage + Vault + RLS in one place.
AuthSupabase AuthEmail OTP + password + social (Twitter, Facebook). Spring-OAuth2 contract preserved via shim.
StorageSupabase StoragePrivate buckets for therapist + store imagery.
SecretsSupabase Vaultpgsodium-encrypted, KMS-backed.
EmailResendTransactional sends; wire-up activates in Phase 2.
Testsbun test + PlaywrightUnit + integration + E2E + visual regression.
Deploy targetLinodeHard 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.

What has been built

Phase 0 — Foundation & auth boundary SHIPPED

Bottom layer functional and verified before any integrations are wired. Closes the live cross-tenant exploit window the original AngularJS app left open.

Phase 1 — Deferred security closure SHIPPED

The 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.

Foundation work (pre-Phase 0) SHIPPED

Security closure — D-list (D1 – D18)

The 3-QC security audit during Phase 1 produced 18 deferred items, tracked in imassage-rebuild/SECURITY.md. Below is the closure register.

IDItemStatus
D1Rate limit on /oauth/token password grantRESOLVED
D2Production env hard-fail (missing client secrets)RESOLVED
D3pre_auth.* HMAC token + explicit prefix-rejectionRESOLVED
D4Audit-log on auth-fail and PII-readRESOLVED
D5Regression tests — Phase 0 + non-linked role coverageRESOLVED
D6JWT TTL — live decode shipped; dashboard tighten remainingMITIGATED-MANUAL
D7Tenant authorization — cross-tenant 403 verified for 3 rolesRESOLVED
D8Logout 503 audit-loggedRESOLVED
D9Account-enumeration oracle closed for credential pathsRESOLVED
D10Audit-write capacity DoS — global IP rate-limit middlewareRESOLVED
D11500-path Supabase error.message leaksRESOLVED
D12Audit-coverage expansion — 5 new pii.read.* hooksRESOLVED
D13safeRemoteIp middleware refactorRESOLVED
D14HMAC contract enforcement — CI grep guardRESOLVED
D15_phase1TestHelpers dead export deletedRESOLVED
D16companyList integer-alias deterministic orderingRESOLVED
D17resolveAccess customer-branch explicit forbiddenRESOLVED
D18Logout-on-expired-token forensic distinction (4 outcomes)RESOLVED
D6 — MITIGATED-MANUAL. Code-side revoke-on-refresh is intentionally not implemented (Supabase's shared-session-id model would kill the new session along with the old one). The live-decode probe in 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.

Test coverage

SuiteCasesCoverage
tests/flows/legacy-shim-phase0.spec.ts12 / 12Auth-boundary on 5 protected endpoints — unauth, superadmin, customer ownership, path-param escape
tests/flows/legacy-shim-phase1.spec.ts18 / 18Rate limits, pre-auth tokens, audit logs, prod hard-fail, logout audit, cross-tenant 403, audit-coverage expansion
scripts/check-pre-auth-contract.shPASSD14 CI grep guard — verifies pre_auth prefix-rejection contract
scripts/check-jwt-ttl.tsPASSD6 ledger + live JWT decode probe (TTL = 3600s confirmed)
scripts/seed-tenant-b.tsidempotent2nd-company seed for D5/D7 cross-tenant verification — re-run produces no errors
tests/lib/availability.test.tsPASSPure-domain slot resolution algorithm (Phase 2A foundation)
tests/lib/runtime-config.test.tsPASSRuntime-config loader path resolution + cache TTL
tests/lib/csrf.test.tsPASSDouble-submit CSRF token validator
tests/lib/migrations.test.tsPASSMigration-runner ordering + idempotency
tests/visual/auth-pages.spec.tsbaselineVisual regression on /login, /signup, /reset
tests/flows/booking.spec.tsWIPPhase 2A — customer happy path + cancel (begins this session)

Database migrations

#FilePurpose
00010001_legacy_user_map.sqlBridge table for legacy user IDs during cutover
00020002_core_schema.sqlCore domain: profiles, stores, therapists, services, appointments, junction tables, role triggers
00030003_rls_policies.sqlRow-Level Security policies per role
00040004_storage_buckets.sqlSupabase Storage buckets for therapist + store imagery
00050005_app_migrations_tracking.sqlMigration ledger table
00060006_app_settings_seed.sqlSeed for app_settings (locale, timezone defaults)
00070007_schema_discrepancies.sqlReconciliation against legacy DDL gaps
00080008_supabase_role_grants.sqlservice_role / authenticated / anon GRANTs
00090009_tighten_anon_grants.sqlLock down anon to read-only on public catalog tables
00100010_reference_data.sqlReference-data import: prefectures, payment modes, company types, languages, statuses
00110011_companies_bank_payments.sqlcompanies, bank_accounts, company_payments — financial entities
00120012_ref_city.sqlCity 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.

QC methodology — n+1 ratchet

Every non-trivial plan and implementation passes the Iterative QC Ratchet (codified in CLAUDE.md). The rule:

Start with 2 QC agents. If any genuine finding appears, fix it and run the next round with one additional, stricter QC agent. Continue until a full round returns zero genuine findings. Then ship.

Approved QC agent types

AgentFocus
senior-evaluatorGoal alignment, scope control, what's missing that others overlook
senior-architect-reviewerArchitecture, hidden coupling, scalability, foundational soundness
security-auditorAuth, RLS, data exposure, secrets, injection, abuse paths
break-it-expertEdge cases, invalid states, concurrency, hostile inputs
vibe-code-guardianAI-coding failure modes: fake completion, hallucinated APIs, plan drift

Convergence trail — D5/D6/D7 batch (most recent)

RoundAgentsFindingsResult
QC1senior-evaluator + break-it-expert (2)6 genuinev2 written
QC2add security-auditor (3)6 genuinev3 written
QC3add vibe-code-guardian (4)4 genuinev4 written
QC4add architect (5)7 genuinev5 written
QC5add senior-architect-reviewer (6)4 genuinev6 written
QC6focused 3-agent re-verification0 genuineCLEAN — 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/.

Commit history — PR #1

Branch phase0-phase1-security-closure contains 6 commits closing Phase 0 + Phase 1 + all 18 D-list items. View on GitHub →

0a49af5D5/D6/D7: cross-tenant authorization tests + JWT TTL live-decode
1fab06dD10/D12/D13/D18: middleware + audit-coverage + logout distinguishability batch
a84f6a1D14/D15/D16/D17: D-list cleanup batch (4 small surgical fixes)
a0c1e42D11: close 500-path Supabase error.message leaks
5ed1269Phase 0 + Phase 1: legacy-shim auth boundary + deferred-security closure
1c50d8aFoundation: legacy-shim infra + reference-data migrations + setup wizard hardening

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).

Phase 2 — Booking flows (next)

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.

Phase 2A scope (this session forward)

Customer flows

  • Browse stores
  • Store detail · therapists · services
  • Slot picker (date → available times)
  • Confirm booking
  • Booking-success page (with email-status flag)
  • My bookings · cancel

Therapist flows

  • Day view: today + tomorrow appointments
  • Status transitions (confirmed / in-progress / completed / no-show)

Store-admin flows

  • Dashboard: today's bookings across owned stores
  • CRUD stores · therapists · services
  • Availability windows (with cross-store overlap rejection)

Superadmin flows

  • Full booking table with override controls

Architectural decisions baked into the Phase 2 spec

#DecisionWhy
R1Migration renamed 00050006Avoid clash with already-shipped 0005_security_hardening.sql
R2appointments.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.
R3Migration 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
R4Cancellation explicitly sets cancellation_policy = 'none' and refund_eligible = true for Phase 2AReal policy enforcement deferred to Phase 3 payments — explicit defaults prevent ad-hoc invention by implementers
R5booking-success.eta accepts emailStatus: 'sent' \| 'failed' flag and surfaces failure to userTelling the customer "email sent" when it actually failed is a support-ticket factory
R6Store-admin handler validates cross-store availability overlap before INSERTTwo stores showing the same therapist as available at the same time is a real bug, not a "physically impossible" hand-wave
R7RLS smoke test requires BOTH a Playwright (HTTP-layer) check AND a SQL/bun test (RLS-policy-layer) checkThe two layers can have independent bugs; one test does not cover the other

Phase 2A modules in scope

PathPurpose
migrations/0013_phase2_schema.sqlTherapist availability windows + double-booking exclusion constraint + store-admin RLS
src/handlers/booking.handler.tsCustomer browse + slot resolution + create/cancel
src/handlers/store-admin.handler.tsCRUD stores, therapists, services, availability
src/handlers/therapist.handler.tsDay-view, status transitions
src/services/availability.service.tsSlot resolution algorithm
src/services/booking.service.tsCreate, cancel, status change, audit emit
src/services/email.service.tsResend wire-up (was stub in Phase 1)
src/lib/availability.tsPure domain logic — already exists, already tested
src/views/customer/*browse · store · slots · confirm · booking-success · my-bookings
src/views/therapist/day-view.etaToday + tomorrow appointment list
src/views/store-admin/*Dashboard + CRUD forms
tests/flows/booking.spec.tsCustomer happy path + cancel (Playwright)
tests/flows/store-admin.spec.tsCRUD flows
tests/flows/therapist.spec.tsDay view + transitions

Phase 2B (later)

HTMX progressive enhancement on slot picker + booking confirm. Not in 2A scope. Two-way door — only opened if a real interaction needs it.

Phase 3 — Payments (after Phase 2)

Stripe + Paidy + PayPal wire-up. Schema stubs already in place. Refund + cancellation-policy enforcement (R4 defaults replaced with real logic).

Phase 4 — Mobile push

APNS + FCM. Schema stubs already in place.

Operator action items

ItemOwnerStatus
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

Hard constraints (do not violate)

  1. Linode-only infrastructure. No AWS / GCP / Azure / Vercel / Cloudflare for new resources.
  2. iMassage-only. Do not regenerate OpenTime forks or OpenTime-jobs rebuilds.
  3. Dropbox-sync risk. Working dir is inside Dropbox. Sensitive credentials must live in .env.local outside Dropbox or a vault — never in synced paths.
  4. Foundations before integrations. The bottom layer (Supabase schema, RLS, auth) must be functional and verified before wiring upper layers.
  5. Real services over mocks. Use Supabase / Resend / Stripe sandboxes. Thin translation shims are fine. Fake fixtures that diverge from prod are not.
  6. Local dev means working. A "preview" must be a functional flow end-to-end — not just visible UI.
  7. Two QC gates per phase. QC1 (architect → senior-architect-reviewer → senior-evaluator) and QC2 (break-it-expert + security-auditor + vibe-code-guardian + live verification). No phase ships without both.
  8. n+1 QC ratchet on non-trivial work. Iterate until one full round returns zero genuine findings.