Security
Security
Section titled “Security”Reporting Vulnerabilities
Section titled “Reporting Vulnerabilities”If you discover a security vulnerability, please email ernest@clinicflow.lat with:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
Do not open a public GitHub issue for security vulnerabilities.
Response time: within 48 hours. We take security reports seriously and will work with you to understand and address the issue.
Multi-Tenant Isolation (Critical — Read Before Adding Queries)
Section titled “Multi-Tenant Isolation (Critical — Read Before Adding Queries)”Protected requests use a publishable-key Supabase client carrying the original user bearer token. Jobs and trusted tenant work use a five-minute tenant-system token. Both are enforced by PostgreSQL RLS.
Every query against a clinic-owned table must still be explicitly scoped:
.eq('clinic_id', req.user.clinicId)// For the clinics table itself:.eq('id', req.user.clinicId)req.user.clinicId comes from the verified ES256 JWT, never from request input.
RLS independently validates the signed clinic claim, current user role,
activation state, and related parent rows.
Regression Guard
Section titled “Regression Guard”src/__tests__/tenantScopingGuard.test.js scans clinic-scoped routers and fails
if a clinic-owned table lacks an explicit scope or reviewed scope-by-parent
exception. src/__tests__/privilegedImportGuard.test.js limits service-role
imports to the audited global/discovery modules.
Legitimate Exceptions
Section titled “Legitimate Exceptions”| Route | Auth Model |
|---|---|
routes/superadmin.js |
X-Admin-Key header (cross-clinic intentionally) |
services/calendarFeedDiscovery.js |
Public ICS lookup by unguessable per-doctor token |
| Scope-by-parent | e.g. patients.js fetches appointments by patient_id after verifying patient belongs to clinic |
Authentication & Authorization
Section titled “Authentication & Authorization”- Passwords: bcrypt with cost factor 12
- JWT: ES256, 8-hour expiry, fixed
role=authenticated, plussub,clinic_id, andapp_role - Session freshness: RLS checks the live user, clinic, and stored role on every database statement
- Admin operations: HTTP middleware and database policies both require the admin application role
- Superadmin: Separate
X-Admin-Keyauth usingcrypto.timingSafeEqual(not JWT)
Encryption at Rest
Section titled “Encryption at Rest”- Patient DUI numbers: AES-256 (
ENCRYPTION_KEY) - WhatsApp access tokens: AES-256 (
ENCRYPTION_KEY) - API responses expose only a configured/not-configured mask; encrypted tokens are fetched only with tenant-system access
Webhook Security
Section titled “Webhook Security”- Meta/WhatsApp: HMAC-SHA256 signature verification on all inbound webhooks
- LemonSqueezy: Signature verification using
LEMONSQUEEZY_SIGNING_SECRET - Both use timing-safe comparison
Rate Limiting
Section titled “Rate Limiting”- Auth endpoints: 10 requests / 15 min / IP (failed attempts only)
- Demo form: 5 requests / hour / IP
- ICS feeds: 30 requests / 15 min / IP
- Global: 100 requests / 15 min / IP
Other Controls
Section titled “Other Controls”- CORS: Locked to
FRONTEND_URLenv var - Helmet: Security headers on all responses (HSTS, X-Content-Type-Options, etc.)
- Audit logs: Admin actions logged to
audit_logstable with IP address - No secrets in responses: Temporary passwords returned once, tokens masked
- Phone numbers: Validated to E.164 format before WhatsApp interactions