Documentation Index
Fetch the complete documentation index at: https://cernio.gadulabs.com/llms.txt
Use this file to discover all available pages before exploring further.
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):
| Role | Description |
|---|
super_admin | Founder. View all orgs/data, manage platform. JWT raw_app_meta_data.platform_role |
Org Roles (per org, customer-facing):
| Role | Permissions |
|---|
owner | Everything + billing + delete org + ownership transfer. 1 person per org. |
admin | Invite/remove members, settings, all features. Cannot delete org. |
member | Discovery, headhunt, lead management — all features within plan limits. |
viewer | Read-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
| Plan | Org CREATION | Being INVITED to an Org |
|---|
| Free | 1 | Unlimited |
| Pro | 1 | Unlimited |
| Team | 1 | Unlimited |
| Enterprise | Unlimited | Unlimited |
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 NULL → auth.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
| File | Change |
|---|
lib/constants.ts → ORGANIZATION_ID | Remove → session.activeOrgId |
lib/supabase.ts | Anon client → Auth client (with session token) |
| All API routes (15+) | ORGANIZATION_ID → from session |
| All client-side hooks | Auth context + org context to be added |
export_ai_profiles table | Remove or deprecate |
| 18 table RLS policies | temp_dev_anon_access → JWT-based |
validateApiKey.ts | Stays for external (batch script) use, JWT for UI |
New Files
| File | Content |
|---|
lib/auth/session.ts | getSession(), requireAuth(), requireRole() |
lib/auth/context.tsx | React AuthProvider + OrgProvider |
app/login/page.tsx | Login UI (email + Google + Apple) |
app/signup/page.tsx | Signup UI |
app/org/new/page.tsx | Org creation |
app/org/settings/page.tsx | Org settings + member management |
middleware.ts | Auth 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.
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)
| # | Task | Estimated Size |
|---|
| AUTH-1 | Enable Supabase Auth (email + Google + Apple providers) | Small |
| AUTH-2 | Update export_ai_organizations table (plan, slug, stripe_customer_id, created_by, settings) | Migration |
| AUTH-3 | Create export_ai_organization_members table (role, status, invited_by) | Migration |
| AUTH-4 | export_ai_profiles → deprecate/remove | Migration |
| AUTH-5 | lib/auth/session.ts — getSession, requireAuth, requireRole | Medium |
| AUTH-6 | lib/auth/context.tsx — React AuthProvider + OrgProvider + useAuth hook | Medium |
| AUTH-7 | Login + Signup + Org creation pages | Medium |
| AUTH-8 | middleware.ts — auth guard (protected routes) | Small |
| AUTH-9 | Replace ORGANIZATION_ID → session.activeOrgId in all API routes | Large (15+ files) |
| AUTH-10 | Auth context integration in all client hooks | Large |
| AUTH-11 | Rewrite 18 table RLS policies → JWT-based + delete temp policies | Large |
| AUTH-12 | Create DOSE founder user + owner + super_admin set | Small |
| AUTH-13 | Remove validateApiKey.ts dev bypass (SEC-F) | Small |
| AUTH-14 | Org settings + member invite/management page | Medium |