Skip to main content

01 — Auth & Multi-Tenant Architecture

Ring: 1 (Launch Blocker) Dependency: None — everything else depends on this Handbook: Ch. 39 (multi-tenant model), Ch. 48 (security), Ch. 141 (organization_users), Ch. 166 (RLS)

Problem

  • No login exists. We don’t know who the user is.
  • ORGANIZATION_ID is hardcoded everywhere.
  • RLS policies are temporary (temp_dev_anon_access — hardcoded to DOSE org_id).
  • Billing, credit, multi-user — all impossible without auth.
  • System behaves like an “internal tool” but it is now a SaaS product.

Decisions

D1: Login Methods

  • Email/password
  • Google OAuth
  • Apple OAuth

D2: Role System — 2 Layers

Platform Roles (internal, not visible to customers):
RoleDescription
super_adminFounder. View all orgs/data, manage platform. JWT raw_app_meta_data.platform_role
Org Roles (per org, customer-facing):
RolePermissions
ownerEverything + billing + delete org + ownership transfer. 1 person per org.
adminInvite/remove members, settings, all features. Cannot delete org.
memberDiscovery, headhunt, lead management — all features within plan limits.
viewerRead-only — view companies/leads, no execution permissions.
The same person can hold different roles in different orgs.

D3: Plan = Org Level

Plan → belongs to the ORG (not the user)
Credit → the ORG's wallet (not the user's)
Billing → charged to the ORG
A user can belong to 2 orgs: one Free, one Pro.

D4: Multi-Org Rules

PlanOrg CREATIONBeing INVITED to an Org
Free1Unlimited
Pro1Unlimited
Team1Unlimited
EnterpriseUnlimitedUnlimited

D5: Signup & Invitation Flow

Signup (email/password or OAuth)
  → Account is created (auth.users)
  → Redirected to "Create Org" page
  → Enters org name + industry info
  → Automatically becomes "owner"
  → Onboarding begins

Invitation:
  Owner/Admin sends email invitation
  → Invitee signs up (if needed) or logs in
  → Automatically linked to org as "member"
  → Role can be changed later

D6: DOSE Migration Scenario

1. Enable Supabase Auth (email/password + Google + Apple)
2. Founder signs up → user_id is created
3. Existing DOSE org (cd3c0336...) → link with founder's user_id as "owner"
4. Set raw_app_meta_data.platform_role = 'super_admin'
5. Replace hardcoded ORGANIZATION_ID → read from JWT claims
6. DELETE all temp_dev_anon_access RLS policies
7. Write JWT-based RLS policies
8. Existing 1591 companies + 1522 contacts remain unchanged

Data Model

Current (will change)

export_ai_organizations  → Exists but profiles table is not linked to auth.users
export_ai_profiles       → Will be removed or merged with auth.users

Target

-- Supabase Auth built-in (do not touch)
auth.users
  id UUID PK
  email TEXT
  raw_app_meta_data JSONB  -- \{ platform_role?: 'super_admin' \}
  raw_user_meta_data JSONB -- \{ full_name, avatar_url \}

-- Existing table to be updated
export_ai_organizations
  id UUID PK (existing: cd3c0336...)
  name TEXT NOT NULL
  slug TEXT UNIQUE          -- URL-friendly (new)
  plan TEXT DEFAULT 'free'  -- 'free' | 'pro' | 'team' | 'enterprise' (new)
  stripe_customer_id TEXT   -- nullable, empty during beta (new)
  created_by UUID → auth.users (new)
  settings JSONB DEFAULT '\{\}' -- org-specific config (new)
  created_at TIMESTAMPTZ
  updated_at TIMESTAMPTZ

-- New table (replaces export_ai_profiles)
export_ai_organization_members
  id UUID PK DEFAULT gen_random_uuid()
  user_id UUID NOT NULLauth.users
  organization_id UUID NOT NULL → export_ai_organizations
  role TEXT NOT NULL DEFAULT 'member'  -- CHECK: owner/admin/member/viewer
  status TEXT NOT NULL DEFAULT 'active' -- CHECK: invited/active/suspended
  invited_by UUID → auth.users
  invited_at TIMESTAMPTZ
  joined_at TIMESTAMPTZ DEFAULT now()
  UNIQUE(user_id, organization_id)

RLS Policies (JWT-based)

-- Example: companies table
CREATE POLICY "org_member_access" ON export_ai_companies
  FOR ALL
  USING (
    organization_id IN (
      SELECT organization_id FROM export_ai_organization_members
      WHERE user_id = auth.uid()
      AND status = 'active'
    )
  );

-- Super admin: view all data
CREATE POLICY "super_admin_access" ON export_ai_companies
  FOR ALL
  USING (
    (auth.jwt() -> 'app_metadata' ->> 'platform_role') = 'super_admin'
  );

Session & Context

// lib/auth/session.ts
interface ISessionContext {
  userId: string;
  activeOrgId: string;
  orgRole: 'owner' | 'admin' | 'member' | 'viewer';
  platformRole?: 'super_admin';
  orgPlan: 'free' | 'pro' | 'team' | 'enterprise';
}

// In every API route:
const session = await getSession(request);
// session.activeOrgId → used instead of ORGANIZATION_ID
// session.orgRole → permission checks
// session.orgPlan → feature/limit checks

Current Code Impact

To Be Removed / Changed

FileChange
lib/constants.tsORGANIZATION_IDRemove → session.activeOrgId
lib/supabase.tsAnon client → Auth client (with session token)
All API routes (15+)ORGANIZATION_ID → from session
All client-side hooksAuth context + org context to be added
export_ai_profiles tableRemove or deprecate
18 table RLS policiestemp_dev_anon_access → JWT-based
validateApiKey.tsStays for external (batch script) use, JWT for UI

New Files

FileContent
lib/auth/session.tsgetSession(), requireAuth(), requireRole()
lib/auth/context.tsxReact AuthProvider + OrgProvider
app/login/page.tsxLogin UI (email + Google + Apple)
app/signup/page.tsxSignup UI
app/org/new/page.tsxOrg creation
app/org/settings/page.tsxOrg settings + member management
middleware.tsAuth guard (protected routes)

Future Decisions (not now, but not forgotten)

FD-1: Shared Companies (Ring 2)

Companies will become org-independent global data. export_ai_companies.organization_id will be removed, replaced by an export_ai_org_companies junction table. This is the SaaS data moat strategy — as the platform grows, AI cost per query decreases. Why not now: Large migration, all queries change. After auth is stable.

FD-2: Platform Support Role (Ring 3+)

A platform_support role will be added for the support team. Can view user data but cannot modify it. For debugging + customer support. Why not now: Solo team, not needed yet.

FD-3: Enterprise Feature-Based Roles (Ring 3+)

In the Enterprise plan, roles will not just be owner/admin/member/viewer but can carry feature-based permissions. Example: can_run_batch, can_export_data, can_manage_billing, can_run_discovery. This way an Enterprise customer can fine-tune like “sales team can run discovery but not batch operations.” Requires a custom role builder UI. Why not now: 4 fixed roles are sufficient at launch. Needs will clarify when Enterprise customers arrive.

FD-4: Org Switching UI (Ring 2)

Org switcher in the sidebar for multi-org users. Switching the active org updates JWT claims. Why not now: Everyone has a single org initially. Needed when Enterprise plan launches.

Atomic Tasks (will become TODO items)

#TaskEstimated Size
AUTH-1Enable Supabase Auth (email + Google + Apple providers)Small
AUTH-2Update export_ai_organizations table (plan, slug, stripe_customer_id, created_by, settings)Migration
AUTH-3Create export_ai_organization_members table (role, status, invited_by)Migration
AUTH-4export_ai_profiles → deprecate/removeMigration
AUTH-5lib/auth/session.ts — getSession, requireAuth, requireRoleMedium
AUTH-6lib/auth/context.tsx — React AuthProvider + OrgProvider + useAuth hookMedium
AUTH-7Login + Signup + Org creation pagesMedium
AUTH-8middleware.ts — auth guard (protected routes)Small
AUTH-9Replace ORGANIZATION_IDsession.activeOrgId in all API routesLarge (15+ files)
AUTH-10Auth context integration in all client hooksLarge
AUTH-11Rewrite 18 table RLS policies → JWT-based + delete temp policiesLarge
AUTH-12Create DOSE founder user + owner + super_admin setSmall
AUTH-13Remove validateApiKey.ts dev bypass (SEC-F)Small
AUTH-14Org settings + member invite/management pageMedium