Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/arrozet/caret/llms.txt

Use this file to discover all available pages before exploring further.

Caret delegates identity entirely to Supabase Auth. There is no custom password hashing, no home-grown session store, and no separate user table for authentication — auth.users inside Supabase is the single source of truth for every account. Once a user signs in, Supabase issues a signed JWT that every backend service and the collab-service validates independently — using Supabase’s JWKS endpoint — before processing any request.

Identity: Supabase Auth

All user accounts live in auth.users, which is managed exclusively by Supabase. The application extends this with a user_profiles table in the public schema that stores display preferences (display name, avatar URL, locale). Because user_profiles is separate from auth.users, re-authenticating through a provider like Google OAuth does not overwrite the user’s customized profile.
auth.users (Supabase managed)
  └── user_profiles (public schema, PK = auth.users.id)
        stores: display_name, avatar_url, locale

Frontend Auth Flow

The frontend uses the Supabase JS client for all auth operations. Auth state is held in a Zustand store (authStore.ts) and exposed to the rest of the application through React hooks and context.
1

Sign in

The user signs in via the auth modal (email/password or OAuth provider). Supabase JS calls Supabase Auth and receives an access token (JWT) and a refresh token.
2

Session storage

Supabase JS persists the session to localStorage. The Zustand authStore subscribes to supabase.auth.onAuthStateChange and updates its state whenever the session changes (sign in, sign out, token refresh).
3

Profile upsert

After successful sign-in, authStore reads and upserts user_profiles via the Supabase JS anon client using the authenticated session. This is the only direct Supabase JS database call the frontend makes.
4

API requests

For all other data operations, the frontend calls the REST API through src/lib/apiClient.ts, which reads the current session’s access token and attaches it as Authorization: Bearer <token> on every request.
5

Token refresh

Supabase JS automatically refreshes the access token before it expires using the refresh token. authStore receives the new session via onAuthStateChange and apiClient picks it up on the next request.

JWT Validation Per Service

Supabase issues asymmetric JWTs (ES256). Every backend service validates incoming tokens independently — there is no centralized auth proxy in the API Gateway. Each service fetches Supabase’s public keys from SUPABASE_URL/auth/v1/.well-known/jwks.json and caches them locally (5-minute TTL) to avoid fetching on every request.
ServiceValidation method
api-gatewayNo JWT validation — applies CORS and rate limiting only; validation is the responsibility of each downstream service
auth-serviceValidates JWT via JWKS (ES256); used for auth-scoped endpoints
document-serviceValidates JWT via JWKS (ES256); extracts user_id from sub claim for repository queries
collab-serviceValidates JWT on WebSocket handshake via JWKS (ES256/RS256); also accepts HS256 tokens using SUPABASE_JWT_SECRET as a fallback (see below)
ai-serviceValidates JWT via JWKS (ES256/RS256) using a FastAPI Depends() dependency; extracts user_id for conversation scoping
Required environment variables for JWT validation across services:
VariableServices that use itPurpose
SUPABASE_URLAll backend servicesSupabase project URL; used to fetch the JWKS endpoint (/auth/v1/.well-known/jwks.json)
SUPABASE_ANON_KEYAll backend servicesSent as apikey header when fetching the JWKS from Supabase GoTrue
SUPABASE_SERVICE_ROLE_KEYauth-serviceAdmin DB key for operations that must bypass RLS
SUPABASE_JWT_SECRETcollab-service onlyHS256 symmetric fallback; used when the JWT header declares alg: HS256 instead of ES256
JWT_SECRETauth-service, document-serviceAdditional secret for internal service-level token validation
Never commit any of these secrets to the repository. Store them as environment variables in .env files for local development (which must be listed in .gitignore) and as Coolify secrets for production deployments. Leaking a SUPABASE_SERVICE_ROLE_KEY bypasses all RLS policies and gives full read/write access to the entire database.

Collaboration Auth

The collab-service is not behind the API Gateway, so it performs its own JWT verification. The frontend passes the Supabase access token as a query parameter on the WebSocket URL:
ws://localhost:3003/document/{doc_id}?token={jwt}
wss://ws.caret.page/document/{doc_id}?token={jwt}
On WebSocket handshake, the collab-service:
  1. Extracts the token query parameter from the upgrade request.
  2. Inspects the JWT header to determine the signing algorithm.
  3. For ES256/RS256 tokens, validates the signature against the Supabase JWKS. For HS256 tokens, validates using SUPABASE_JWT_SECRET.
  4. Rejects the connection with a 401 close frame if the token is missing, malformed, or expired.
  5. Allows the connection and records the authenticated user_id for awareness and audit purposes.
JWTs have limited lifetimes. Long-lived collaboration sessions may need to reconnect after token expiry. The frontend useCollaborationSession hook is responsible for managing reconnection when Supabase JS issues a refreshed token.

Protected Routes

On the frontend, authenticated routes are wrapped with the AuthGuard component. If the user has no valid session, AuthGuard redirects them to /login before rendering the protected view.
RouteProtection
/documentsAuthGuard required
/documents/:idAuthGuard required
/settingsAuthGuard required
/Public (landing page)
/loginPublic (landing page with auth modal)
/debug/collab-harnessDevelopment only

Row-Level Security

Even when a request passes JWT validation at the service layer, PostgreSQL RLS provides a second enforcement boundary at the database level. RLS policies are defined in SQL migration files and evaluate on every query. How backend services interact with RLS:

Service-role credentials

Backend services typically connect with the SUPABASE_SERVICE_ROLE_KEY, which bypasses RLS. This is appropriate for admin operations (creating workspaces, generating version snapshots, writing Y.js updates) that the service itself initiates on behalf of the user.

User-scoped JWT

When a service needs to enforce RLS for user-scoped queries (e.g. validating workspace membership before returning data), it can pass the user’s JWT to Supabase and let RLS policies filter the result set automatically.
Frontend RLS: The frontend uses the Supabase JS anon client with the authenticated session to read and upsert user_profiles. RLS on user_profiles permits each authenticated user to read and write only their own row. All other application tables are inaccessible to the frontend anon client. Policy source files:
Table groupPolicy file
Core document tablesdocument-service/src/db/migrations/001_rls_core_tables.sql
Collaboration tablescollab-service/src/db/migrations/001_rls_collab_tables.sql
AI tablesai-service/src/db/migrations/versions/0003_enable_rls_on_public_tables.py
Always test RLS policy changes against a dedicated Supabase test project before applying them to production.

Build docs developers (and LLMs) love