My App
Database

Database Schema

Database structure, tables, and relationships

Database Overview

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

Key Features

Multi-Tenant Architecture

  • Row-level tenant isolation via tenant_id
  • Secure data separation between organizations
  • RLS enforces tenant and provider isolation

Vector Embeddings

  • 768-dimensional vectors (Gemini text-embedding-004)
  • HNSW indexes for cosine similarity on user_embeddings, tags, embeddings
  • Powers For You feed recommendations

Partitioned Tables

  • activities partitioned by quarter (2026 Q1–2027 Q1)
  • Improved query performance and easier archival

ENUMs

TypeValues
entity_statuspending, active, suspended, archived
signup_statuspending, confirmed, cancelled, attended, no_show
content_statusdraft, published, archived
booking_statuspending, confirmed, cancelled, completed

Core Entity Groups

1. Tenant & Core Users

TablePurpose
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).

2. Providers

TablePurpose
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.

3. Companies

TablePurpose
companiesBusiness + profile (stage, funding, challenges). profile_completeness_score auto-calculated.
company_staffCompany membership. role FK to roles.

4. Programmes & Courses

TablePurpose
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.

5. Events

TablePurpose
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.

6. Resources

TablePurpose
resourcesContent library. resource_type: link, article, video, pdf, template, tool, course. url or body.

7. Mentorship

TablePurpose
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).

8. Tags & Embeddings

TablePurpose
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.

9. Social

TablePurpose
notificationsNotification tray. type, title, body, action_url, source_type, source_id.
commentsThreaded comments. target_type currently resource. parent_comment_id, status.

10. Analytics & System

TablePurpose
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.

11. Geography

TablePurpose
postcodesUK postcodes. latitude, longitude, region, admin_district, constituencies.

Relationships (ER Summary)

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

Example Queries

Get Company's Enrolled Programmes

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;

Get Course Progress

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;

For You Pipeline

  1. Inputs: activities, user_preferences, companies (profile), user_embeddings
  2. Content: embeddings (content vectors), tag_links (filters)
  3. Flow: User embedding vs content embedding (cosine similarity) → filter by tags, tenant, status → rank and return

Working with the Database

Via Supabase Client

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");

Server-Side

  • getServiceDb() from server/lib/db.ts — service role client (bypasses RLS)
  • createClient() from lib/supabase/server.ts — user-scoped client for auth checks

Migrations

Schema source: supabase/migrations/20260210171318_init_platform.sql

Run migrations via Supabase CLI or your deployment pipeline.

On this page