ONSET Clinic Portal - Railway Deploy Checklist¶
1) Authenticate and link project¶
2) Push environment variables to Railway¶
Set these in Railway service variables:
NEXT_PUBLIC_ONSET_API_URL(legacy:NEXT_PUBLIC_SUPABASE_URL)NEXT_PUBLIC_ONSET_API_ANON_KEY(legacy:NEXT_PUBLIC_SUPABASE_ANON_KEY)NEXT_PUBLIC_APP_URL=https://clinic.theonset.appSTRIPE_SECRET_KEYSTRIPE_WEBHOOK_SECRETNEXT_PUBLIC_STRIPE_PUBLISHABLE_KEYSTRIPE_SOLO_MONTHLY_PRICE_ID/STRIPE_SOLO_ANNUAL_PRICE_IDSTRIPE_PRACTICE_MONTHLY_PRICE_ID/STRIPE_PRACTICE_ANNUAL_PRICE_IDSTRIPE_PRACTICE_SEAT_MONTHLY_PRICE_ID/STRIPE_PRACTICE_SEAT_ANNUAL_PRICE_IDSTRIPE_FULLSCRIPT_ADDON_MONTHLY_PRICE_ID/STRIPE_FULLSCRIPT_ADDON_ANNUAL_PRICE_IDONSET_API_SERVICE_ROLE_KEY(legacy:SUPABASE_SERVICE_ROLE_KEY)
Generate the price IDs with
npm run stripe:pricing(run once with the Stripe test key, once with the live key). The legacy Pro product/prices have been archived in Stripe and removed from the env. -PATIENT_PII_ENCRYPTION_KEY(base64url-encoded 32-byte AES key — required for patient create/update; generate withnode -e "console.log(require('crypto').randomBytes(32).toString('base64url'))") -NOTIFY_FUNCTION_SECRET(shared with Supabase edge function) -SENDGRID_API_KEY(required for patient invite emails and portal Help tool submissions with attachments — copy the same key from Supabase prod Edge Function secrets or SendGrid dashboard) -NOTIFY_FROM_EMAIL(optional, defaultOnset Clinic <notifications@theonset.app>) -CLINIC_SUPPORT_INBOX_EMAIL(optional, defaultryan@theonset.app— destination for portal Help submissions) -CLINIC_SUPPORT_ADMIN_URL(optional — deep link base in support emails; defaulthttps://admin.last1.enterprises/admin/onset/support-tickets) -CRON_SECRET(used by/api/cron/weekly-summary— generate a 32+ char random string) -NEXT_PUBLIC_ONSET_APP_URL(optional, defaults tohttps://theonset.app) -NEXT_PUBLIC_ONSET_IOS_APP_STORE_URL(optional — patient invite handout PDF App Store link; defaults tohttps://apps.apple.com/us/app/the-onset/id6761314183)
Optional one-shot CLI set (after railway link):
railway variables set \
NEXT_PUBLIC_ONSET_API_URL="..." \
NEXT_PUBLIC_ONSET_API_ANON_KEY="..." \
NEXT_PUBLIC_APP_URL="https://clinic.theonset.app" \
STRIPE_SECRET_KEY="..." \
STRIPE_WEBHOOK_SECRET="..." \
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="..." \
STRIPE_SOLO_MONTHLY_PRICE_ID="..." \
STRIPE_SOLO_ANNUAL_PRICE_ID="..." \
STRIPE_PRACTICE_MONTHLY_PRICE_ID="..." \
STRIPE_PRACTICE_ANNUAL_PRICE_ID="..." \
STRIPE_PRACTICE_SEAT_MONTHLY_PRICE_ID="..." \
STRIPE_PRACTICE_SEAT_ANNUAL_PRICE_ID="..." \
STRIPE_FULLSCRIPT_ADDON_MONTHLY_PRICE_ID="..." \
STRIPE_FULLSCRIPT_ADDON_ANNUAL_PRICE_ID="..." \
ONSET_API_SERVICE_ROLE_KEY="..."
3) Deploy¶
4) Domain mapping¶
In Railway service settings:
- Add custom domain:
clinic.theonset.app
In DNS provider:
- Add the CNAME target that Railway provides for
clinic.theonset.app
5) Stripe webhook¶
In Stripe Dashboard -> Developers -> Webhooks:
- Endpoint URL:
https://clinic.theonset.app/api/stripe/webhook - Events (all 5 must be enabled — the handlers exist in
src/app/api/stripe/webhook/route.ts): checkout.session.completed— flips practitioner to PRO on first paid checkoutcustomer.subscription.deleted— resets to Free, preserves patient datacustomer.subscription.updated— syncs tier/cap on any dashboard-driven change or dunning outcomecustomer.subscription.trial_will_end— fires 3 days before trial end; enqueues a clinic_notifications row so we can email a reminder before the card is auto-chargedinvoice.payment_failed— enqueues a clinic_notifications row AND sends an immediate SendGrid alert with a Settings → Manage billing deeplink
5a) Stripe Customer Portal¶
In Stripe Dashboard -> Settings -> Billing -> Customer portal:
- Account default configuration must exist and be active.
- Required features: payment method update, invoice history, subscription cancel (mode:
at_period_end), customer update (email/name/tax_id/address). - "After update" return URL:
https://clinic.theonset.app/settings. - The
/api/billing/portalroute uses the account default unlessSTRIPE_BILLING_PORTAL_CONFIGURATION_IDis set in env.
5b) Stripe trial behavior¶
/api/stripe/checkoutcreates the Checkout Session withpayment_method_collection: "always"— card is required at signup.- Default trial is 14 days, granted only on the Practice (top) tier on first checkout (
STRIPE_TRIAL_DAYSenv override available). trial_settings.end_behavior.missing_payment_method: "cancel"— if the card is removed before trial end, the subscription cancels rather than rolling onto a dead card.
6) Smoke tests¶
https://clinic.theonset.app/loginloads.- New signup works.
/settingsshows plan card and upgrade prompt for free-tier.- Upgrade buttons redirect to Stripe Checkout.
- On successful payment, practitioner row updates to:
tier = promax_patients = 999
6.5) Weekly summary email cron (Pro practitioners)¶
Wire any HTTP cron service to hit the secure trigger on Monday morning UTC.
- Endpoint:
POST https://clinic.theonset.app/api/cron/weekly-summary - Required env vars on the clinic portal service:
CRON_SECRET— random 32+ char string, shared with the cron callerNOTIFY_FUNCTION_SECRET— already setNEXT_PUBLIC_ONSET_API_URL(or legacy NEXT_PUBLIC_SUPABASE_URL) — already set- Authentication options:
Authorization: Bearer <CRON_SECRET>(preferred, works with Railway Cron / GitHub Actions / Upstash QStash / external schedulers)?secret=<CRON_SECRET>plusx-vercel-cronheader (for Vercel Cron clones)
Suggested Railway Cron service (separate one-off command service in the same Railway project):
curl -fsSL -X POST \
-H "Authorization: Bearer $CRON_SECRET" \
https://clinic.theonset.app/api/cron/weekly-summary
Schedule: 0 13 * * 1 (Monday 1pm UTC ≈ 9am ET / 6am PT).
The endpoint just authenticates the call and forwards eventType: "weekly_summary" to the practitioner-notifications Supabase edge function, which already selects Pro practitioners and composes the email body.
7) End-to-end test plan (manual cross-platform)¶
- Sign up practitioner.
- Add patient.
- Build + assign protocol (10+ items).
- iOS app enters connection code, accepts protocol.
- Complete items -> adherence sync appears in clinic portal patient detail.
- Share labs in iOS -> labs appear in clinic portal patient detail.
- Upgrade to PRO via Stripe -> tier and limits update.