Security Architecture
Authentication, authorization, and security patterns for the 1 OAK MLS Platform
Security Architecture
Consolidated reference for authentication, authorization, and security patterns.
Authentication
Magic Link (Primary)
The platform uses Supabase Auth with magic link as the primary authentication method:
- User enters email on login page
- Supabase sends a one-time magic link to that email
- User clicks the link, which redirects to
/api/auth/callback - Callback exchanges the code for a session cookie
- Session is stored as an HTTP-only cookie managed by Supabase
No passwords are stored or managed by the application.
Session Management
- Sessions are stored as HTTP-only cookies by the Supabase Auth client
- Server components use
createServerClientfrom@supabase/ssrto read sessions - Client components use
createBrowserClientfor client-side session access - Sessions auto-refresh when nearing expiry
Authorization
Role Hierarchy
member < agent < admin < ownerEach higher role inherits all permissions of the roles below it. Roles control dashboard navigation visibility, API endpoint access, and member management capabilities.
For the complete permissions matrix, member management rules, and platform admin details, see the dedicated Roles & Permissions reference.
API Route Auth Pattern
All admin API routes are protected using helpers from apps/admin/src/lib/auth/api-auth.ts:
import { requireWorkspaceAccess, isAuthError } from "@/lib/auth/api-auth"
export async function GET(request: NextRequest, { params }) {
const { id: workspaceId } = await params
const auth = await requireWorkspaceAccess(workspaceId, "member")
if (isAuthError(auth)) return auth
// auth.user, auth.role, auth.memberId available
const supabase = createSupabaseAdminClient()
// ... use service role client for data access
}Available helpers:
| Helper | Purpose |
|---|---|
requireAuth() | Verify user is logged in |
requireWorkspaceAccess(id, role) | Verify user has workspace membership with minimum role |
requirePlatformAdmin(role) | Verify user is a platform administrator |
isAuthError(result) | Type guard to check if result is an error response |
Platform Admin
A separate platform_admins table tracks users with global platform access. Platform admins can access any workspace regardless of membership.
Row Level Security (RLS)
Strategy
The platform uses a two-tier RLS approach:
User-facing tables (RLS with policies):
workspaces— Users see workspaces they're members ofworkspace_members— Members see their own workspace's membersuser_profiles— Users see profiles in their workspacesfavorites— Users see only their own favoritessaved_searches— Users see only their own searches
Internal tables (RLS enabled, no policies — service role only):
mls_connectionsfield_mappingslisting_mediacompliance_profilessync_runsworkspace_domainsimport_tasksai_usageai_content_audit
The "no policies" pattern means only the service_role key can access these tables. All API routes use the service role client after validating the user's session and workspace membership.
Token Encryption
MLS API credentials (Bridge access tokens) are encrypted at rest using AES-256-GCM:
Plaintext token → AES-256-GCM encrypt → iv:authTag:ciphertext → stored in mls_connections.token_ciphertext- Encryption key:
TOKEN_ENCRYPTION_KEYenvironment variable (64-char hex string = 32 bytes) - Algorithm: AES-256-GCM (authenticated encryption)
- Each token gets a unique random IV
- Tokens are decrypted only at sync time, never exposed in API responses
Rate Limiting
Public-facing routes use Upstash Redis for rate limiting (see packages/shared/src/ratelimit/index.ts):
| Tier | Limit | Used For |
|---|---|---|
auth | 5 req/min | Login, invitations |
api | 60 req/min | General API |
search | 30 req/min | Search routes |
public | 120 req/min | Public listing data |
Rate limit headers are returned on every response:
X-RateLimit-Limit— Maximum requests allowedX-RateLimit-Remaining— Requests remaining in windowX-RateLimit-Reset— Unix timestamp when limit resets
Multi-Tenant Isolation
Every data table includes a workspace_id column. This is the foundational isolation mechanism:
- Database level: All queries include
workspace_idin WHERE clauses - API level:
requireWorkspaceAccess()ensures users can only access their own workspaces - Search level: Typesense collections are per-workspace (collection name = workspace slug)
- Storage level: Supabase Storage paths are prefixed with workspace ID
Workspace ID Enforcement
- Every INSERT must include
workspace_id - Every SELECT filters by
workspace_id - No cross-workspace queries exist in the application code
- RLS policies on user-facing tables enforce workspace boundaries
Cron Authentication
Scheduled sync endpoints use a shared secret:
Authorization: Bearer {CRON_SECRET}The CRON_SECRET is validated in the cron route handler before any sync operations run.
Security Checklist for New Features
When building new features, verify:
- API routes use
requireWorkspaceAccess()orrequireAuth() - New tables include
workspace_idcolumn - RLS is enabled on new tables (with policies or service-role-only)
- Sensitive data is encrypted (tokens, credentials)
- User input is validated with Zod
- No secrets in API responses (tokens, encryption keys)
- Rate limiting on public endpoints
Related Documentation
- API Reference — All API endpoints and auth requirements
- Database Schema — Table definitions and RLS configuration
- Architecture Decisions — ADR-004: Multi-tenant first