Database
Database Schema Database structure, tables, and relationships
The platform uses PostgreSQL (via Supabase) with a multi-tenant architecture. Extensions: pgcrypto, vector (pgvector). All tables use plural names. Multi-tenant: tenant_id on business tables; RLS enforces isolation.
Source of truth: supabase/docs/schema_reference.md in the repository
Row-level tenant isolation via tenant_id
Secure data separation between organizations
RLS enforces tenant and provider isolation
768-dimensional vectors (Gemini text-embedding-004)
HNSW indexes for cosine similarity on user_embeddings, tags, embeddings
Powers For You feed recommendations
activities partitioned by quarter (2026 Q1–2027 Q1)
Improved query performance and easier archival
Type Values entity_statuspending, active, suspended, archived signup_statuspending, confirmed, cancelled, attended, no_show content_statusdraft, published, archived booking_statuspending, confirmed, cancelled, completed
Table Purpose tenantsCustomer of the platform. slug unique, settings JSONB. usersAuth-backed users. id = auth.users(id). tenant_usersUser ↔ tenant membership. role FK to roles. user_preferencesPreferences for recommendations (topics, learning style). user_embeddingsUser taste vectors (768-dim). UNIQUE(user_id, tenant_id). user_saved_itemsBookmarks. Polymorphic (item_type, item_id). rolesUnified roles: owner, admin, editor, member, viewer. user_rolesPlatform role assignments (platform team only).
Table Purpose providersContent publisher. tenant_id, UNIQUE(tenant_id, slug). is_platform_operator for main org. providers_staffProvider team. role FK to roles. Invitation flow via invited_at, accepted_at.
Table Purpose companiesBusiness + profile (stage, funding, challenges). profile_completeness_score auto-calculated. company_staffCompany membership. role FK to roles.
Table Purpose programmesProgramme definition. delivery_mode: cohort, self_paced, hybrid. cohortsTime-bound run. enrollment_opens_at, enrollment_closes_at, delivery_start_date, delivery_end_date. coursesCourse within programme. order_index, unlocks_at. course_pagesHierarchical pages. parent_page_id, path, required_completion_module_ids. course_modulesContent blocks. Types: text, video, image, self_reflection, quiz, checkbox, star_rating, resource_link, page_link, two_column, navigation, comments. programme_signupsCompany enrollment. UNIQUE(company_id, programme_id, cohort_id). Application: applied_at, reviewed_by, enrolled_at. course_signupsUser enrollment. progress_percent, started_at, completed_at. course_engagementsModule responses. UNIQUE(user_id, module_id). response JSONB, completed_at. placesVenues for events.
Table Purpose eventsEvent. Optional programme_id, cohort_id, place_id. event_type: workshop, webinar, networking, demo_day, office_hours, conference, other. event_signupsRegistration. attendance_mode, attended, attendance_confirmed_at.
Table Purpose resourcesContent library. resource_type: link, article, video, pdf, template, tool, course. url or body.
Table Purpose mentorsMentor profile. user_id UNIQUE. expertise_areas, industries, calendar_token_encrypted. mentor_programmesMentor ↔ programme. contract_reference, hourly_rate, rate_currency. mentor_availabilitiesSlots. day_of_week (0–6), starts_at_time, ends_at_time, specific_date for one-offs. mentor_creditsCredit ledger per company per programme. credits_granted, credits_used, credits_remaining generated. mentor_sessionsBooked session. external_calendar_id, credit_id, credits_consumed. mentor_session_signupsSession attendees. Role: mentee, observer. mentor_pairingsLong-term mentor–company relationship. UNIQUE(mentor_id, company_id, programme_id).
Table Purpose tagsTags with optional set. embedding vector(768). tag_linksPolymorphic links. target_type: programme, course, event, resource, mentor. embeddingsContent embeddings. content_type: programme, course, event, resource. input_hash for staleness.
Table Purpose notificationsNotification tray. type, title, body, action_url, source_type, source_id. commentsThreaded comments. target_type currently resource. parent_comment_id, status.
Table Purpose activitiesActivity log. Partitioned by quarter. target_type, action, metadata JSONB. provider_summariesDaily aggregates per provider. UNIQUE(provider_id, date). audit_logsChange audit. table_name, record_id, operation, old_values, new_values. impersonation_logsAdmin impersonation. action: start, stop. api_error_logsStructured API error diagnostics. request_id, error_code, error_key, origin_tag. event_outboxDurable event queue for async publishing. Retry tracking: attempts, next_attempt_at, published_at.
Table Purpose postcodesUK postcodes. latitude, longitude, region, admin_district, constituencies.
tenants
├── tenant_users → users
├── providers
│ ├── providers_staff → users
│ ├── programmes → cohorts, courses → course_pages → course_modules
│ ├── mentor_programmes → mentors
│ ├── events
│ └── resources
├── companies
│ ├── company_staff → users
│ ├── programme_signups → programmes, cohorts
│ ├── mentor_credits
│ └── mentor_pairings → mentors
└── places
users
├── user_preferences, user_embeddings, user_saved_items
├── course_signups, course_engagements, event_signups
├── mentor_session_signups
└── notifications
SELECT p. * , ps . status AS enrollment_status
FROM programmes p
JOIN programme_signups ps ON ps . programme_id = p . id
WHERE ps . company_id = 'company-uuid'
AND ps . status = 'active' ;
SELECT m. * , array_agg( t . name ) AS expertise_tags
FROM mentors m
JOIN tag_links tl ON tl . target_id = m . id AND tl . target_type = 'mentor'
JOIN tags t ON t . id = tl . tag_id
WHERE t . name IN ( 'fundraising' , 'product' )
GROUP BY m . id ;
SELECT c . title ,
COUNT ( DISTINCT cm . id ) AS total_modules,
COUNT ( DISTINCT ce . module_id ) AS completed_modules,
ROUND ( COUNT ( DISTINCT ce . module_id ):: numeric / COUNT ( DISTINCT cm . id ) * 100 ) AS progress_percentage
FROM courses c
JOIN course_pages cp ON cp . course_id = c . id
JOIN course_modules cm ON cm . page_id = cp . id
LEFT JOIN course_engagements ce ON ce . module_id = cm . id AND ce . user_id = 'user-uuid'
WHERE c . id = 'course-uuid'
GROUP BY c . id , c . title ;
Inputs : activities, user_preferences, companies (profile), user_embeddings
Content : embeddings (content vectors), tag_links (filters)
Flow : User embedding vs content embedding (cosine similarity) → filter by tags, tenant, status → rank and return
import { createClient } from "@supabase/supabase-js" ;
const supabase = createClient (
process.env. NEXT_PUBLIC_SUPABASE_URL ! ,
process.env. NEXT_PUBLIC_SUPABASE_ANON_KEY !
);
const { data , error } = await supabase
. from ( "programmes" )
. select ( "*, provider:providers(*), cohorts(*)" )
. eq ( "status" , "published" );
getServiceDb() from server/lib/db.ts — service role client (bypasses RLS)
createClient() from lib/supabase/server.ts — user-scoped client for auth checks
Schema source: supabase/migrations/20260210171318_init_platform.sql
Run migrations via Supabase CLI or your deployment pipeline.