Caugia security posture
This document describes the security model for GRIP OS (Caugia) as of 2026-04-24. It is intended for investors, prospective design partners, and any security reviewer who wants to understand our auth, data isolation, secret handling, backup, and incident response posture before we formalise SOC 2 attestation in Q3 2026.
For security disclosure, email security@caugia.com. For general product questions, tom@caugia.com.
Threat model
We take the following threat categories seriously and design against each.
- Account takeover. A malicious actor obtains a user's session cookie or Bearer token and gains access to workspaces the user owns or is a member of.
- Data exfiltration. A malicious actor with partial access attempts to read rows belonging to a workspace they do not own or co-own. This includes cross-tenant leaks via a shared database, shared cache, or shared background job queue.
- LLM prompt injection. A malicious actor places adversarial content inside a workspace (via a comment, a connector-ingested blob, or a file upload) that Sophie, the AI advisor, later reads and executes as an instruction. The goal of the attacker is to trick Sophie into disclosing data, taking unauthorised actions, or contaminating recommendations.
- Supply-chain compromise. A dependency in the Node.js, Python, or infrastructure toolchain is compromised and ships malicious code that reaches production.
Lower-severity categories we monitor but do not explicitly design against in this document: denial of service, social engineering of the founder, and physical access attacks.
Authentication
Caugia uses Supabase Auth as the source of truth for user identity. Every authenticated API call resolves to a concrete user row in auth.users.
Two session strategies are supported and checked in src/lib/auth/api-guard.ts:
- SSR cookie session. Used by the Next.js app. Supabase SSR cookies are HttpOnly, SameSite=Lax, and bound to the
os.caugia.comdomain. Cookie rotation is handled by the Supabase client on every refresh. - Bearer access token. Used by automated clients and any future public API callers. Tokens are validated against Supabase and the resolved user id is used for every downstream authorisation check.
Three auth guards enforce access in API routes:
requireUserAuth(request): any logged-in user (cookie or Bearer).requireWorkspaceAccess(request, id): logged-in user with a row inworkspace_membersfor that workspace. Used for writes and any endpoint that returns private owner fields.requireWorkspaceReadAccess(request, id): allows unauthenticated read access whenworkspaces.is_public_demo = true. Private owner fields are stripped from the response when the caller is the public-demo pseudo-user.
Service-to-service calls (webhooks, cron, Make.com) use requireServiceAuth(request) with either SUPABASE_SERVICE_ROLE_KEY or CRON_SECRET as a Bearer token.
No fallback to open access. If the guard cannot verify the caller, the request returns 401.
Data isolation
Caugia is multi-tenant. Every workspace is isolated at the database layer.
- The
workspace_memberstable is the single source of truth for who can access a workspace. Row Level Security (RLS) policies referenceworkspace_memberson every tenant-scoped table. - Every query path filters by workspace id. The API guards resolve the user first, then the workspace membership, then pass a concrete
workspace_idfilter down to the Supabase client. - Public demo workspaces are marked with
is_public_demo = true. The workspace GET endpoint strips owner-only fields (stripe_customer_id,brief_recipients,slack_webhook_url,metadata, and the rest of the list insrc/app/api/workspace/[id]/route.ts) before returning the row to an unauthenticated public-demo caller. - No cross-workspace query path exists in the application code. Admin endpoints (under
/api/admin) are gated by the service role key and are not reachable from the browser.
Secret handling
- Secrets are stored exclusively as Vercel environment variables in the production project. Two projects exist, one for preview and one for production, with separate secrets.
.env.localis in.gitignoreand is never committed. A.env.examplefile documents the required variable names without values.- No secret is ever logged. The logger layer strips Authorization headers, cookie headers, and any key named
_SECRET,_TOKEN,*_KEY, orpasswordbefore writing to stdout. - LLM provider keys (Anthropic) are loaded on the server only and are never shipped to the client. Sophie chat calls go through an internal API route that validates the user session before proxying to Anthropic.
- Stripe webhook signatures are validated on every inbound webhook using
STRIPE_WEBHOOK_SECRET.
Backups
- Supabase Postgres is configured with managed daily backups and point-in-time recovery (PITR) on the production project.
- Retention window: 7 days of PITR plus 30 days of daily snapshots on the current paid tier. Retention will be increased as we move into SOC 2 audit scope.
- Restore procedure is documented in the internal runbook. Time-to-first-byte target for a full restore is under four hours.
- No customer data is stored outside Supabase. Vercel functions are stateless.
Incident response
- Primary security contact: Tom Meijer, tom@caugia.com. Security-specific mailbox: security@caugia.com.
- Severity 1 (active exploitation, data breach in progress, production down): 24-hour SLA for initial response, communicated directly to affected workspaces.
- Severity 2 (data integrity risk, suspected intrusion, significant feature outage): 72-hour SLA for initial response.
- Severity 3 (hardening request, low-risk report): 7-day SLA.
- All incidents are logged, triaged, and close out with a written post-mortem shared with any affected workspace owner.
Disclosure
We welcome responsible disclosure. Report any suspected vulnerability, data exposure, or authentication bypass to security@caugia.com. We do not currently run a bug bounty programme, but we commit to acknowledging every valid report within seven days and crediting the reporter in our changelog if they wish to be named.
Please do not attempt to access data that does not belong to you, run destructive testing on production data, or disclose publicly before we have had a reasonable window to remediate.
