CalGest Launch-Day Operational Checklist
Generated 2026-07-23 from a code-verified production-readiness audit. Source of truth for each item is cited withfile:line. Ordered by when it must happen.
“Convex” = production Convex deployment env (npx convex env set / dashboard).
Frontend VITE_/EXPO_PUBLIC_ vars are build-time and bake into the bundle, so
they must be correct before each app is built.
Status note (2026-07-23): the production Convex deployment has 30 env vars set (names verified viaconvex env list --prod), but it is still missing the now requiredSENTRY_DSNandSENTRY_ENVIRONMENT. The deploy workflow checks the complete contract before pushing code and the deployed function checks it again afterward. Items below remain the canonical reference for account-side and client build configuration.
BEFORE DEPLOY
1. External services provisioned
- Stripe (managed_payments / alpha) — create the 6 recurring prices + the annual coupon; their IDs feed the env vars below.
packages/backend/convex/plans.ts:283-296lists the exact keys.assertBillingConfig()(plans.ts:234) hard-fails checkout if any is unset or still ends in_TODO, so placeholders block paid signups.- Coupon must be
percent_off: 20, duration: forever→STRIPE_COUPON_ANNUAL(plans.ts:68, comment at:60-66).
- Coupon must be
- Clerk — production instance with a JWT template named exactly
convex(hardcodedapplicationID: "convex"inauth.config.js:11). Issuer domain →CLERK_JWT_ISSUER_DOMAIN. - Clerk → custom email delivery via webhook: the app renders/sends Clerk transactional emails itself through the
email.createdwebhook event (http.ts:687-707, dispatches toclerkEmailActions.handleEmailCreated). Clerk must be configured to emitemail.created(i.e. “send emails via webhook” / integration enabled), otherwise verification/reset emails silently never send. - Resend — verify the sending domain; the from-address is required in prod (
notificationEmail.ts:16-27andclerkEmailActions.ts:14-21throw ifRESEND_FROM_EMAILunset outside an explicit dev env). - Cloudflare R2 — two EU-jurisdiction buckets already referenced:
calgest-prod(andcalgest-dev) inapps/cdn/wrangler.jsonc:12-16. Generate an R2 API token (S3 creds + token). - GatewayAPI (SMS) — account + sender token; choose a webhook auth token for delivery callbacks.
- OpenAI — API key for the AI assistant agent (
agent.ts:1,85-86). - Sentry — DSN + auth token/org/project for error reporting and source-map upload. The production backend gate requires the DSN.
- Keep the project on the free tier and create its single uptime monitor for
GET https://<convex-site-url>/healthz/ready. - Configure an email alert for new/high-volume errors; do not enable tracing, replay, logs, profiling, or default PII.
- Keep the project on the free tier and create its single uptime monitor for
2. Production Convex deployment env vars
Read locations in parentheses. All required unless marked optional. Auth / Clerk-
CLERK_JWT_ISSUER_DOMAIN(auth.config.js:10) -
CLERK_SECRET_KEY— backend Clerk API for Google-calendar OAuth tokens & user locale (calendarImport.ts:27,userLocale.ts:26) -
CLERK_WEBHOOK_SECRET— svix verification (http.ts:602)
STRIPE_SECRET_KEY read in http.ts:265 and by @convex-dev/stripe; STRIPE_WEBHOOK_SECRET by the component’s registerRoutes)
-
STRIPE_SECRET_KEY -
STRIPE_WEBHOOK_SECRET -
STRIPE_PRICE_SOLO_MONTHLY,STRIPE_PRICE_SOLO_YEARLY -
STRIPE_PRICE_TEAM_MONTHLY,STRIPE_PRICE_TEAM_YEARLY -
STRIPE_PRICE_TEAM_SEAT_MONTHLY,STRIPE_PRICE_TEAM_SEAT_YEARLY -
STRIPE_COUPON_ANNUAL -
ALLOWED_REDIRECT_DOMAINS— comma-separated host allowlist. Fails closed:stripe.ts:77-85rejects every non-localhost checkout success/cancel redirect when empty, so an unset value silently breaks checkout in production. Must contain the dashboard host (e.g.dashboard.calgest.com).
internalAttachments.ts:19-22, r2Deletion.ts:52-55; R2 component README lists all 5)
-
R2_BUCKET(=calgest-prod) -
R2_ENDPOINT -
R2_ACCESS_KEY_ID -
R2_SECRET_ACCESS_KEY -
R2_TOKEN(required by@convex-dev/r2component) -
ASSET_TOKEN_SECRET— HMAC secret for signed download URLs (security.ts:325). Must be byte-identical to the CDN worker’sASSET_TOKEN_SECRET(apps/cdn/src/index.ts:157) or every attachment/photo download 403s. -
CDN_URL— base for signed URLs (security.ts:308, throws if unset when issuing a download). Must behttps://cdn.calgest.com.
-
RESEND_API_KEY(Resend component) -
RESEND_FROM_EMAIL(notificationEmail.ts:16,clerkEmailActions.ts:14) -
CONTACT_EMAIL— optional, defaults togeneral@calgest.com(contactRequestsActions.ts:27) -
RESEND_WEBHOOK_SECRET— not required: no Resend delivery-event route is registered inhttp.ts. Skip unless you add one.
-
GATEWAYAPI_TOKEN— send auth (sms.ts:340) -
GATEWAYAPI_WEBHOOK_SECRET— delivery-callback JWT verification; handler returns 503 without it (http.ts:903-907)
-
OPENAI_API_KEY(AI agent) -
SENTRY_DSNandSENTRY_ENVIRONMENT=production— required by the production configuration gate; backend reporting no-ops outside that explicit environment and schedules scrubbed error delivery through an internal action (lib/observability.ts)
mobileVersion.ts:22-27) — optional but recommended so you can force-update later:
-
MIN_SUPPORTED_MOBILE_VERSION(defaults0.0.0= no floor) -
IOS_APP_STORE_URL,ANDROID_PLAY_STORE_URL(store-redirect for forced updates)
3. Frontend build-time env vars (set in host/CI before building each bundle)
Dashboard host (Vite SPA —apps/dashboard):
-
VITE_CONVEX_URL,VITE_CLERK_PUBLISHABLE_KEY,VITE_CDN_URL,VITE_SENTRY_DSN -
VITE_BOOKING_URL— documented inapps/dashboard/.env.example; set it explicitly so the public-booking URL shown in settings targets production. - Optional:
VITE_APP_VERSION; build-onlySENTRY_AUTH_TOKEN/SENTRY_ORG/SENTRY_PROJECTare required by the production deploy for source maps.
apps/web):
-
VITE_CONVEX_URL,VITE_CLERK_PUBLISHABLE_KEY,VITE_SENTRY_DSN,VITE_COMING_SOON(set"true"only if you want the splash) -
VITE_DASHBOARD_URL— documented and production-validated by the web app. An empty value is rejected before build so sign-in/pricing CTAs cannot silently ship broken. - Build-only Sentry source-map vars as above.
apps/mobile/eas.json → production profile uses environment: "production", channel production; set these as EAS env/secrets for that environment):
-
EXPO_PUBLIC_CONVEX_URL,EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY,EXPO_PUBLIC_SENTRY_DSN -
EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID,EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID,EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME(Google sign-in; the URL scheme is also hardcoded inapp.json:65and must match) -
EXPO_PUBLIC_DASHBOARD_URL— documented and production-validated by the mobile app. -
SENTRY_AUTH_TOKEN/ORG/PROJECTfor source maps. SetSENTRY_AUTH_TOKENto EAS Sensitive visibility, not Secret, because theeas env:execpreflight cannot read Secret-visibility variables. - EAS project owner
adamtrip-solutions, bundle/packagecom.adamtrip.calgest(app.json:63-82). Confirm the store submission profile (submit.production) has credentials.
4. CDN worker (Cloudflare — apps/cdn)
-
wrangler secret put ASSET_TOKEN_SECRETfor production (must equal the ConvexASSET_TOKEN_SECRET). - Confirm
vars.ALLOWED_ORIGINScovers the real dashboard/app origins — currentlyhttps://dashboard.calgest.com,https://app.calgest.com(wrangler.jsonc:9-11); CORS atindex.ts:8rejects anything else. - Custom domain
cdn.calgest.combound (wrangler.jsoncroutes), R2 bindingR2_BUCKET→calgest-prod.
AT DEPLOY
- Deploy backend:
npx convex deploy(registers all HTTP routes + crons). - Deploy CDN worker:
wrangler deploy(prod env). - Build/deploy dashboard + web; build/submit mobile via EAS production profile.
- Register Stripe webhook endpoint →
https://<convex-site-url>/stripe/webhook(path fromhttp.ts:177). Subscribe exactly these events (the only ones handled —http.ts):checkout.session.completedcheckout.session.async_payment_succeededcheckout.session.async_payment_failedcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_failedinvoice.paidpayment_intent.succeededcustomer.updated- Copy its signing secret into
STRIPE_WEBHOOK_SECRET.
- Register Clerk webhook endpoint →
https://<convex-site-url>/clerk-webhook(http.ts:592). Subscribe:user.created,user.updated,user.deleted,email.created,session.created(http.ts:653-717). Copy signing secret intoCLERK_WEBHOOK_SECRET. - Register GatewayAPI delivery webhook →
https://<convex-site-url>/gatewayapi/webhook(http.ts:906) with the chosen token asGATEWAYAPI_WEBHOOK_SECRET.
AFTER DEPLOY
5. One-off backfills / migrations
Run once. On a truly empty production DB these are no-ops, but run them anyway (idempotent) — and they are mandatory if any data was imported/migrated from a prior deployment.@convex-dev/migrations runners
(packages/backend/convex/migrations.ts); note the deploy workflow only
automates runBackfillOrganizationMembershipUserId — the rest are manual:
-
npx convex run migrations:runBackfillAttachmentEntityType --prod -
npx convex run migrations:runBackfillOrganizationMembershipUserId --prod(automated in deploy.yml) -
npx convex run migrations:runBackfillActivationCompleted --prod— billing correctness; feeds theactivationCompletedForSubscriptionIdguard and the bundled-credit cron. -
npx convex run migrations:runBackfillAiUsageCosts --prod -
npx convex run migrations:runBackfillAppointmentSeriesProgress --prod
backfillSearchTerms.ts — re-run each until
done: true (500/batch). The search query relies on searchTerms
(search.ts:32-50), so skipping them makes pre-existing
customers/employees/appointments unsearchable:
-
backfillSearchTerms:backfillCustomers -
backfillSearchTerms:backfillEmployees -
backfillSearchTerms:splitCustomerPhones -
backfillSearchTerms:splitEmployeePhones -
backfillSearchTerms:backfillAppointments -
backfillSearchTerms:backfillAppointmentDenormalization
seed.ts is dev/demo seeding — do not run in production.)
5a. Customer identity linkage preflight
Customer portal access now uses the explicitcustomers.userId link and fails
closed when it is absent. A read-only production check on 2026-07-23 found one
Customer role but zero Customer-role memberships, so the current production
dataset has no customer user to interrupt. Re-check immediately before launch.
- Page through
customerIdentityMigration:discoverCustomerRolesand runcustomerIdentityMigration:preflightfor every returned role. - If Customer-role memberships now exist, use the bounded
customerIdentityMigration:backfillBatchmutation first withdryRun: true, then withdryRun: falseonly for unambiguouslinkableIDs. It matches exact normalized email addresses and never auto-links ambiguous/no-match rows. - Resolve every remaining status explicitly before enabling customer portal access. If production has acquired memberships since the verified snapshot, use a two-phase release or a planned maintenance window so fail-closed access does not become an unexpected outage.
6. Verify crons registered
After deploy, confirm these appear in the Convex dashboard crons list (crons.ts):
- roll forward recurrence series (daily 02:00)
- purge old contact requests (daily 03:30)
- auto-complete finished appointments (every 1 min)
- cleanup expired asset upload intents (every 15 min)
- process bundled credit anniversaries (daily 03:00)
- cleanup processed stripe events (daily 04:00)
- reconcile subscriptions from stripe (daily 05:00) — nightly Stripe-truth drift correction
- purge expired message logs (daily 06:00) — GDPR retention
- sweep stalled organization deletions (hourly) — GDPR erasure resilience
- sweep orphaned r2 objects (daily 06:30) — R2 deletion compensation queue
7. Smoke tests
-
GET https://<convex-site-url>/healthz→ 200{"status":"ok"}(http.ts:1048) -
GET https://<convex-site-url>/healthz/ready→ 200 (does a real DB probe;http.ts:1049). The production workflow retries this database-backed route and fails the deploy if it never becomes ready. - End-to-end: sign up (Clerk email arrives via Resend), start a subscription checkout (verify
ALLOWED_REDIRECT_DOMAINSlets the redirect through and the webhook activates the org), upload an attachment then download it (verifiesASSET_TOKEN_SECRETparity backend↔CDN), send one SMS + confirm delivery webhook flips status. - Marketing site: click Sign in / Try for free / a pricing CTA and confirm they land on the dashboard (guards against an empty
VITE_DASHBOARD_URLbuild).