Skip to content

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 plan platform-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 restartix organization (our own clinic), reached via the vanity custom domain platforma.restartix.ro (an alias for restartix.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.medium hold 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 plays

Scope 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 liveFull 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 statsF3 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 restartix org via custom-domain aliasplatforma.restartix.ro resolves to the restartix org through the existing custom-hostname path (cloudflaresaas Go pkg + portal resolveOrganizationByDomain). restartix.ro is 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 + restartix org_terms as 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). Stamp strapi_user_id on 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 + ready on 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-only database-rds module — 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/production is provisioned and live — RDS Multi-AZ + Redis + ECS Fargate, serving real patients since 2026-06-05, prod DB at schema v38. Single AWS account 538636660353, 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_dispatch from master, rebuilding images per-env (Next.js bakes env-specific vars at build time; no staging-SHA promotion).

  • Approval gate — GitHub production Environment + 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-editable stays 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-production OIDC role (trusts the production GitHub Environment, not a branch) + deploy-production.yml (workflow_dispatch from master, rebuilds images) + the production Environment with required reviewers

  • [x] Implement the database-rds module (was scaffold-only): aws_db_subnet_group, parameter group (rds.force_ssl=1, max_connections=200, shared_preload_libraries=pg_stat_statements), aws_db_instance Multi-AZ db.t4g.medium + Performance Insights (7-day) + Enhanced Monitoring, SM-managed master password, storage_encrypted under the prod CMK, deletion_protection=true, apply_immediately=false, maintenance_window=sun:02:00-03:00 UTC, 7-day PITR, final_snapshot_identifier set

  • [x] infra/envs/production/main.tf: KMS CMK + Redis Multi-AZ (replicas=1, cache.t4g.small) + VPC (nat_strategy="nat-gateway", both AZs) + all restartix/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 + clinic at desired_count=0. ALB + ECR repos + ACM cert. core-api secret_env_vars must include LEGACY_HANDOFF_SECRET (from restartix/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.ro as a custom hostname on the restartix.ro zone + DV cert; lock ALB SG to Cloudflare IP ranges

  • [x] Secrets population (populate-production-secrets.sh, cloned from staging, ENV=production hardcoded, recovery_window=30 on 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.sh env 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 (commit e25149e, not pushed): authed GET /api/legacy-handoff?session=<id> mints HS256 {email, name, content_id, jti, exp:+120s, iss:"api.restartix.ro", aud:"platforma.restartix.ro"} signed with LEGACY_HANDOFF_SECRET, returns JSON {url:"<LEGACY_HANDOFF_URL>?t=…"} — the full hand-off URL comes from a Strapi LEGACY_HANDOFF_URL env var (paired with LEGACY_HANDOFF_SECRET; Railway-configurable, NOT hardcoded; https://platforma.restartix.ro/enter — includes the /enter path so the route name is config too; the controller appends ?t=). The aud claim is platforma.restartix.ro (changed 2026-06-04 to match the domain — both sides + the Go LegacyHandoffAudience default + test fixtures updated); iss stays api.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, and window.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, grant consumer.legacyHandoff to 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-use jti via Redis SET NX, return {email, name}. Built via internal/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_id claim; handoff 200 returns it when non-empty; /enter carries it to /demo as ?c= + sets the patient name on the Clerk user; /democlaim(program_id = content_id)
  • [x] Portal /enter route (added to isPublicRoute): calls the handoff endpoint → Clerk getUserList/createUser by email → createSignInToken → redirect /demo with the ticket (app/enter/page.tsx)
  • [x] /demo ticket 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) + optional protocols.SelfEnrollForClaim (2nd idempotent tx). Mounted outside RequireCurrentConsents/RequireOrganizationScope, self-service "me" posture. Response carries enrolled/protocol_id? — Stream C must handle enrolled:false
  • [x] /enter + /demo added to isPublicRoute in apps/portal/proxy.ts; org context resolves via the platforma.restartix.ro custom domain

Shippedmake 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] /demo page (page + demo-client + demo-consent + demo-session + actions) — ticket sign-in reused (ticket-sign-in.tsx, strips only ticket); c=content_id kept in the URL (refresh-safe; non-sensitive)
  • [x] Consent screen — gated on /me is_patient_at_current_org (skips for returning patients); greeting + honest beta/camera blurb + dynamically-fetched required documents (legal_basis≠consent, each w/ a LegalDocumentDialog); 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. PlaystartDemoSessionAction sets a demo-active cookie + enters the existing kiosk run flow
  • [x] Return-stats/runs/[runId]/done reads demo-active → "Back to the session" → /demo?run=<id>; /demo reads ?runGET /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/empty content_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_id claim (no DB designation column). Go-live step (user): publish the program + confirm renders are ready on 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 the framing stage; 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/tv at 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 into 000023, not a new ALTER — matches the editable-migration convention + check-classification parses CREATE TABLE) + idx_session_runs_started_geo. New internal/shared/clientgeo reads CF-IPCountry/CF-IPCity; threaded through POST /v1/session-runs and 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 the patient_id AND org_id filters, 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.fetchPlatformMediaQoS populates media_qos (watch % = avg_completion_pct, summed buffering, p95 load/TTFB); available=false only 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 in 000023 → existing staging/local DBs need a reset-staging-db.sh to pick them up (prod apply runs the full set fresh). PlatformStats TS type hand-authored (avoid colliding with another chat's generated.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/city geo capture in Stream F; strapi_user_id is 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 check green; 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)

  1. Clerk production application was the prerequisite for /enter provisioning and for the production Next.js build (publishable key is a build-arg). Stood up; reused by the September launch.
  2. platforma.restartix.ro custom hostname is ~30 min (zone already in Cloudflare) — but the org's custom-domain must be set + verified for resolveOrganizationByDomain to resolve it.
  3. Strapi mint endpoint lives in a sibling repo (restartix-admin-api); coordinate the deploy of the legacy side with the new /enter route + the shared secret.
  4. Renders baked + ready for the one session before launch — the play gate blocks on pending/failed renders.
  5. 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

DecisionWhereDefault / lean
Legacy ↔ new account linkageStream BRESOLVED 2026-06-03 — link by email; no legacy-id column (emails are unique)
Consent gate copy: explicit "I agree" vs implied "Start"Stream CExplicit "I agree" (medical platform)
RDS instance class for the stress weeksStream HStart db.t4g.medium; bump only if the test shows DB-bound
Legacy entry-point placement + copy in restartix-admin-api UIStream BTBD 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-backups daily-dump module)

Everything the full-beta june-launch.md plan described folds into that gate.

  • 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)
  • DecisionsWhy Aurora Serverless v2 for staging, not production, Why clinic is controller, platform is processor
  • Consents migration — the 4 mandatory purposes + the re-consent gate