Skip to content

Deployment

ClinicFlow runs on Railway (backend + frontend), with Supabase for the database, the Railway Redis plugin for queues and caching, and Cloudflare for DNS/SSL.


clinicflow.lat (Cloudflare DNS)
├── app.clinicflow.lat → Railway (Frontend static service)
├── api.clinicflow.lat → Railway (Backend Node.js service)
└── Email routing → Gmail (support@, sales@, info@, billing@)

  1. Create a new Railway project
  2. Add a service → Deploy from GitHub repo → select clinic-saas-production
  3. Set Root Directory to backend
  4. Start command: node src/index.js (or Railway auto-detects from package.json start script)
  5. Add all environment variables from Environment Variables
  6. Set a custom domain: api.clinicflow.lat
  1. Add another service in the same Railway project
  2. Deploy from same repo, Root Directoryfrontend
  3. Build command: npm run build
  4. Start command: caddy run --config Caddyfile (or Railway static site detection)
  5. Environment variables:
    VITE_API_URL=https://api.clinicflow.lat/api
    VITE_LS_URL_BASIC=https://...
    VITE_LS_URL_PRO=https://...
    VITE_LS_URL_CLINICA=https://...
  6. Set custom domain: app.clinicflow.lat

The Caddyfile handles SPA routing (all paths → index.html):

:80 {
root * /app/dist
try_files {path} {path}/ /index.html
file_server
}

  1. Create a new Supabase project at supabase.com
  2. Go to SQL Editor → paste and run backend/src/db/schema.sql
  3. Enable Row Level Security on all tables (the schema file does this)
  4. Copy credentials: Settings → API → URL, anon key, service role key

After the initial schema is applied, deploy migrations:

Terminal window
cd backend
npm run migrate

Supabase Pro plan includes daily backups. For critical data (appointments, patients), set up point-in-time recovery (PITR) if on the Pro plan.


Railway provides a managed Redis plugin. Add it from the Railway dashboard:

  1. Open your project → NewPluginRedis
  2. Copy the connection string — it becomes REDIS_URL automatically in Railway environment variables

For local development, use any Redis instance (local Docker, Railway fork, or a hosted provider).


Point app and api subdomains to Railway:

Type Name Content Proxy
CNAME app your-frontend.up.railway.app ✓ (orange cloud)
CNAME api your-backend.up.railway.app ✓ (orange cloud)

SSL is handled by Cloudflare’s edge. Railway also provides SSL termination. Both work.

Email routing (for support@, sales@, info@, billing@):

In Cloudflare → Email → Email Routing:

  • Create catch-all rule forwarding to your Gmail address
  • Or add individual rules per address

  1. Create a store at lemonsqueezy.com
  2. Create products for each plan (Basic, Pro, Clínica)
  3. For each product, create a variant and copy the Variant IDLS_VARIANT_* env vars
  4. In Store → Webhooks: add a webhook pointing to https://api.clinicflow.lat/webhooks/lemonsqueezy
    • Events: order_created
    • Copy the Signing SecretLEMONSQUEEZY_SIGNING_SECRET

  1. Create a project at sentry.io (choose Node.js for backend, React for frontend)
  2. Copy the DSN values → SENTRY_DSN (backend), VITE_SENTRY_DSN (frontend)
  3. Sentry is initialized in backend/src/instrument.js and frontend/src/main.jsx

The webhook for all clinics’ WhatsApp messages routes to:

https://api.clinicflow.lat/webhooks/whatsapp

Configure in your Meta App → WhatsApp → Configuration:

  • Webhook URL: https://api.clinicflow.lat/webhooks/whatsapp
  • Verify Token: value of WHATSAPP_WEBHOOK_VERIFY_TOKEN
  • Subscribed fields: messages

Each clinic registers their own phone number through the Settings tab in the dashboard (see WhatsApp Setup Guide).


Before going live with a new environment:

  • Schema applied to Supabase (schema.sql run in SQL Editor)
  • All required env vars set in Railway
  • ENCRYPTION_KEY is exactly 32 characters
  • JWT_SECRET is at least 32 characters
  • Meta webhook URL configured and verified
  • LemonSqueezy webhook URL configured
  • Sentry DSN set (or left blank to disable)
  • Custom domains configured in Railway + Cloudflare
  • First superadmin invite code created via POST /api/superadmin/invite-codes
  • Health check passes: curl https://api.clinicflow.lat/health