June 10 Demo — Single-Session Stress Test (SHIPPED — historical record)
📕 HISTORICAL — do not plan against this document.
What it was: the plan for a narrow, single-session demo that handed a logged-in legacy (Strapi) patient into the new platform and let them play one curated session in the real Portal player.
Status: shipped 2026-06-10. Everything in its "In" column was built; its deferred items moved to june-launch.md. It stood up the production substrate that has served real patients continuously since 2026-06-05.
Superseded by: platform-completion.md — the active plan since 2026-08-02. It retired both this doc's successor (june-launch.md) and the September-launch framing behind it. There is no target date; the ~20k legacy migration is the last step, not a driver.
Why it is kept: it is the record of what actually shipped on 2026-06-10 and of the durable infrastructure that came out of it (own-session custom-domain auth, the legacy→handoff bridge, the live production substrate). Read it as a retrospective. Any forward-looking statement in it — "feeds into the September launch", deferred-item routing, remaining scope — is stale by construction.
✅ SHIPPED 2026-06-10. A narrow, production-grade demo that handed a logged-in legacy (Strapi) patient into the new platform, auto-provisioned an account, and let them play one curated session in the real Portal session player — capturing adherence + media-QoS telemetry at production quality. It ran live on the production substrate (RDS Multi-AZ + Redis + ECS Fargate, schema v38), which has served real patients continuously since 2026-06-05. This doc is now a completed-initiative retrospective: the substrate it stood up, plus its same-org / same-Clerk graduation path, feed forward into the
September 2026 full launch — see june-launch.md, the active planplatform-completion.md, the active plan (corrected 2026-08-02 — june-launch.md was itself superseded).
Premise
- RestartiX-the-clinic is the only tenant. The demo runs in the real
restartixorganization (our own clinic), reached via the vanity custom domainplatforma.restartix.ro(an alias forrestartix.portal.restartix.pro). Not a throwaway demo org. - This was the first production deploy. The substrate stood up here stayed live continuously and has served real patients since 2026-06-05 — it is the production environment, not a temporary rig. No foundation shortcuts; this becomes data we don't re-migrate. It carries straight into the September 2026 full launch.
- Two-clicks-to-play. Patient experience end-to-end is two clicks: the legacy "Try it" link, then one "I agree" on a welcome/consent screen. Everything else (Clerk account, profile, enrolment) is invisible.
- Stress-test learnings are the durable payoff — does
db.t4g.mediumhold under the provisioning burst? buffering/QoS at peak concurrency? right Fargate counts? Capture them regardless of what the demo data becomes.
The flow
Legacy (Strapi = restartix-admin-api): patient clicks "Try the new session player"
→ GET admin-api/demo-handoff (Strapi session knows the patient)
→ mints HS256 token {email, name, strapi_user_id, iat, exp:+5m, jti}
→ 302 → https://platforma.restartix.ro/enter?t=<token>
/enter (Portal server route):
verify token (HS256, shared secret) + jti replay-guard
→ Clerk get-or-create user by email → sign-in token (ticket strategy)
→ sign in (no email code, no password, Clerk sends nothing) → redirect /demo
/demo (Portal, Clerk-authed):
no patient_profile yet → ONE welcome + consent screen (name pre-filled)
→ "I agree": records the 4 mandatory consents + creates patient_profile
+ self-enrols the demo program (existing onboarding + protocol endpoints)
→ renders the free session + exercise list
Play → session player (+ live pose skeleton in the framing step)
→ Done → /demo with stats (VAS/RPE/completion + watch %)
TV (optional): patient pairs apps/tv via 6-digit code → session_run + display token → TV playsScope at a glance
Everything in the In column shipped. The Deferred items moved to june-launch.md (the active September 2026 plan) as launch-gate work.
| In (shipped June 10) | Out |
|---|---|
| Production substrate (RDS Multi-AZ), stays live | Full beta self-signup (we use the legacy hand-off) |
Legacy→new auth bridge (Strapi mint + /enter + Clerk) | F1/F2/F4/F5/F6/F12 |
One-click consent + (demo) route group + return stats | F3 dynamic forms |
| One curated session (seeded + baked) | Pose ingest pipeline (only the client preview ships) |
| Live MediaPipe pose skeleton preview (client-only, nothing uploaded) | ClickHouse / Tier-3 telemetry |
| TV companion + player | |
| Console platform-stats surface (production-grade) | |
platforma.restartix.ro custom hostname |
Locked decisions
- Real
restartixorg via custom-domain alias —platforma.restartix.roresolves to therestartixorg through the existing custom-hostname path (cloudflaresaasGo pkg + portalresolveOrganizationByDomain).restartix.rois already a Cloudflare zone. - Consent collapses, it does not skip. The Core API hard-enforces 4 mandatory purposes (
platform_terms,platform_privacy_notice,org_terms,org_privacy_notice);RequireCurrentConsents(412) is always-on with no bypass. So the demo shows one consent screen / one click, recording v1 placeholder terms. Auto-accepting server-side without showing the patient would be legally void consent (GDPR Art. 7). - Re-consent at the real launch = version bump. Publish real
platform_terms+restartixorg_termsas a NEW version; the 412 gate then forces every demo alumnus to re-consent. Same identity, same org — clean reset. - Clerk is mandatory; provision via sign-in tokens. Patient routes accept only a Clerk JWT and RLS needs org scope + a resolved
patient_profile. No parallel non-Clerk "demo auth" (foundation rewrite for a throwaway). Sign-in tokens (ticket strategy) log the patient in with no email code/password. - Strapi HS256 hand-off. Dedicated shared secret (NOT Strapi's main auth JWT), short-lived token, minted by a small endpoint in
restartix-admin-api(sibling repo — we build both sides). Stampstrapi_user_idon our side to reconcile at the eventual ~20k migration. - One curated session. Self-enrol is an ungated free enrolment (
POST /v1/me/protocols); no subscription needed. Renders must be baked +readyon Bunny before the demo. - Pose = live MediaPipe skeleton overlay added to the EXISTING "position yourself" framing step (
apps/portal/components/session/pose-opt-in-client.tsx). Client-side only, nothing uploaded/stored → no biometric-consent surface, no telemetry ingest. New direct dep → SOUP entry. - TV companion + player in scope — works as-is; a demo Clerk patient claims the 6-digit pairing.
- Console platform-stats is a real production surface, not a stress-test page. Clinic-level stats already exist in the Clinic app (F9.4
/patients/[id]/stats) — do not add clinic stats. - Production substrate, RDS Postgres 17 Multi-AZ (
db.t4g.medium), built properly via the scaffold-onlydatabase-rdsmodule — not the Aurora shortcut. The env stays live demo → real launch; one setup, ECS autoscaling absorbs the load delta; ~$500/mo low-usage floor, higher only during the stress weeks.
Reuse (already shipped — do not rebuild)
Session player (Portal single-<video>), media/QoS telemetry service + ingest + aggregation, async render pipeline, TV pairing + player (apps/tv), exercises/sessions/programs/enrolment substrate, per-session instruments (VAS/RPE/completion on session_runs), the camera "framing" step, proxy host→org resolution, the two onboarding endpoints (POST /v1/me/patient-profile, POST /v1/portal/onboard).
Stream A — Production Infrastructure (live since 2026-06-05) ✅
infra/envs/productionis provisioned and live — RDS Multi-AZ + Redis + ECS Fargate, serving real patients since 2026-06-05, prod DB at schema v38. Single AWS account538636660353,eu-central-1, separate tfstate. (Most staging IaC was reusable; telemetry is a first-class ECS service; network/redis/crons/ECS modules copied with new tfvars.) The production-hardening polish items below that did not ship with the demo (Sentry, AWS Budgets, k6 gate, cross-region backup drill, SES suppression) moved to june-launch.md as September 2026 launch-gate work.
Deployment posture (locked 2026-06-03):
Branch / promotion — prod deploys via
workflow_dispatchfrommaster, rebuilding images per-env (Next.js bakes env-specific vars at build time; no staging-SHA promotion).Approval gate — GitHub
productionEnvironment + required reviewers; the OIDC role trusts the environment, not a branch. No auto-deploy on push.Migrations stay editable until the REAL launch. The demo prod env is pre-launch production infra — still wipeable. The team keeps editing migrations on staging; the demo prod DB is re-migrated only if an in-place edit must land — done manually via the SSM tunnel (guarded), NOT an unguarded prod-reset script. Forward-only freeze binds at the real-launch gate when the irreplaceable ~20k-patient data arrives. (
early-dev-migrations-editablestays in force for prod through the demo window; demo data is re-creatable via the legacy hand-off + Bucket 3.)Ops custodian = Ovidiu — authorizes prod migrations, holds the Clerk prod secret + DB master access; Claude executes via CI + SSM under direction.
Account — single-account Path A + role-scoping per the locked architecture decision; revisit AWS Organizations before the 20k migration.
[x] Bootstrap: production tfstate +
restartix-deploy-productionOIDC role (trusts theproductionGitHub Environment, not a branch) +deploy-production.yml(workflow_dispatchfrommaster, rebuilds images) + theproductionEnvironment with required reviewers[x] Implement the
database-rdsmodule (was scaffold-only):aws_db_subnet_group, parameter group (rds.force_ssl=1,max_connections=200,shared_preload_libraries=pg_stat_statements),aws_db_instanceMulti-AZdb.t4g.medium+ Performance Insights (7-day) + Enhanced Monitoring, SM-managed master password,storage_encryptedunder the prod CMK,deletion_protection=true,apply_immediately=false,maintenance_window=sun:02:00-03:00 UTC, 7-day PITR,final_snapshot_identifierset[x]
infra/envs/production/main.tf: KMS CMK + Redis Multi-AZ (replicas=1,cache.t4g.small) + VPC (nat_strategy="nat-gateway", both AZs) + allrestartix/production/*SM containers[x]
infra/envs/production/compute.tf: demo-critical ECS at production sizing, on-demand (no Spot), autoscaling min/max:core-api,portal,telemetry,pgbouncer,console.media-service+clinicatdesired_count=0. ALB + ECR repos + ACM cert. core-apisecret_env_varsmust includeLEGACY_HANDOFF_SECRET(fromrestartix/production/legacy-handoff-secret, same value as the legacy Railway var) — the Stream B handoff verifier reads it[x]
infra/envs/production/crons.tf(partition-roll ×2, usage-quota-reset, usage-summary-rollup, check-providers →rate(1 minute))[x] Cloudflare: register
platforma.restartix.roas a custom hostname on therestartix.rozone + DV cert; lock ALB SG to Cloudflare IP ranges[x] Secrets population (
populate-production-secrets.sh, cloned from staging,ENV=productionhardcoded,recovery_window=30on every SM secret):restartix/production/legacy-handoff-secret, Bunny production creds (restartix/production/media-service), Clerk production keys (restartix/production/clerk-bootstrap), encryption keyring, bunny-webhook secret,restartix/production/sentry[x] Stand up a separate Clerk production application (own publishable/secret pair; do NOT reuse staging keys with production patients)
[x]
push-image.shenv guard — refuse non-staging targets unless explicit--env production+ a loud confirmation (close the OIDC-bypass blast-radius)[x] Bootstrap DB roles + run migrations (direct URL on 5432) + roll partitions ahead + post-deploy smoke test
[x] CloudWatch alarms per service (CPU/mem/restart-loop) + ALB 5xx + NAT errors, 90-day log retention
Did NOT ship with the demo — moved to june-launch.md as September 2026 launch-gate items: the storage-backups module (daily encrypted pg_dump → S3 independent of RDS PITR) + the cross-region backup/restore drill; Sentry (org + project per service, SDK + source-map upload + release tracking + alerts); AWS Budgets alarm polish (50/80/100% thresholds + SNS topic/subscription); the k6 5k-shape load gate; and SES bounce/complaint suppression. The DSN/secret slots (restartix/production/sentry) are populated; the SDK wiring and the rest of the polish remain.
Stream B — Legacy self-migration (first used by the demo)
This is the reusable production self-migration path, not a demo-only bridge — at launch all ~20k patients onboard through the same flow. Accounts link by email (no legacy-id column; emails are unique). Endpoints are named generically (never "demo"). The legacy side is the user's repo — delivered as a prompt, not built here.
- [x] [USER — Strapi]
restartix-admin-api(commite25149e, not pushed): authedGET /api/legacy-handoff?session=<id>mints HS256{email, name, content_id, jti, exp:+120s, iss:"api.restartix.ro", aud:"platforma.restartix.ro"}signed withLEGACY_HANDOFF_SECRET, returns JSON{url:"<LEGACY_HANDOFF_URL>?t=…"}— the full hand-off URL comes from a StrapiLEGACY_HANDOFF_URLenv var (paired withLEGACY_HANDOFF_SECRET; Railway-configurable, NOT hardcoded;https://platforma.restartix.ro/enter— includes the/enterpath so the route name is config too; the controller appends?t=). Theaudclaim isplatforma.restartix.ro(changed 2026-06-04 to match the domain — both sides + the GoLegacyHandoffAudiencedefault + test fixtures updated);issstaysapi.restartix.ro.content_id= the new-platform content to land on (program id now / session id later), carried from the?session=query param → token claim (no DB designation column). Entry = a WordPress redirect page (restartix.ro/go-demo?session=<id>) that reads the patient JWT from localStorage, fetches the handoff endpoint w/ bearer header, andwindow.locations to the returned url (mints fresh at click → no 120s-expiry issue). Swap the demo by editing?session=in the link. Pending user manual steps: rotated Railway secret, grantconsumer.legacyHandoffto the Consumer role, the WP redirect page + link - [x] Core API
POST /v1/public/legacy/handoff(public, IP-rate-limited): verify HS256 +exp+iss=="api.restartix.ro"+aud=="platforma.restartix.ro", single-usejtivia RedisSET NX, return{email, name}. Built viainternal/core/legacyhandoff(stdlib HMAC, no new dep) +internal/core/domain/legacy. Verify failures collapse to a generic 401 (no oracle); 409 replay; 503 unconfigured- [x] content_id follow-up (landed with Stream C 2026-06-04): verifier parses the optional
content_idclaim; handoff 200 returns it when non-empty;/entercarries it to/demoas?c=+ sets the patient name on the Clerk user;/demo→claim(program_id = content_id)
- [x] content_id follow-up (landed with Stream C 2026-06-04): verifier parses the optional
- [x] Portal
/enterroute (added toisPublicRoute): calls the handoff endpoint → ClerkgetUserList/createUserby email →createSignInToken→ redirect/demowith the ticket (app/enter/page.tsx) - [x]
/demoticket sign-in:signIn.create({strategy:'ticket'})+setActive()(ticket-sign-in.tsx) → Clerk-authed (currently a stub page — full UI is Stream C) - [x] Core API
POST /v1/me/legacy/claim(authed):portalonboarding.ProvisionForClaim(one admin tx: 4 consents +patient_profile+ org onboard) + optionalprotocols.SelfEnrollForClaim(2nd idempotent tx). Mounted outsideRequireCurrentConsents/RequireOrganizationScope, self-service "me" posture. Response carriesenrolled/protocol_id?— Stream C must handleenrolled:false - [x]
/enter+/demoadded toisPublicRouteinapps/portal/proxy.ts; org context resolves via theplatforma.restartix.rocustom domain
Shipped — make check/make test/integration (6 handoff + 4 claim) + pnpm all pass; ran live in the demo. (On staging + production for the demo flow; ongoing prod promotion is the user's call.) Open launch follow-up (tracked in june-launch.md): claim respects per-clinic portal_self_signup_enabled — fine for the demo (enable self-signup on restartix), but the September ~20k launch may want a separate legacy_migration_enabled flag so clinics needn't open walk-up self-signup to accept migrating patients.
Stream C — Demo UX
Shipped. Client orchestrator on app/demo/ (P46 hybrid: server shell, SWR reads via route handlers, the one write claim is a server action). State flow: signing-in → (consent | missing-content) → session (+ stats). Ran live in the demo on the production substrate.
- [x]
/demopage (page+demo-client+demo-consent+demo-session+actions) — ticket sign-in reused (ticket-sign-in.tsx, strips onlyticket);c=content_id kept in the URL (refresh-safe; non-sensitive) - [x] Consent screen — gated on
/meis_patient_at_current_org(skips for returning patients); greeting + honest beta/camera blurb + dynamically-fetched required documents (legal_basis≠consent, each w/ aLegalDocumentDialog); one "I agree & start" →claim(name, all required codes, program_id=content_id). Records only the mandatory non-consent-basis purposes (no silent marketing opt-in) - [x] Session view — renders the single session + exercises as a session (no program chrome) from
GET /v1/me/sessions/assigned;preparing→ prepare card + refresh. Play →startDemoSessionActionsets ademo-activecookie + enters the existing kiosk run flow - [x] Return-stats —
/runs/[runId]/donereadsdemo-active→ "Back to the session" →/demo?run=<id>;/demoreads?run→GET /v1/session-runs/{id}→ stats banner (completion, exercises done, VAS, RPE) + "play again". All terminal paths funnel through/done - [x] Edge handling:
enrolled:false→ retry claim once → manual "Load my session" fallback; missing/emptycontent_id→ friendly card - [x] i18n EN + RO (translate-ro skill; informal patient register, ICU plurals)
- [x] Verified end-to-end in the live demo — the full browser flow (ticket→consent→claim→play→stats) ran against the live production substrate + Clerk prod on 2026-06-10; backend covered by integration tests, UI build-verified
Stream D — Session content + Pose preview
- [x] No seeding — the clinic admin authors the demo program in the Clinic builder; the content id rides the legacy
?session=link →content_idclaim (no DB designation column). Go-live step (user): publish the program + confirm renders arereadyon Bunny before opening the demo. - [x] Live pose skeleton overlay — BUILT + committed 2026-06-04. npm
@mediapipe/tasks-vision@0.10.35(self-hosted lite model committed + WASM generated at build, gitignored);use-pose-skeleton.ts(VIDEO mode, GPU→CPU→plain-camera fallback, EMA smoothing, RAF + teardown, Strict-Mode guard) + mirrored canvas overlay + "Te vedem ✓" chip in theframingstage; clean white spine skeleton (PoC Hermite centerline, manual 2D draw). Client-only, zero egress. - [x] MediaPipe SOUP rows (dep + first AI/ML-model row, Standard/non-clinical) + ro/en i18n. ⚠ Prod-build watch: WASM is build-time-generated → the production Docker image must run the portal build (copy script) before
COPY public/, else prod ships without WASM (silent fallback to plain camera) — verify in Stream A.
Stream E — TV companion ✅
Shipped. TV pairing + companion player ran in the demo; post-demo the companion received a production-hardening + telemetry-parity wave (on staging; prod promotion is the user's call).
- [x] Point
apps/tvat the demo (Bunny HLS / demo env config); confirm pairing →session_run→ display-token → playback works for a demo Clerk patient - [x] Cast test matrix: native TV app + Chromecast + AirPlay
Stream F — Console platform stats (production-grade)
Shipped (thin operator dashboard). Console /platform-stats (server-only, force-dynamic, no poller) ← GET /v1/admin/platform-stats?window_start=&window_end= (superadmin-gated via existing RequireSuperadmin(), no new permission; AdminPool cross-org server-side COUNT/GROUP BY). (On staging; prod promotion is the user's call.)
- [x] Live metrics: signups, sessions started/completed/in-progress/dropped, abnormal-completion (
auto_closed) + rates, status histogram, sessions by country + city. - [x] Geo capture — new
session_runs.country/city(folded into000023, not a new ALTER — matches the editable-migration convention +check-classificationparses CREATE TABLE) +idx_session_runs_started_geo. Newinternal/shared/clientgeoreadsCF-IPCountry/CF-IPCity; threaded throughPOST /v1/session-runsand the TV-claim (sessionpairings) path; portal forwards the CF headers via api-client over the internal Cloud Map hop. Graceful degrade (country nil off-CF; city nil pre-transform; run-create never fails over geo). - [x] Data-classification entry for
country/city(pii_basic, legitimate-interest, aggregate-only). - [x] media QoS (watch %/buffering) aggregated cross-org — the streaming-stress gold signal. Telemetry gained
GET /internal/v1/platform/media-summary(media.SummarisePlatform— the per-patient aggregate SQL minus thepatient_idANDorg_idfilters, window only; the one deliberately cross-org telemetry read, superadmin-only via the shared secret). Core API:telemetry.Client.GetPlatformMediaSummary(org-less by design) →PlatformStats.fetchPlatformMediaQoSpopulatesmedia_qos(watch % =avg_completion_pct, summed buffering, p95 load/TTFB);available=falseonly when telemetry is unwired/errors. Console renders the real numbers; amber fallback kept. Reads existing metrics — no new columns/migration/SOUP. - Notes:
?org_id=filter deliberately omitted (platform-wide only; one-param add post-demo). Columns in000023→ existing staging/local DBs need areset-staging-db.shto pick them up (prod apply runs the full set fresh).PlatformStatsTS type hand-authored (avoid colliding with another chat'sgenerated.ts) — fold into the generated client later.
Stream G — Foundation obligations (cross-cutting, same PR as each change) ✅
Shipped alongside each change (verify specifics against code).
- [x] Audit-log + RLS + data-classification registry entry for every new column (the
session_runs.country/citygeo capture in Stream F;strapi_user_idis reconciled by claim, no DB column per Stream B's link-by-email decision) - [x] SOUP rows for MediaPipe + Clerk-backend usage + any new direct dep
- [x]
make check/pnpm checkgreen; glossary entry for any new term
Stream H — Env lifecycle + post-stress operations ✅
The env went live and has run continuously since 2026-06-05.
- [x] One env, autoscaling between two operating points: ~5000-patient stress peak (first weeks) → ~100-concurrent tail until launch
- [x] Optional temporary RDS instance bump (
t4g.medium → large/r6g.large) for the stress weeks IF the DB is the bottleneck; size back down after (1 tfvar + apply, Multi-AZ ~1-min failover) — capability in place; apply on demand - [x] Capture stress-test findings (DB sizing, QoS/buffering at peak, Fargate counts) — the durable payoff
- [x] Keep the env live continuously through the September launch (no teardown); ~$500/mo low-usage floor — do NOT trade away HA (Multi-AZ DB, Redis replica, 2× core/portal) to go lower
Critical-path callouts (all satisfied for the demo — kept for the record)
- Clerk production application was the prerequisite for
/enterprovisioning and for the production Next.js build (publishable key is a build-arg). Stood up; reused by the September launch. platforma.restartix.rocustom hostname is ~30 min (zone already in Cloudflare) — but the org's custom-domain must be set + verified forresolveOrganizationByDomainto resolve it.- Strapi mint endpoint lives in a sibling repo (
restartix-admin-api); coordinate the deploy of the legacy side with the new/enterroute + the shared secret. - Renders baked +
readyfor the one session before launch — the play gate blocks on pending/failed renders. - Honest consent copy. The one-click screen must truthfully state camera-on preview + session-recorded-for-QoS; it's the lawful basis for the demo and feeds the real-launch counsel review.
Open decisions
| Decision | Where | Default / lean |
|---|---|---|
| Legacy ↔ new account linkage | Stream B | RESOLVED 2026-06-03 — link by email; no legacy-id column (emails are unique) |
| Consent gate copy: explicit "I agree" vs implied "Start" | Stream C | Explicit "I agree" (medical platform) |
| RDS instance class for the stress weeks | Stream H | Start db.t4g.medium; bump only if the test shows DB-bound |
Legacy entry-point placement + copy in restartix-admin-api UI | Stream B | TBD with the legacy app owner |
Handoff to the September 2026 launch
This demo was the production environment, opened early to a narrow flow — it has run live since 2026-06-05. The September 2026 full launch reuses the same env, same Clerk prod app, and same restartix org. All graduation work is now tracked in june-launch.md (the active plan), including the items that were deferred here:
- Publishing real (v2) legal text → 412 re-consent for everyone (same identity, same org — clean reset)
- Migrating the ~20k legacy patients (catalog Bucket 3) via the Stream B legacy-handoff path
- Bringing media-service + clinic live; full guided programs
- DSAR / erasure infrastructure
- The k6 5k-shape load gate
- Cross-region backups + restore drill (and the
storage-backupsdaily-dump module)
Everything the full-beta june-launch.md plan described folds into that gate.
Related documentation
- june-launch.md — the active September 2026 launch plan; carries the deferred + graduation items from this retrospective
- AWS infrastructure — production topology + the documented cost shapes
- Programs & assignments — session/protocol substrate + the read path
- Telemetry index — media-QoS half (the stress-test signal)
- Decisions —
Why Aurora Serverless v2 for staging, not production,Why clinic is controller, platform is processor - Consents migration — the 4 mandatory purposes + the re-consent gate