Architecture
Security
OpsDash is built with a three-layer security architecture: Row-Level Security at the database, server-side auth and role checks at the application layer, and audit logging for every mutation.
Three-layer model
Database — Supabase RLS
Every query is filtered by org_id at the PostgreSQL level. Even if application code had a bug, the database would not return another org's data.
Application — Server Actions
Every server action calls withOrg() first. It verifies authentication, resolves the org by slug, and checks org membership. If any check fails, the action returns an error immediately.
Role checks
Sensitive actions additionally check the user's org role. requireNonViewer() blocks viewer mutations. withOrgAdmin() blocks non-admin roles. requireFeatureAccess() checks feature flag + role allowlist.
Row-Level Security
All data tables have RLS policies that enforce org_id scoping.
-- Used in all RLS policies
is_org_member(org_id uuid) → boolean
is_org_admin(org_id uuid) → boolean
is_same_org_member(user_id uuid) → boolean| Table | Operation | Policy |
|---|---|---|
| All domain tables | SELECT / INSERT / UPDATE / DELETE | is_org_member(org_id) |
| organizations | SELECT | deleted_at IS NULL AND is_org_member(id) |
| organizations | UPDATE | is_org_admin(id) |
| audit_logs | SELECT (admin) | is_org_admin(org_id) |
| audit_logs | INSERT | is_org_member(org_id) |
| feature_flags | INSERT / UPDATE | is_org_admin(org_id) |
| users | SELECT | auth.uid() = id OR is_same_org_member(id) |
Authentication
OpsDash uses Supabase Auth. Sessions are stored in HTTP-only cookies — no tokens in localStorage.
- Email + password (always available)
- Google OAuth (configurable)
- GitHub OAuth (configurable)
- TOTP 2FA (any authenticator app — Supabase MFA, available on every plate incl. Free)
Audit logging
Every mutation calls createAuditLog() after a successful DB write. Audit logs are written to the audit_logs table and are visible to org admins in Settings → Security → Audit Log.
await createAuditLog(supabase, org.id, user.id, 'contact.created', {
contact_id: contact.id,
name: contact.full_name,
});Viewing audit logs in the UI is gated to the Full Loop plate (platform:audit-logs). On lower plates, logs are still written to the table but not surfaced in the UI.
API key security
- Keys are generated as
opsdash_<random 64-char hex> - Only the SHA-256 hash is stored — the plain key is shown once at creation
- Keys carry a free-form
scopesarray for future per-scope enforcement (e.g.lead:create,form:submit) - Every request touches the key's
last_used_at; call volume counts against the org-wide monthly quota inorg_usage - Rate limited to the org's plan API quota (enforced in
verifyApiKey()→ 429 when exceeded)
Cron endpoint security
All cron endpoints require a Bearer token matching CRON_SECRET. This must be a random 32-byte hex string — never leave it unset in production.
# Generate a secure cron secret
openssl rand -hex 32