Skip to main content

UAT Deploy Runbook

Overview

The OdontoX UAT environment lives at uat.odontox.io and is deployed from the perf/neon-tanstack branch. The Cloudflare Worker (odonto-uat) handles the API at uat.odontox.io/api/*; Cloudflare Pages serves the rest of the UI under the same hostname. The UAT database is a Neon branch off prod — schema and data are real prod copies, so all outbound communication (email, WhatsApp, SMS, Stripe charges) is hard-disabled via the UAT_DISABLE_OUTBOUND=true environment variable to ensure no real patient is contacted or charged from UAT.

Architecture (single subdomain, path-routed)

PathHandled by
https://uat.odontox.io/api/*Worker odonto-uat
https://uat.odontox.io/*Cloudflare Pages (UI)
This collapses prod’s multi-subdomain split (api., id., go., portal., tenants.) into a single host. The UI helper subdomain-utils.ts detects uat.odontox.io and keeps cross-host helpers on the same origin. Public share links and asset URLs likewise stay path-rooted.

Resources already created

ResourceUAT value
Worker nameodonto-uat
KV namespace OTT_STOREfad4a0a6b8d9449bb6cceac8723524b2
R2 bucketodontox-files-uat
Rate limiter namespace1002 (prod uses 1001)
Durable Object migrationv1-clinic-hub-uat
Cron schedulemirrors prod (0 4, 0 9, 0 14, 0 16)
Database (Neon branch)ep-super-sun-a1y850mh-pooler.ap-southeast-1

Secrets to set (run from server/)

DATABASE_URL is already set. Run each line below — values come from a secure source (1Password / prod wrangler), NOT this file.
# Already done at provision time:
# echo "<UAT Neon URL>" | wrangler secret put DATABASE_URL --env uat

# REQUIRED — UAT-specific JWT (generate fresh, do NOT reuse prod):
openssl rand -base64 64 | wrangler secret put JWT_SECRET --env uat

# CRITICAL — must match prod so prod-cloned PHI in the Neon branch is
# decryptable. Security trade-off: a UAT compromise gives the attacker the
# same key as prod, but cloning data is the whole point of UAT.
wrangler secret put ENCRYPTION_KEY --env uat   # paste prod value at prompt

# Public-link signing secret (UAT-specific, generate fresh):
openssl rand -base64 32 | wrangler secret put PUBLIC_LINK_SECRET --env uat

# Firebase admin (same project as prod is fine; rotate to UAT-only if you
# prefer a separate Firebase project):
wrangler secret put FIREBASE_PROJECT_ID --env uat
wrangler secret put FIREBASE_CLIENT_EMAIL --env uat
wrangler secret put FIREBASE_PRIVATE_KEY --env uat

# Email (Zepto). Outbound is gated by UAT_DISABLE_OUTBOUND so the key is
# only needed if you ever want to test the un-gated path; otherwise skip.
wrangler secret put ZEPTO_API_KEY --env uat   # OPTIONAL

# WhatsApp Cloud API. Likewise gated; skip unless you toggle the flag off.
wrangler secret put WHATSAPP_ACCESS_TOKEN --env uat            # OPTIONAL
wrangler secret put WHATSAPP_WEBHOOK_VERIFY_TOKEN --env uat    # OPTIONAL

# Stripe. Use TEST keys if you ever want to verify Stripe codepaths in UAT.
# Outbound is gated by default.
wrangler secret put STRIPE_SECRET_KEY --env uat        # OPTIONAL — use test key
wrangler secret put STRIPE_WEBHOOK_SECRET --env uat    # OPTIONAL — test key

# DeepSeek (AI):
wrangler secret put DEEPSEEK_API_KEY --env uat

# Langfuse tracing (HIPAA cloud). Use a UAT-specific project key, not prod.
wrangler secret put LANGFUSE_PUBLIC_KEY --env uat
wrangler secret put LANGFUSE_SECRET_KEY --env uat

# PostHog analytics (UAT project key):
wrangler secret put POSTHOG_API_KEY --env uat
wrangler secret put POSTHOG_HOST --env uat

# Cloudflare Turnstile (UAT site key):
wrangler secret put TURNSTILE_SECRET_KEY --env uat

# OpenAI (if any DICOM-vision fallbacks remain wired to OpenAI):
wrangler secret put OPENAI_API_KEY --env uat

# R2 S3-compatible credentials (UAT bucket: odontox-files-uat):
wrangler secret put R2_ACCESS_KEY_ID --env uat
wrangler secret put R2_SECRET_ACCESS_KEY --env uat
wrangler secret put R2_ENDPOINT --env uat

# Superadmin gate:
wrangler secret put SUPERADMIN_ALERT_EMAIL --env uat

# OTLP ingest (if observability pipe is enabled):
wrangler secret put OTLP_INGEST_SECRET --env uat
After setting them all:
cd server && wrangler secret list --env uat

DNS (Cloudflare DNS, proxied)

The single uat.odontox.io record is shared by Pages + the Worker route. Pages will give you a *.pages.dev target after the project is created.
TypeNameTargetProxy
CNAMEuat<your-pages-project>.pages.devyes
The Worker route uat.odontox.io/api/* short-circuits before Pages.

Cloudflare Pages project

  1. Cloudflare dashboard → Workers & Pages → Create application → Pages → Connect to Git.
  2. Repo: odontoX. Production branch: perf/neon-tanstack.
  3. Project name: odontox-uat-ui.
  4. Build command: cd ui && pnpm install && pnpm build (UI build script is vite build && node postbuild.js.)
  5. Build output directory: ui/dist.
  6. Environment variables (Production branch):
    • VITE_API_URL — leave empty (api-url.ts auto-detects UAT host).
    • VITE_ASSET_URL — leave empty.
  7. Custom domain → Set up custom domain → uat.odontox.io.

Worker deploy

cd server && wrangler deploy --env uat

Verification checklist

  • curl https://uat.odontox.io/api/v1/health returns 200.
  • Login flow on https://uat.odontox.io/auth/login succeeds against the UAT Neon branch DB (not prod).
  • Patients / Appointments / Billing modules load data; cross-check at least one record vs. the UAT branch DB to confirm it’s UAT, not prod.
  • Mutations (create patient, schedule appointment) write only to the UAT DB — re-read to confirm; the prod DB row count must NOT change.
  • Worker logs show [UAT] outbound email disabled, skipping ... lines and zero send events appear in the Zepto dashboard during testing.
  • Stripe dashboard (test + live) shows zero new charges/customers from UAT activity. Worker logs show [UAT] outbound Stripe ... disabled.
  • WhatsApp Business Manager shows zero outbound messages from the UAT session. Worker logs show [UAT] outbound WhatsApp disabled.
  • Cron handlers fire on schedule (wait at least 24 h, then wrangler tail --env uat | grep cron).
  • On login/logout, clearAllQueryCaches clears React Query localStorage (verify via DevTools → Application → Local Storage).
  • Cross-tenant cache isolation: log in as a user with multiple clinics, switch clinics, confirm data does not bleed between caches.

Rollback

UAT is non-customer-facing. To roll back, redeploy the previous Worker version with wrangler rollback --env uat and revert the Pages deployment in the dashboard.