Security & Compliance
Technical design of authentication, authorization, and data protection
Overview
Danvas employs a multi-layered security strategy to protect sensitive operational data. This includes robust identity management via Clerk, strict multi-tenant isolation, automated rate limiting, and comprehensive audit trails.
Authentication & Identity
Clerk Integration
We leverage Clerk for all authentication and session management.
- Teams: A Canvas Forge concept, not a Clerk one.
users.teamIdis a local tenant identifier configured throughCANVAS_TEAM_ID. Clerk Organizations are not used and are never read to establish team membership. - Roles: Enforced from the local
users.rolecolumn only. Clerk metadata and session claims are not written or read as authorization inputs. - Provisioning: Invitation-only.
provisionUser()claims an outstanding invitation on first sign-in or grants nothing, and Clerk webhooks sync approved profile fields only. See ADR-0018.
Auth Guards
All data access is guarded by server-side checks. Prefer getActionContext() — it bundles auth, role, and location scope in one call and is the source of truth for the navigation. The raw requireUserAuth() / requireAdmin() shims from @repo/auth/get-user-auth are still exported but are being phased out.
// apps/app/src/lib/action-context.ts
import { getActionContext } from "@/lib/action-context";
export async function secureAction() {
const { auth } = await getActionContext("admin");
// auth.userId, auth.teamId, auth.role, auth.locationIds
// redirects to /403 for non-admins; switch to "manager" /
// "adminOrManager" in API routes to throw instead of redirect.
}Data Isolation
Danvas is a multi-tenant platform. Data isolation is enforced at the database query level using the local teamId.
- Multi-tenancy: Every row in the database (except global configuration) contains a
teamId. - Query Scoping: All Drizzle queries MUST include a filter for the active team.
- Location Isolation: Operational data (reports, shifts, incidents) is further isolated by
locationId.
API & Network Security
Rate Limiting
We use Upstash Redis to implement sliding-window rate limiting on critical endpoints.
- AI Chat: 20 requests per minute per user.
- Public APIs: 10 requests per minute per IP.
- Fallback: System falls back to in-memory limiting if Redis is unreachable.
Security Headers
All HTTP responses include security-hardening headers via Nosecone:
- CSP: Restricts content sources to trusted domains.
- HSTS: Enforces secure connections over HTTPS.
- X-Frame-Options: Prevents clickjacking attacks.
Data Protection
Encryption at Rest
Sensitive information, such as Slack bot secrets, is encrypted using AES-256-GCM before being stored in the database.
PII Redaction at the Edge
Customer contact details (emails, phone numbers) must never reach model context, error reports, or product analytics. A single redaction primitive lives in @repo/security/pii and is wired into every output surface:
import { redactPII, scrubPII } from "@repo/security/pii";
redactPII("Email me at jane@example.com or 415-555-1234");
// → "Email me at [EMAIL_REDACTED] or [PHONE_REDACTED]"
scrubPII({ name: "Jane", contact: "jane@example.com", nested: { phone: "415-555-1234" } });
// → { name: "Jane", contact: "[EMAIL_REDACTED]", nested: { phone: "[PHONE_REDACTED]" } }Consumers:
- Sentry —
scrubSentryEventruns inbeforeSendfor the client, server, and edge runtimes. It scrubsmessage,exception.values,breadcrumbs, andextrabefore the event leaves the process. - PostHog — Use
captureSecureEventfrom@repo/telemetry/serverinstead of callinganalytics.capturedirectly. Event properties are scrubbed withscrubPIIbefore transmission. Scrubbing is depth-bounded (six levels) to guard against pathological inputs. Add new patterns topackages/security/pii.tsrather than re-implementing locally — there should be one source of truth.
Audit Trail
The audit_log table captures all administrative and sensitive actions, including:
- User role changes.
- Incident escalations.
- Form definition updates.
- Security configuration changes.
logAudit() runs inside the same database transaction as the mutation it describes, so a crash between the write and the audit call cannot leave an action without a trail.
Coupon capabilities and Contact evidence
Coupon assignment links, private issuance URLs, resume cookies, and legacy staff proof have different authority. Private entry tokens are exchanged for path-scoped HttpOnly sessions; recovery IDs identify an original attempt and are not permission to redeem or reveal a token. A private issuance token is revealed once and is never returned by the history list or replay.
Ordinary Coupon reports mask Contact values for every role. Association repair, Contact-profile reveal, and raw Coupon export have their own server authorization, operational purpose, and audit; one does not grant the others. Merges preserve append-only consent and effective revocations. Anonymous NPS stores no Contact or contact/marketing consent.
Keep private links, cookies, proof fragments, and guest personal data out of logs, analytics, support tickets, screenshots, video, and traces. Legacy proof navigation uses a fragment consumed before telemetry and held only in document memory; it must be rescanned after refresh/full authentication navigation. Use the feature's capability-aware telemetry controls and the separate real-device/privacy acceptance checks; successful synthetic browser tests do not certify those operational checks.
See Coupons and Guest NPS, Contacts, and the rollout runbook.