Do not open a public GitHub issue for security vulnerabilities.
Report vulnerabilities privately via GitHub Security Advisories, or email the maintainers directly with:
- The affected component or file path
- A clear description of the vulnerability and its impact
- Reproduction steps or a proof-of-concept
- Your assessment of severity and blast radius
We acknowledge all reports within 72 hours and communicate resolution status as the fix progresses.
Settler's security model is enforced across five independent layers. No single layer is sufficient alone — they compound.
- Request middleware —
tenantMiddlewareresolvesreq.tenantIdfrom JWT, API key, header, or subdomain before any route handler executes. Requests without a resolvable tenant are rejected with403. - TypeScript interfaces — Every repository method signature includes a mandatory
tenantId: stringparameter. Callers that omit it fail TypeScript compilation. - SQL queries — Every
SELECT,UPDATE, andDELETEagainst tenant-scoped tables includesAND tenant_id = $N. TheassertTenantScoped()guard throws in development if this is violated. - Row-Level Security — PostgreSQL RLS policies filter rows by
current_setting('app.current_tenant_id'). This is the database-level defense-in-depth layer. - Entity-level checks —
UserRepository.save()andJobRepository.create()throwError('Tenant mismatch')if an entity'stenantIddoes not match the scopedtenantId.
For the full mechanical specification of every invariant and its test coverage, see SECURITY_INVARIANTS.md.
requirePermission()middleware verifies the authenticated user is a member of the resolved tenant before any privileged route handler executes- OpenFGA attribute-based access control is available for fine-grained authorization
- Fail-closed posture: when OpenFGA is unavailable, requests are denied (403) rather than defaulted to permissive
- All request bodies are validated with Zod schemas before processing
- Rate limiting is enforced at the IP and authenticated-user level via Redis
- Helmet sets HTTP security headers on all responses
- CORS is explicitly configured — no wildcard origins in production
- API keys are scoped to a tenant and validated on every request
- All secrets managed via environment variables — no hardcoded credentials in source
- Supabase service role key is server-only and never exposed to the client
- JWT secrets are minimum 32 characters, enforced at startup
- Webhook payloads are signature-verified before processing
- Dependency scanning and secret scanning run in CI on every push
- Least-privilege tokens — no over-scoped credentials
- Branch protection and required PR review on the default branch
- Automated dependency updates with audit on every PR
- Code scanning and secret scanning enforced via GitHub Actions
| Severity | Acknowledgment | Target Resolution |
|---|---|---|
| Critical (RCE, data exfiltration) | 24 hours | 72 hours |
| High (auth bypass, tenant leakage) | 48 hours | 7 days |
| Medium | 72 hours | 30 days |
| Low / informational | Best effort | Next release |