Authentication¶
!!! info "TL;DR" Canviq uses magic link login with custom HMAC-signed session cookies. No passwords, no OAuth. Sessions are AES-256-GCM encrypted and stored in httpOnly cookies with 8-hour expiry.
Authentication Flow¶
Canviq uses a single authentication method: magic link login via email.
User enters email → Magic link sent via Resend → User clicks link →
Session cookie created → Redirected to dashboard
There are no passwords, no OAuth providers, and no Supabase Auth. The system uses a custom public.users table with HMAC-signed session cookies.
Step 1: Request Magic Link¶
Navigate to /auth/login and enter your email address:
A magic link is sent to the email address via Resend. The link contains a one-time token stored in the auth_magic_links table.
Step 2: Confirm Login¶
Click the magic link in your email, which redirects to /auth/confirm?token=.... The server verifies the token, creates a session, and redirects to the dashboard.
Step 3: Session Created¶
On successful verification, the server:
- Finds or creates the user in the
userstable - Looks up the user's organization membership in
organization_members - Creates an encrypted + signed session cookie
Session Management¶
Session Cookie¶
Canviq uses a single cookie named feedback_session:
| Property | Value |
|---|---|
| Name | feedback_session |
| Encryption | AES-256-GCM (IV + authTag + ciphertext) |
| Signing | HMAC-SHA256 (constant-time comparison) |
| httpOnly | Yes (prevents JavaScript access) |
| secure | Yes in production (HTTPS only) |
| sameSite | lax (CSRF protection) |
| maxAge | 8 hours |
| Rotation | Auto-rotates at 4 hours (50% threshold) |
Session Data¶
The encrypted cookie payload contains:
interface SessionData {
userId: string // UUID from public.users
email: string
organizationId: string // UUID from organizations
organizationRole: 'owner' | 'admin' | 'member'
expiresAt: number // Unix timestamp
}
Session Revocation¶
Sessions can be revoked via a Redis deny-list:
- Revocation adds the user ID to an Upstash Redis key with an 8-hour TTL
- The
auth()function checks the deny-list on every request - If Redis is unavailable, sessions are allowed (fail-open)
- Admin can revoke sessions via
POST /api/admin/sessions/revoke
Signing Out¶
Deletes the feedback_session cookie.
Multi-Tenancy¶
Authentication is tightly coupled with organization membership:
- Middleware extracts the organization from the subdomain (e.g.,
acme.canviq.app→ org slugacme) - Session includes
organizationIdandorganizationRole - RLS policies use
current_user_id()andcurrent_organization_id()(set via request headers from the server client)
Users without an organization membership cannot create a session.
Authorization Model¶
Roles¶
| Role | Description |
|---|---|
owner | Full control including billing |
admin | Manage submissions, surveys, team |
member | Submit, vote, comment |
RLS Functions¶
All row-level security policies use these PostgreSQL functions:
| Function | Returns | Purpose |
|---|---|---|
current_user_id() | UUID | Current user from request headers |
current_organization_id() | UUID | Current org from request headers |
is_org_member() | boolean | Is the user a member of the current org |
is_org_admin() | boolean | Is the user an admin or owner |
is_team_member(user_id) | boolean | Legacy: checks team_members table |
Security Notes¶
- No passwords stored — Magic link only, no password hashing
- No OAuth — Google and GitHub OAuth are disabled (
supabase/config.toml) - Session secret —
SESSION_SECRETenv var required (min 32 chars) - Encryption — AES-256-GCM with random IV per encryption
- Signing — HMAC-SHA256 with constant-time comparison to prevent timing attacks
- Revocation — Redis-backed deny-list with automatic TTL expiry
Next Steps¶
- Quickstart — Get started with the platform
- API Reference — REST API endpoints
- Database Schema — RLS policies in detail