Database Schema
Purpose
Defines the current StudyBoost public schema shape used by backend services and feature modules.
Core Notes
- Primary database: PostgreSQL (Neon)
- ORM: Prisma (
backend/prisma/schema.prisma) - Migration source of truth:
backend/prisma/migrations/* - Auth/session data currently lives in
public(no separateauthschema yet)
Auth Tables (Public Schema)
users
Core identity table used across documents, subscriptions, notifications, and auth.
Important columns:
user_id(UUID, PK)email(unique)username(unique)password(hashed)onboarding_draft(JSONB) - Stores volatile AI/Wizard onboarding state.onboarding_completed(Boolean) - Tracks final readiness status.getting_started_completed(Boolean, default false) - Whether the user has completed/dismissed the first-time getting-started guide (#281). Added by migration202606301932_add_getting_started_completed.university,major_focus,study_goal- Canonicalized profile fields.is_active,is_verified
Institutions Catalog (Public Schema)
institutions
A master catalog of 100k+ global universities and schools used for profile canonicalization.
Columns:
institution_id(UUID, PK)institution_name(unique)slug(unique, URL-friendly name)is_activeinstitution_city(VarChar 255, nullable) — full city nameinstitution_state(VarChar 255, nullable) — full state/region name (e.g.New York, notNY)institution_country(VarChar 255, nullable) — full country name (e.g.United States,Canada)
Indexes:
idx_institution_name_trgm: GIN Trigram index oninstitution_namefor high-performance fuzzy matching.idx_institution_slug: B-tree index for rapid URL lookups.- Location indexes for directory filtering (FEAT-58 / #279):
idx_institution_country,idx_institution_state,idx_institution_city,idx_institution_country_state,idx_institution_country_state_city,idx_institution_active_country(is_active, institution_country), andidx_institution_location_active(country, state, city, is_active).
Notes:
- Trigram index added via migration
202605030322_onboarding_overhaul. - Enabled by the
pg_trgmextension. institution_country/institution_statestore full names (verified against production); the #279 region filter maps region slugs → these full-name values server-side (backend/src/institutions/institution-regions.ts).
auth_sessions
Stores active refresh-session state per device/login.
Columns:
session_id(UUID, PK)user_id(FK ->users.user_id)refresh_token_hash(unique)user_agentip_addressexpires_atcreated_at
Indexes:
idx_auth_sessions_useridx_auth_sessions_expires_atuk_auth_sessions_refresh_token_hash
email_verification_tokens
Stores one-time email verification tokens (hashed).
Columns:
id(UUID, PK)user_id(FK ->users.user_id)token_hash(unique)expires_atcreated_atused_at
Indexes:
idx_email_verification_tokens_useridx_email_verification_tokens_expires_atidx_email_verification_tokens_used_atuk_email_verification_tokens_token_hash
Queue Infrastructure Tables (Public Schema)
background_jobs
Tracks lifecycle state for asynchronous queue jobs processed by FEAT-33 workers.
Columns:
id(UUID, PK)queue_name(ocr_queue,ai_processing_queue,indexing_queue,email_queue,notification_queue)job_type(logical task type)bull_job_id(BullMQ job identifier)status(pending|processing|completed|failed)payload_jsonresult_jsonerror_messageattempts_mademax_attemptsidempotency_key(unique when present)created_atstarted_atfinished_atupdated_at
Indexes:
idx_background_jobs_queue_nameidx_background_jobs_statusidx_background_jobs_created_atidx_background_jobs_queue_statusuk_background_jobs_idempotency_key(partial unique index for non-null keys)
Notes:
- Added via migration
202604272230_add_background_jobs_table. - Used by queue producers and worker processors for retry-safe status tracking.
Structured Notes Tables (Public Schema)
structured_notes
Stores user-owned note records for FEAT-59.
Columns:
id(UUID, PK)user_id(FK ->users.user_id)titlebodystatus(active|archived|deleted)deadline_atreminder_atarchived_atdeleted_atcreated_atupdated_at
Indexes:
idx_structured_notes_user_status_updatedidx_structured_notes_user_deadlineidx_structured_notes_user_created
structured_note_references
Stores typed note references to catalog entities.
Columns:
id(UUID, PK)note_id(FK ->structured_notes.id)reference_type(document|course|institution)document_id(nullable FK ->documents.document_id)course_id(nullable FK ->courses.course_id)institution_id(nullable FK ->institutions.institution_id)contextaccess_state_snapshotdeadline_atreminder_atposition_orderarchived_atcreated_atupdated_at
Indexes:
idx_structured_note_references_noteidx_structured_note_references_typeidx_structured_note_references_documentidx_structured_note_references_courseidx_structured_note_references_institution
structured_note_canvas_items
Stores persisted card coordinates/layout metadata per note.
Columns:
id(UUID, PK)note_id(FK ->structured_notes.id)x,y,width,heightgroup_labelcreated_atupdated_at
Indexes:
idx_structured_note_canvas_note
Notes:
- Added via migration
202605021540_add_structured_notes_tables. - Migration is additive and guard-safe (
IF NOT EXISTS).
Notes Canvas Tables — FEAT-59 v2 (Public Schema)
Milanote-style redesign of Structured Notes (Boards → Elements → Connectors).
Additive alongside the v1 structured_* tables, which remain during the
coexistence window (see docs/features/FEAT-59-Structured-Notes-Milanote-Redesign.md).
note_boards
Recursive canvas containers. Each user has one root and one inbox board (auto-provisioned); all other boards have kind = 'board' and may nest.
Columns:
id(UUID, PK)user_id(FK ->users.user_id, cascade)parent_board_id(nullable self-FK ->note_boards.id, cascade; NULL = top-level)titlekind(root|inbox|board)viewport_zoom(NUMERIC(4,2), 0.25–2.00),viewport_x,viewport_ystatus(active|archived|deleted)archived_at,deleted_at,created_at,updated_at
Indexes:
idx_note_boards_user_status_updatedidx_note_boards_user_parentuk_note_boards_user_kind(partial unique on(user_id, kind)wherekind IN ('root','inbox'))
note_elements
Generic, freely-placed cards (text, to-do, link, document/course/institution reference, column, board-link).
Columns:
id(UUID, PK)board_id(FK ->note_boards.id, cascade)user_id(FK ->users.user_id, cascade)element_type(text|todo|link|document|course|institution|column|board_link)parent_element_id(nullable self-FK ->note_elements.id, cascade; column membership)- geometry:
x,y,width,height,z_index,rotation - presentation:
color(palette token),title,body,content(JSONB) - reference targets:
document_id,course_id,institution_id,target_board_id(all nullable,ON DELETE SET NULL) access_state_snapshot,deadline_at,reminder_at,position_orderstatus,legacy_note_id,legacy_reference_id(backfill traceability), timestamps
Indexes: idx_note_elements_board_status, _user, _parent, _document, _course, _institution, _target_board; unique uk_note_elements_legacy_note, uk_note_elements_legacy_reference.
Notes:
chk_note_elements_targetenforces target exclusivity (not presence), so anON DELETE SET NULLon a hard-deleted catalog target preserves the card (rendersunavailable). Presence is enforced by the app on create.
note_connectors
Directed lines/arrows between two elements on the same board.
Columns:
id(UUID, PK)board_id(FK ->note_boards.id, cascade)user_id(FK ->users.user_id, cascade)source_element_id,target_element_id(FK ->note_elements.id, cascade; CHECK source ≠ target)style(arrow|line),label,color, timestamps
Indexes: idx_note_connectors_board, _source, _target.
Notes:
- Added via migration
202606151200_add_notes_boards_elements_connectors. - Additive and guard-safe (
IF NOT EXISTS); v1structured_*tables untouched.
Referral Ledger Tables (Public Schema)
referral_settings
Global referral configuration row (settings_key = 'global').
Columns:
enable_referralscommission_percentagefraud_window_daysmin_payout_amountcreated_at,updated_at
referral_codes
One auto-generated referral code per user.
Columns:
id(UUID, PK)user_id(unique FK ->users.user_id)code(case-insensitive unique viaLOWER(code)index)is_activecreated_at,updated_at
referrals
Attribution mapping between referrer and referred users.
Columns:
id(UUID, PK)referrer_user_id(FK ->users.user_id)referred_user_id(unique FK ->users.user_id)attribution_source(registration|checkout)locked_atcreated_at
payment_events
Payment ledger rows used as commission source records for subscriptions now and future paid features later.
Columns:
id(UUID, PK)user_id(FK ->users.user_id)source_type,source_id(unique pair)amount,currencystripe_payment_intent_id,stripe_charge_id,payment_fingerprintstatus(confirmed|refunded|fraud_flagged)paid_at,metadata,created_at
commissions
Commission ledger rows (source of truth for earnings state; no balance field).
Columns:
id(UUID, PK)referral_id(FK ->referrals.id)payment_event_id(unique nullable FK ->payment_events.id)amount,currency,percentagestatus(pending|locked|ready_for_payout|paid|cancelled)available_atpayout_batch_id(nullable FK ->payout_batches.id)created_at,updated_at
commission_status_events
Trace table for explicit commission status transitions.
Columns:
commission_id(FK ->commissions.id)from_status,to_statusreasonactor_user_id(nullable FK ->users.user_id)created_at
payout_batches
Future payout grouping placeholder; no disbursement logic yet.
Columns:
id(UUID, PK)total_amountstatus(pending|processing|completed|failed)created_at,updated_at
Notes:
- Added via migration
202605120900_add_referral_ledger_system. - Includes default disabled referral settings and default
enable_referralsfeature flag insertion.
Relationship Highlights
users 1 -> many auth_sessionsusers 1 -> many email_verification_tokensusers 1 -> many structured_notesusers 1 -> 1 referral_codesusers 1 -> many referrals(as referrer) andusers 1 -> 0..1 referrals(as referred)users 1 -> many payment_eventsreferrals 1 -> many commissionscommissions 1 -> many commission_status_eventspayout_batches 1 -> many commissionsstructured_notes 1 -> many structured_note_referencesstructured_notes 1 -> many structured_note_canvas_itemsusers 1 -> many note_boards,note_elements,note_connectors(FEAT-59 v2)note_boards 1 -> many note_boards(self, nested boards) and1 -> many note_elements/note_connectorsnote_elements 1 -> many note_elements(self, column children) and1 -> many note_connectors(as source/target)background_jobsis standalone in v1 (no FK dependency to domain tables)- Existing feature tables (
documents,notifications,user_subscriptions, etc.) continue to referenceusers.user_id
Schema Change Rules
- Follow mandatory migration controls in
/docs/database-schema/migration-convention:- required intent header (
PURPOSE,IMPACT,DATA RISK,ROLLBACK STRATEGY) - dangerous migration classification and required destructive checklist evidence
- migration rationale/changelog block in the PR
- required intent header (
- Additive auth/security migrations must include QA docs updates in
docs/features/* - Never push schema changes directly to
main; use PR flow todev