Skip to content

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.

Navigate to /auth/login and enter your email address:

POST /api/auth/verify
Content-Type: application/json

{
  "email": "user@example.com"
}

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:

  1. Finds or creates the user in the users table
  2. Looks up the user's organization membership in organization_members
  3. Creates an encrypted + signed session cookie

Session Management

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

POST /api/auth/signout

Deletes the feedback_session cookie.

Multi-Tenancy

Authentication is tightly coupled with organization membership:

  1. Middleware extracts the organization from the subdomain (e.g., acme.canviq.app → org slug acme)
  2. Session includes organizationId and organizationRole
  3. RLS policies use current_user_id() and current_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 secretSESSION_SECRET env 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