Skip to main content

CalGest Launch-Day Operational Checklist

Generated 2026-07-23 from a code-verified production-readiness audit. Source of truth for each item is cited with file: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 via convex env list --prod), but it is still missing the now required SENTRY_DSN and SENTRY_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-296 lists 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: foreverSTRIPE_COUPON_ANNUAL (plans.ts:68, comment at :60-66).
  • Clerk — production instance with a JWT template named exactly convex (hardcoded applicationID: "convex" in auth.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.created webhook event (http.ts:687-707, dispatches to clerkEmailActions.handleEmailCreated). Clerk must be configured to emit email.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-27 and clerkEmailActions.ts:14-21 throw if RESEND_FROM_EMAIL unset outside an explicit dev env).
  • Cloudflare R2 — two EU-jurisdiction buckets already referenced: calgest-prod (and calgest-dev) in apps/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.

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 (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-85 rejects 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).
R2 / attachments / CDN (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/r2 component)
  • ASSET_TOKEN_SECRET — HMAC secret for signed download URLs (security.ts:325). Must be byte-identical to the CDN worker’s ASSET_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 be https://cdn.calgest.com.
Email
  • RESEND_API_KEY (Resend component)
  • RESEND_FROM_EMAIL (notificationEmail.ts:16, clerkEmailActions.ts:14)
  • CONTACT_EMAIL — optional, defaults to general@calgest.com (contactRequestsActions.ts:27)
  • RESEND_WEBHOOK_SECRETnot required: no Resend delivery-event route is registered in http.ts. Skip unless you add one.
SMS (GatewayAPI)
  • GATEWAYAPI_TOKEN — send auth (sms.ts:340)
  • GATEWAYAPI_WEBHOOK_SECRET — delivery-callback JWT verification; handler returns 503 without it (http.ts:903-907)
AI / observability
  • OPENAI_API_KEY (AI agent)
  • SENTRY_DSN and SENTRY_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)
Mobile force-update gate (mobileVersion.ts:22-27) — optional but recommended so you can force-update later:
  • MIN_SUPPORTED_MOBILE_VERSION (defaults 0.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 in apps/dashboard/.env.example; set it explicitly so the public-booking URL shown in settings targets production.
  • Optional: VITE_APP_VERSION; build-only SENTRY_AUTH_TOKEN/SENTRY_ORG/SENTRY_PROJECT are required by the production deploy for source maps.
Web / marketing host (Vite SPA — 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.
EAS / mobile build profiles (apps/mobile/eas.jsonproduction 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 in app.json:65 and must match)
  • EXPO_PUBLIC_DASHBOARD_URL — documented and production-validated by the mobile app.
  • SENTRY_AUTH_TOKEN/ORG/PROJECT for source maps. Set SENTRY_AUTH_TOKEN to EAS Sensitive visibility, not Secret, because the eas env:exec preflight cannot read Secret-visibility variables.
  • EAS project owner adamtrip-solutions, bundle/package com.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_SECRET for production (must equal the Convex ASSET_TOKEN_SECRET).
  • Confirm vars.ALLOWED_ORIGINS covers the real dashboard/app origins — currently https://dashboard.calgest.com,https://app.calgest.com (wrangler.jsonc:9-11); CORS at index.ts:8 rejects anything else.
  • Custom domain cdn.calgest.com bound (wrangler.jsonc routes), R2 binding R2_BUCKETcalgest-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 endpointhttps://<convex-site-url>/stripe/webhook (path from http.ts:177). Subscribe exactly these events (the only ones handled — http.ts):
    • checkout.session.completed
    • checkout.session.async_payment_succeeded
    • checkout.session.async_payment_failed
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.payment_failed
    • invoice.paid
    • payment_intent.succeeded
    • customer.updated
    • Copy its signing secret into STRIPE_WEBHOOK_SECRET.
  • Register Clerk webhook endpointhttps://<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 into CLERK_WEBHOOK_SECRET.
  • Register GatewayAPI delivery webhookhttps://<convex-site-url>/gatewayapi/webhook (http.ts:906) with the chosen token as GATEWAYAPI_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 the activationCompletedForSubscriptionId guard and the bundled-credit cron.
  • npx convex run migrations:runBackfillAiUsageCosts --prod
  • npx convex run migrations:runBackfillAppointmentSeriesProgress --prod
Batched internal mutations in backfillSearchTerms.tsre-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 explicit customers.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:discoverCustomerRoles and run customerIdentityMigration:preflight for every returned role.
  • If Customer-role memberships now exist, use the bounded customerIdentityMigration:backfillBatch mutation first with dryRun: true, then with dryRun: false only for unambiguous linkable IDs. 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_DOMAINS lets the redirect through and the webhook activates the org), upload an attachment then download it (verifies ASSET_TOKEN_SECRET parity 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_URL build).