Skip to content

Local Development

Get ClinicFlow running locally from scratch.


Requirement Version Notes
Node.js 22+ node --version to check
npm 10+ Comes with Node 22
Redis Any Local Docker, Railway plugin, or hosted provider
Supabase Create a free project at supabase.com

Terminal window
git clone git@github.com:ErnestNuma/clinic-saas-production.git
cd clinic-saas-production
# Install backend dependencies
cd backend && npm install
# Install frontend dependencies
cd ../frontend && npm install

Terminal window
cd backend
cp .env.example .env

Open .env and fill in the required values. The minimum set to get running locally:

PORT=3001
NODE_ENV=development
# Supabase (get from supabase.com → project → Settings → API)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=eyJhbGc...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGc...
# URL-encode password special characters, e.g. # as %23.
SUPABASE_DB_URL=postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres
# Redis (Railway plugin or local instance)
REDIS_URL=rediss://default:[password]@[host]:[port]
# Auth
JWT_PRIVATE_KEY={"kty":"EC","crv":"P-256","x":"...","y":"...","d":"...","alg":"ES256","use":"sig","kid":"local-key"}
JWT_PUBLIC_KEY={"kty":"EC","crv":"P-256","x":"...","y":"...","alg":"ES256","use":"sig","kid":"local-key","key_ops":["verify"]}
JWT_KEY_ID=local-key
JWT_ISSUER=https://your-project.supabase.co/auth/v1
JWT_AUDIENCE=authenticated
JWT_SECRET=your-qr-token-secret-at-least-32-chars
# Encryption (EXACTLY 32 characters)
ENCRYPTION_KEY=exactly-32-characters-goes-here!
# App URLs (local dev)
FRONTEND_URL=http://localhost:5173
BACKEND_URL=http://localhost:3001
BACKEND_PUBLIC_URL=http://localhost:3001
# Superadmin
SUPER_ADMIN_KEY=any-long-random-string-for-local
# WhatsApp (optional for local — chatbot won't work without these)
WHATSAPP_APP_SECRET=your-meta-app-secret
WHATSAPP_WEBHOOK_VERIFY_TOKEN=your-verify-token

See Environment Variables for the complete reference.

Terminal window
cd frontend
cp .env.example .env

Edit .env:

VITE_API_URL=http://localhost:3001/api

The LemonSqueezy URLs can be left blank for local development (Billing page links won’t work).


  1. Go to your Supabase project → SQL Editor
  2. Open backend/src/db/schema.sql
  3. Paste and run the entire file

This creates all tables, indexes, and RLS policies.

To run future migrations:

Terminal window
cd backend
npm run migrate

Step 4: Create Your First Clinic (Invite Code)

Section titled “Step 4: Create Your First Clinic (Invite Code)”

Registration requires an invite code. For local development, create one via the superadmin API:

Terminal window
curl -X POST http://localhost:3001/api/superadmin/invite-codes \
-H "X-Admin-Key: your-SUPER_ADMIN_KEY-value" \
-H "Content-Type: application/json" \
-d '{"plan": "clinica", "notes": "local dev"}'

Copy the code from the response. Use it at http://localhost:5173/register.


Terminal window
cd backend
npm run dev

Server starts on http://localhost:3001. Logs stream to the terminal.

Expected output:

info: ✅ Bull Board secured and integrated at /admin/queues
info: ✅ Redis connected
info: ✅ ClinicFlow API running on port 3001

Open a new terminal:

Terminal window
cd frontend
npm run dev

Frontend starts on http://localhost:5173.


Terminal window
# Backend tests
cd backend && npm test
# Expected: 458 tests, all pass
# Frontend tests
cd frontend && npm test
# Lint
cd frontend && npm run lint
# Expected: 0 errors

The /tmp filesystem is full. BullMQ uses tmp space for job processing.

Terminal window
df -h /tmp # check usage
du -sh /tmp/* | sort -rh | head -10 # find large files

Common culprit: accumulated log files. Remove safely:

Terminal window
rm /tmp/backend.log # if no process holds it open

If you see insufficient privilege or empty query results, RLS is blocking your queries. Make sure you’re using the service role key in .env (SUPABASE_SERVICE_ROLE_KEY), not the anon key, for supabaseAdmin.

The anon key is only for public-facing operations (unauthenticated reads).

To test the chatbot locally, you need Meta to send webhooks to your machine. Use ngrok:

Terminal window
ngrok http 3001

Then set your Meta App webhook URL to https://{ngrok-id}.ngrok.io/webhooks/whatsapp.

If you see JsonWebTokenError: secretOrPrivateKey must have a value, your JWT_SECRET is empty or too short. It must be at least 32 characters.

ENCRYPTION_KEY must be exactly 32 characters for AES-256. Add or remove characters until echo -n "$ENCRYPTION_KEY" | wc -c returns 32.