1 OAK MLS

Security Architecture

Authentication, authorization, and security patterns for the 1 OAK MLS Platform

Security Architecture

Consolidated reference for authentication, authorization, and security patterns.

Authentication

The platform uses Supabase Auth with magic link as the primary authentication method:

  1. User enters email on login page
  2. Supabase sends a one-time magic link to that email
  3. User clicks the link, which redirects to /api/auth/callback
  4. Callback exchanges the code for a session cookie
  5. 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 createServerClient from @supabase/ssr to read sessions
  • Client components use createBrowserClient for client-side session access
  • Sessions auto-refresh when nearing expiry

Authorization

Role Hierarchy

member < agent < admin < owner

Each 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:

HelperPurpose
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 of
  • workspace_members — Members see their own workspace's members
  • user_profiles — Users see profiles in their workspaces
  • favorites — Users see only their own favorites
  • saved_searches — Users see only their own searches

Internal tables (RLS enabled, no policies — service role only):

  • mls_connections
  • field_mappings
  • listing_media
  • compliance_profiles
  • sync_runs
  • workspace_domains
  • import_tasks
  • ai_usage
  • ai_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_KEY environment 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):

TierLimitUsed For
auth5 req/minLogin, invitations
api60 req/minGeneral API
search30 req/minSearch routes
public120 req/minPublic listing data

Rate limit headers are returned on every response:

  • X-RateLimit-Limit — Maximum requests allowed
  • X-RateLimit-Remaining — Requests remaining in window
  • X-RateLimit-Reset — Unix timestamp when limit resets

Multi-Tenant Isolation

Every data table includes a workspace_id column. This is the foundational isolation mechanism:

  1. Database level: All queries include workspace_id in WHERE clauses
  2. API level: requireWorkspaceAccess() ensures users can only access their own workspaces
  3. Search level: Typesense collections are per-workspace (collection name = workspace slug)
  4. 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() or requireAuth()
  • New tables include workspace_id column
  • 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

On this page