Security
Security in Reactor spans three layers: infrastructure secrets, application-level row-level security, and operational access control. This guide covers all three for self-hosted and managed deployments.
Security model
Section titled “Security model”flowchart TB Client[Client / SDK] -->|JWT| API[Public API /auth /data /storage] Admin[CLI / CI] -->|Admin Bearer| AdminAPI[/_admin/*] API --> RLS[PostgreSQL RLS] API --> Vault[Vault / env secrets] AdminAPI --> Vault| Layer | Mechanism | Scope |
|---|---|---|
| User auth | JWT (access + refresh tokens) | End-user API access |
| Data isolation | PostgreSQL RLS policies | Per-row tenant/user scoping |
| Admin ops | Bearer token on /_admin/* | Deploy, migrate, doctor, logs |
| Internal | /_internal/* + shared secret | Capability-to-capability (sizes 5–6 only) |
| Secrets | Vault (embedded or OpenBao) | API keys, encryption keys |
Secrets management
Section titled “Secrets management”Required secrets
Section titled “Required secrets”Generate strong, unique values for each:
# Admin token (min 32 chars)openssl rand -hex 32
# Auth data key (32 bytes, base64)openssl rand -base64 32
# Storage signing secretopenssl rand -hex 32
# Functions env encryption keyopenssl rand -base64 32
# Jobs webhook secretopenssl rand -hex 32
# Vault master key (if using embedded vault)openssl rand -hex 32Storage options
Section titled “Storage options”Recommended for containers and Fly.io:
flyctl secrets set \ REACTOR_ADMIN__TOKEN="..." \ REACTOR_AUTH__DATA_KEY="..." \ REACTOR_STORAGE__SIGNING_SECRET="..." \ REACTOR_FUNCTIONS__DATA_KEY="..." \ REACTOR_JOBS__WEBHOOK_SECRET="..."Never commit secrets to git. Use .env locally (gitignored).
[vault]backend = "embedded"path = ".reactor/vault"master_key = "env:REACTOR_VAULT_MASTER_KEY"Reference secrets in config:
[ai]openrouter_api_key = "vault:ai/openrouter_key"[vault]backend = "openbao"address = "https://openbao.internal:8200"auth_method = "approle"role_id = "env:BAO_ROLE_ID"secret_id_file = "/run/secrets/bao-secret-id"kv_mount = "secret"transit_mount = "transit"Used in C6@fly shared cluster (3-node HA OpenBao).
reactor cloud secrets set STRIPE_KEY=sk_live_xxx RESEND_KEY=re_xxxSecrets are AES-256-GCM encrypted at rest in the control plane DB and injected into your Fly machine on restart.
Secret rotation
Section titled “Secret rotation”| Secret | Rotation impact | Procedure |
|---|---|---|
admin.token | Breaks CLI/CI until updated | Set new value, restart, update CI vars |
auth.data_key | Requires re-encryption migration | Use auth key rotation API (planned) |
storage.signing_secret | Invalidates existing signed URLs | Rotate during low traffic; URLs expire naturally |
| JWT signing keys | Invalidates active sessions | Auth rotates keys on schedule; clients refresh |
Row-level security (RLS)
Section titled “Row-level security (RLS)”Reactor Data enforces access control at the PostgreSQL layer. Policies reference the authenticated user’s JWT claims via current_setting('request.jwt.claims').
Enable RLS on a table
Section titled “Enable RLS on a table”-- migrations/001_create_posts.sqlCREATE TABLE posts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id), title TEXT NOT NULL, body TEXT, published BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT now());
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- Users read their own postsCREATE POLICY posts_select_own ON posts FOR SELECT USING (user_id = (current_setting('request.jwt.claims', true)::json->>'sub')::uuid);
-- Users insert their own postsCREATE POLICY posts_insert_own ON posts FOR INSERT WITH CHECK (user_id = (current_setting('request.jwt.claims', true)::json->>'sub')::uuid);
-- Users update their own postsCREATE POLICY posts_update_own ON posts FOR UPDATE USING (user_id = (current_setting('request.jwt.claims', true)::json->>'sub')::uuid);
-- Anyone can read published postsCREATE POLICY posts_select_published ON posts FOR SELECT USING (published = true);Service role bypass
Section titled “Service role bypass”Server-side functions using the service role JWT (or internal auth at size 2) bypass RLS for admin operations. Never expose service credentials to clients.
// Client SDK — subject to RLSconst { data } = await reactor.data.from('posts').select('*');
// Server function — uses service context// RLS policies with service_role check apply insteadMulti-tenant RLS pattern
Section titled “Multi-tenant RLS pattern”For org-scoped data, include org_id in JWT claims:
CREATE POLICY org_isolation ON documents FOR ALL USING ( org_id = (current_setting('request.jwt.claims', true)::json->>'org_id')::uuid );See the Multi-tenant app guide for a complete walkthrough.
Admin endpoints
Section titled “Admin endpoints”All routes under /_admin/* require the admin bearer token:
Authorization: Bearer <admin.token>| Endpoint | Method | Risk if exposed |
|---|---|---|
/_admin/deploy | POST | Arbitrary code/migration deployment |
/_admin/migrate | POST | Schema modification |
/_admin/shutdown | POST | Denial of service |
/_admin/logs | GET | Information disclosure |
/_admin/doctor | GET | Infrastructure fingerprinting |
/_admin/version | GET | Low — version info |
Hardening admin access
Section titled “Hardening admin access”[admin]token = "{{ env REACTOR_ADMIN_TOKEN }}"allow_remote = false # default: localhost onlyKeep allow_remote = false. Run deploys via SSH tunnel:
ssh -L 8000:127.0.0.1:8000 user@vpsreactor deploy --endpoint http://localhost:8000Set allow_remote = true and restrict at the firewall:
location /_admin/ { allow 203.0.113.0/24; # CI runner IP range deny all; proxy_pass http://127.0.0.1:8000;}Admin token is injected at provision time. Access deploy via authenticated CLI only — the control plane proxies with your API key, not the raw admin token.
Network security
Section titled “Network security”Terminate TLS at Caddy, nginx, Fly.io, or your CDN. Reactor serves plain HTTP internally.
Restrict origins in production:
# Per-capability CORS is configured at the server level[server]# Use env or reverse proxy headers for CORS in v0Place CORS rules at the reverse proxy when possible:
api.myapp.com { @preflight method OPTIONS handle @preflight { header Access-Control-Allow-Origin "https://myapp.com" header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" header Access-Control-Allow-Headers "Authorization, Content-Type" respond 204 } reverse_proxy localhost:8000}PostgreSQL
Section titled “PostgreSQL”- Enable SSL for remote connections (
sslmode=requirein connection URL) - Use least-privilege database roles — Reactor migrations create capability-specific schemas
- On shared cluster: per-tenant roles with
CONNECTION LIMIT
Firewall checklist
Section titled “Firewall checklist”- Port 8000 (or your bind port) not publicly exposed without TLS proxy
- PostgreSQL not publicly accessible
- Admin endpoints blocked from internet (or IP-restricted)
- SSH key-only access to VPS
Authentication hardening
Section titled “Authentication hardening”[auth]data_key = "{{ env REACTOR_AUTH_DATA_KEY }}"jwt_issuer = "reactor-auth"jwt_audience = "reactor"access_ttl_secs = 3600 # 1 hour — shorten for high-security appsrefresh_ttl_secs = 604800 # 7 days — reduce from default 30 dayspublic_url = "https://api.myapp.com"- Enable SMTP for email verification before allowing sign-ups in production
- Configure OAuth providers with strict redirect URI allowlists (see OAuth setup guide)
- Monitor failed auth attempts via
reactor_http_requests_total{capability="auth",status="401"}
Shared cluster isolation (C6@fly)
Section titled “Shared cluster isolation (C6@fly)”On the Reactor.cloud shared cluster:
- Each tenant gets database
tenant_<ref>with dedicated role - Host-based routing resolves tenant before any query executes
- Quotas enforce resource limits per tier
- NATS topics are tenant-prefixed:
reactor.{ref}.data.>
If tenant isolation is suspected compromised:
- Disable the tenant route immediately
- Audit logs for the project ref
- Escalate to security@reactor.cloud
Production checklist
Section titled “Production checklist”Secrets and credentials
Section titled “Secrets and credentials”- All required secrets generated with cryptographic randomness
- Secrets stored in vault, env vars, or provider secret manager — not in git
- Admin token rotated from default/dev value
- Separate secrets per environment (dev/staging/prod)
Access control
Section titled “Access control”-
[auth]section configured — no anonymous access - RLS enabled on all user-facing tables
-
allow_remote = falsefor admin (or IP-restricted) - Service role credentials never shipped to clients
- OAuth redirect URIs restricted to your domains
Network and transport
Section titled “Network and transport”- HTTPS enabled with valid certificates
- PostgreSQL SSL enabled for remote connections
- Firewall rules restrict direct database access
- CORS limited to known frontend origins
Operations
Section titled “Operations”- Automated database backups configured
- File storage backups (blobs, functions, sites)
-
/healthmonitored by load balancer -
/metricsscraped by Prometheus - Log aggregation configured
- Incident response contacts documented
Compliance
Section titled “Compliance”- Data retention policies defined
- User deletion flow implemented (auth + data cascade)
- Audit logging enabled for admin operations (C6@fly control plane)
Related
Section titled “Related”- Configuration —
[admin],[vault],[auth]reference - Observability — monitoring auth failures and admin access
- OAuth setup — secure third-party login