1. Create a Supabase project

    Go to supabase.com and create a new project.

  2. Get your credentials

    Navigate to Settings > API and copy:

    • Project URL
    • Publishable key
    • Secret key
  3. Initialize the database

    Run npm run init and provide your PostgreSQL connection URL, or manually run the SQL files in the SQL Editor.

Schema Overview

The database schema is organized into several functional areas. Click on each table to see detailed field information.

RLS follows the account-centric model: reads are scoped through memberships, and writes that include a user attribution column also require user_id = auth.uid() unless the write is service-role-only. This prevents one workspace member from forging another member's chat message or document ownership. Sensitive profile identity/display fields are persisted through server-side routes with the service role rather than direct browser writes.

Data API Grants

Supabase Data API access is grant-explicit in this boilerplate. RLS decides which rows a role can see or mutate, but Postgres grants decide whether anon, authenticated, or service_role can reach a table, view, sequence, or RPC at all. The migration 20260527225001_explicit_data_api_grants.sql resets legacy/default privileges, revokes broad access, and then grants only the intended Data API surface.

RLS is not a substitute for Data API grants

When you add a new exposed public object, include the matching GRANT or REVOKE statements in the migration and mirror them into supabase/schema.sql or supabase/cms-schema.sql. Fresh Supabase projects and database resets should not depend on dashboard defaults.

Role Expected surface
anonPublic read-only objects such as roles, affiliate tiers, changelog entries, CMS pages/blocks, and blog taxonomy.
authenticatedUser/admin Data API paths that are still bounded by RLS policies, including account-scoped dashboard, chat, documents, jobs, invitations, consent, CMS admin, and related reads/writes.
service_roleBackend-only full table access, service queues, audit/log tables, document chunks, and privileged RPC paths. The key stays server-only.

For new RPCs, revoke EXECUTE from public, anon, and authenticated unless browser clients are explicitly meant to call the function. For Data API inserts that use identity or serial columns, grant sequence privileges intentionally rather than relying on defaults.

Users & Auth
Multi-Tenancy
Billing & Credits
AI & Chat
CMS
Jobs System

profiles User Profiles

Extends auth.users with additional profile data. Auto-created on signup via trigger.

Field Type Description
iduuid (PK)References auth.users(id)
emailtextUser email (synced from auth.users)
full_nametextFull display name
first_nametextFirst name (from onboarding)
last_nametextLast name (from onboarding)
birthdaydateOptional birthday
phonetextOptional phone number
avatar_urltextProfile picture URL (OAuth or upload)
is_adminbooleanPlatform super-admin flag
is_disabledbooleanAccount disabled by admin
onboarding_completedbooleanHas completed onboarding flow
newsletter_subscribedbooleanNewsletter subscription status
scheduled_deletion_attimestamptzGDPR deletion scheduled date
created_attimestamptzProfile creation timestamp
updated_attimestamptzLast update timestamp
Auto-Admin Assignment

When a user signs up with an email matching app_settings.admin_email, the is_admin flag is automatically set to true.

app_settings Application Settings

Global key-value settings for the application.

Field Type Description
keytext (PK)Setting key (e.g., 'admin_email')
valuetextSetting value
created_attimestamptzCreation timestamp
updated_attimestamptzLast update timestamp

accounts Accounts (Core)

The central entity of the system. All resources are attached to accounts, not users directly.

Field Type Description
iduuid (PK)Unique account identifier
typeenum'personal' | 'workspace'
nametextAccount display name
slugtext (unique)URL-friendly identifier (workspaces only)
owner_user_iduuid (FK)References auth.users(id)
credits_balanceintegerCurrent credit balance
max_membersintegerMember limit (null = unlimited)
stripe_customer_idtext (unique)Stripe customer ID
created_attimestamptzCreation timestamp
updated_attimestamptzLast update timestamp

roles Dynamic Roles

Role definitions with permissions. System roles (owner, admin, member) are protected.

Field Type Description
iduuid (PK)Unique role identifier
nametextDisplay name (e.g., "Administrator")
slugtext (unique)Identifier slug (e.g., "admin")
descriptiontextRole description
permissionsjsonbJSONB array of permission strings (default [])
is_systembooleanProtected system role (cannot delete)
colortextBadge color (e.g., "blue", "amber")
icontextIcon name (e.g., "crown", "shield")
display_orderintegerSort order for display
created_attimestamptzCreation timestamp
updated_attimestamptzLast update timestamp

Available Permissions: account:update, account:delete, billing:view, billing:manage, members:view, members:invite, members:remove, members:update_role, api_keys:view, api_keys:create, api_keys:delete, ai:use. Roles store these as a JSONB array of strings; is_system roles (owner/admin/member) cannot be deleted.

memberships User-Account Relationships

Links users to accounts with their assigned role. Unique constraint: (account_id, user_id)

Field Type Description
iduuid (PK)Unique membership identifier
account_iduuid (FK)References accounts(id)
user_iduuid (FK)References profiles(id)
roleenumLegacy: 'owner' | 'admin' | 'member'
role_slugtext (FK)References roles(slug) - dynamic roles
created_attimestamptzMembership creation
updated_attimestamptzLast update

invitations Workspace Invitations

Pending invitations to join a workspace. Expire after 7 days by default.

Field Type Description
iduuid (PK)Unique invitation identifier
account_iduuid (FK)References accounts(id)
emailtextInvitee email address
role_slugtext (FK)Role to assign on acceptance
invited_byuuid (FK)References profiles(id)
tokentext (unique)Secure invitation token
statusenum'pending' | 'accepted' | 'expired' | 'revoked'
expires_attimestamptzExpiration (default: 7 days)
created_attimestamptzCreation timestamp

subscriptions Stripe Subscriptions

Mirrors Stripe subscription data. Synced via webhooks.

Field Type Description
iduuid (PK)Local subscription ID
account_iduuid (FK)References accounts(id)
stripe_subscription_idtext (unique)Stripe subscription ID (sub_xxx)
stripe_price_idtextStripe price ID (price_xxx)
plan_idtextPlan ID from config/pricing.ts
statusenum'active' | 'canceled' | 'past_due' | etc.
current_period_starttimestamptzBilling period start
current_period_endtimestamptzBilling period end
cancel_at_period_endbooleanWill cancel at period end
canceled_attimestamptzCancellation timestamp
trial_endtimestamptzStripe-mirrored trial end timestamp; partial index subscriptions_trial_end_idx covers the trialing-status hot path
paused_attimestamptzSet on customer.subscription.paused by applyPaused(). Audit only. Required by the migration shipped 2026-05-06 — without these columns the pause/resume webhook handlers fail silently
resumed_attimestamptzSet on customer.subscription.resumed by applyResumed()
created_attimestamptzCreation timestamp
updated_attimestamptzLast update timestamp
Plans are in Code, Not Database

The plan_id references plans defined in config/pricing.ts. Plan details (price, features, credits) are NOT stored in the database.

payments One-Time Payments

Records of one-time purchases (credit packs, products, etc.).

Field Type Description
iduuid (PK)Payment identifier
account_iduuid (FK)References accounts(id)
user_iduuid (FK)User who made the payment
amountintegerAmount in cents
currencytextCurrency code (EUR, USD, GBP, CAD, CHF)
typeenum'credit_pack' | 'product' | 'service' | 'donation' | 'other' (default) | 'license'
statusenum'pending' | 'completed' | 'failed' | 'refunded'
descriptiontextPayment description
stripe_session_idtext (unique)Stripe checkout session ID
stripe_payment_intent_idtextStripe payment intent ID
stripe_invoice_idtextStripe invoice ID
metadatajsonbAdditional payment data
created_attimestamptzPayment timestamp

licenses One-Time Licenses

Records of license purchases for one-time payment access (alternative to subscriptions).

Field Type Description
iduuid (PK)License identifier
account_iduuid (FK)References accounts(id) (ON DELETE CASCADE)
product_idtextProduct ID from config/pricing.ts
license_typeenum'lifetime' | 'yearly' | 'monthly' | 'custom'
statusenum'active' | 'expired' | 'revoked'
stripe_session_idtext (unique)Stripe checkout session id — idempotency gate on webhook replay
stripe_payment_intent_idtextStripe payment intent reference
amount_paidintegerAmount in cents
currencytextCurrency code (default 'EUR')
purchased_byuuid (FK)References auth.users(id) — user who initiated the purchase
starts_attimestamptzLicense start date (default now())
expires_attimestamptzExpiration date (NULL for lifetime)
featuresjsonbFeature flags snapshot at purchase (e.g., {"api_access": true})
credits_includedintegerOne-time credits granted on purchase
credits_grantedbooleanWhether the one-time credits have been added
limitsjsonbResource limits snapshot (e.g., {"projects": 10})
metadatajsonbAdditional license data
created_attimestamptzPurchase timestamp
updated_attimestamptzLast update timestamp
Billing Models

Configure via billingModel in config/app.ts: 'subscription' | 'license' | 'hybrid'. Hybrid mode allows both subscriptions and licenses.

credit_transactions Credit Ledger

Audit trail of all credit changes. Immutable.

Field Type Description
iduuid (PK)Transaction identifier
account_iduuid (FK)References accounts(id)
amountintegerChange amount (+ = add, - = deduct)
balance_afterintegerBalance after transaction
reasontextHuman-readable reason
sourceenum (credit_source)'subscription_refill' | 'one_time_purchase' | 'admin_adjustment' | 'ai_usage' | 'refund' | 'bonus' | 'license_purchase' | 'referral'. Plus webhook audit-trail values used for zero-amount ledger rows: 'payment_method_added', 'payment_method_removed', 'payment_failed', 'dispute_created', 'dispute_closed', 'checkout_expired', 'async_payment_failed', 'trial_ending', 'payment_action_required'.
metadatajsonbAdditional transaction data
created_attimestamptzTransaction timestamp

chat_sessions Chat Sessions

Conversation sessions with optional agent assignment.

Field Type Description
iduuid (PK)Session identifier
account_iduuid (FK)References accounts(id)
user_iduuid (FK)References profiles(id)
titletextSession title (auto-generated or user-defined)
agent_idtextDefault agent for this session (e.g., 'chat')
created_attimestamptzSession creation
updated_attimestamptzLast activity timestamp

chat_messages Chat Messages

Individual messages within a session.

Field Type Description
iduuid (PK)Message identifier
session_iduuid (FK)References chat_sessions(id)
account_iduuid (FK)References accounts(id)
roleenum'user' | 'assistant' | 'system'
contenttextMessage content
tokens_usedintegerTokens consumed by this message
agent_idtextAgent that generated response
model_idtextLLM model used (e.g., 'gpt-4o')
created_attimestamptzMessage timestamp

ai_requests AI Request Logs

Detailed logging of all AI API calls for observability and cost tracking.

Field Type Description
iduuid (PK)Request identifier
account_iduuid (FK)References accounts(id)
user_iduuid (FK)References profiles(id) (who made the request)
agent_typetextAgent used (e.g., 'chat', 'code-assistant')
modeltextLLM model (e.g., 'gpt-4o-mini')
input_tokensintegerInput tokens consumed
output_tokensintegerOutput tokens generated
total_tokensintegerComputed: input + output (generated)
cost_centsnumericCalculated cost in cents
latency_msintegerResponse time in milliseconds
statustext'success' | 'error'
error_messagetextError details (if status is 'error')
metadatajsonbAdditional request metadata
created_attimestamptzRequest timestamp

cms_pages CMS Pages

Multi-locale content pages. All locales stored in JSONB fields per row.

Canonical CMS locale keys are the BCP 47 ids from i18n/config.ts (fr-FR, en-US, en-CA, fr-CH). The app still reads legacy language-only keys (fr, en) through localized helpers for backwards compatibility, while admin writes and schema seeds normalize content back to the configured BCP 47 keys.

Field Type Description
iduuid (PK)Page identifier
slugtext (unique)URL slug (e.g., 'about', 'blog/my-post')
titlejsonb{"fr-FR": "...", "fr-CH": "...", "en-US": "...", "en-CA": "..."}
contentjsonbHTML/Markdown content per locale
excerptjsonbShort description (for blog posts)
featured_imagetextFeatured image URL
category_iduuid (FK)Blog category reference (ON DELETE SET NULL)
publishedjsonbPublished status per locale
seo_titlejsonbSEO title override
seo_descriptionjsonbMeta description
seo_noindexjsonbNoindex flag per locale
created_attimestamptzCreation timestamp
updated_attimestamptzLast update

cms_blocks CMS Blocks

Reusable content snippets (promo bars, banners, etc.).

Field Type Description
iduuid (PK)Block identifier
keytext (unique)Block key (e.g., 'promo_bar')
contentjsonbBlock content per locale
created_attimestamptzCreation timestamp
updated_attimestamptzLast update

blog_categories Blog Categories

Multi-locale blog categories with color coding and display ordering.

Field Type Description
iduuid (PK)Category identifier
slugtext (unique)URL-safe identifier
namejsonbMulti-locale name {"fr-FR": "...", "fr-CH": "...", "en-US": "...", "en-CA": "..."}
descriptionjsonbMulti-locale description
colortextBadge color (slate, amber, blue, green, red, purple, pink, indigo, teal, orange)
display_orderintegerSort order for display
created_attimestamptzCreation timestamp
updated_attimestamptzLast update

blog_tags Blog Tags

Multi-locale blog tags for content labeling.

Field Type Description
iduuid (PK)Tag identifier
slugtext (unique)URL-safe identifier
namejsonbMulti-locale name {"fr-FR": "...", "fr-CH": "...", "en-US": "...", "en-CA": "..."}
created_attimestamptzCreation timestamp
updated_attimestamptzLast update

blog_post_tags Blog Post Tags (Join Table)

Many-to-many relationship between blog posts and tags.

Field Type Description
iduuid (PK)Row identifier
page_iduuid (FK)References cms_pages (ON DELETE CASCADE)
tag_iduuid (FK)References blog_tags (ON DELETE CASCADE)
created_attimestamptzCreation timestamp

jobs Job Definitions

Background job configurations for scheduled and manual tasks.

Field Type Description
iduuid (PK)Job identifier
nametext (unique)Job name
descriptiontextJob description
executorenum'edge' | 'internal'
function_nametextHandler function name
cron_expressiontextCron schedule (e.g., '0 3 * * *')
is_enabledbooleanJob enabled status
notify_on_failurebooleanEmail admin when job fails
timeout_secondsintegerExecution timeout
max_retriesintegerMax retry attempts
last_run_attimestamptzLast execution time
last_run_statusenumLast run result
run_countintegerTotal run count
success_countintegerSuccessful runs
failure_countintegerFailed runs
configjsonbJob-specific configuration

job_runs Job Execution History

Records of job executions with output and error logging.

Field Type Description
iduuid (PK)Run identifier
job_iduuid (FK)References jobs(id)
statusenum'pending' | 'running' | 'success' | 'failed'
triggered_byenum'cron' | 'manual' | 'webhook' | 'api'
started_attimestamptzExecution start
completed_attimestamptzExecution end
duration_msintegerExecution duration
outputjsonbJob output/result
errortextError message if failed

api_keys B2B API Keys

Hashed API keys for programmatic access.

Field Type Description
iduuid (PK)Key identifier
account_iduuid (FK)References accounts(id)
nametextKey display name
key_prefixtextVisible prefix (e.g., 'sk_live_abc...')
key_hashtextHashed key value
scopestext[]Permitted API scopes
last_used_attimestamptzLast usage timestamp
expires_attimestamptzExpiration date (optional)
is_activebooleanKey active status

user_consents GDPR Consents

User consent records for GDPR compliance. Logged-in consent changes are written server-side via POST /api/user/consents so IP/user-agent capture, CSRF checks, rate limiting, and service-role writes stay out of browser code.

Field Type Description
iduuid (PK)Consent identifier
user_iduuid (FK)References profiles(id)
typeenum'marketing' | 'analytics' | 'necessary'
acceptedbooleanConsent status
ip_addressinetIP when consent given
accepted_attimestamptzConsent timestamp

account_deletion_requests GDPR Deletion Queue

Pending account deletion requests with 30-day grace period.

Field Type Description
iduuid (PK)Request identifier
account_iduuid (FK)References accounts(id)
requested_byuuid (FK)References profiles(id)
statusenum'pending' | 'processing' | 'completed' | 'cancelled' | 'failed'
scheduled_fortimestamptzDeletion date (default: now + 30 days)
completed_attimestamptzActual deletion timestamp
cascade_membersbooleanWhen true, the deletion job also deletes every member's auth.users row. Set only by /my-account when a workspace request is part of the owner's personal-account cascade in B2B mode — see "B2B Member Cascade" below. A BEFORE UPDATE column-lock trigger makes this column (and every other field except status) immutable to non-service_role callers, and the owner-cancel UPDATE policy carries a matching WITH CHECK; a member or owner cannot flip cascade_members via the Data API to force member-account deletion.
processor_statusjsonbPer-processor deletion proof for Stripe, newsletter/email, Storage, Supabase Auth, and DB cascade steps.
processor_errorsjsonbRedacted processor failures that blocked completion.
error_messagetextShort redacted summary when the request is marked failed.
retry_countintegerNumber of failed processor attempts.

admin_logs Admin Audit Logs

Admin action audit trail for compliance and debugging.

Field Type Description
iduuid (PK)Log entry identifier
admin_user_iduuid (FK)Admin who performed action
actiontextAction performed (e.g., 'user.disable')
target_typetextEntity type (e.g., 'user', 'subscription')
target_iduuidAffected entity ID
detailsjsonbAction details and changes
ip_addressinetAdmin IP address
created_attimestamptzAction timestamp

in_app_notifications In-App Notifications

User notifications with read status tracking. Column-lock trigger restricts user updates to the read column only.

Field Type Description
iduuid (PK)Notification identifier
user_iduuid (FK)References profiles(id)
account_iduuid (FK)NOT NULL — References accounts(id) (ON DELETE CASCADE)
typeenum'info' | 'success' | 'warning' | 'error' | 'system'
titletextNotification title (max 200 chars)
messagetextNotification body (max 1000 chars)
linktextOptional relative URL for navigation
readbooleanRead status (default: false)
created_attimestamptzCreation timestamp

changelog_entries Changelog Entries

Public changelog with multi-locale JSONB content. Supports versioning and type badges.

Field Type Description
iduuid (PK)Entry identifier
versiontextVersion label (e.g., '1.2.0')
typeenum'feature' | 'improvement' | 'fix' | 'breaking' | 'security'
titlejsonbMulti-locale title ({ "fr-FR": "...", "fr-CH": "...", "en-US": "...", "en-CA": "..." })
contentjsonbMulti-locale markdown content
publishedbooleanPublish status (global, not per-locale)
published_attimestamptzPublication date (for ordering)
created_attimestamptzCreation timestamp
updated_attimestamptzLast update timestamp

documents RAG Documents

Uploaded documents for RAG chat. Tracks processing status, file metadata, and chunk count.

Field Type Description
iduuid (PK)Document identifier
account_iduuid (FK)References accounts(id)
user_iduuid (FK)References profiles(id) (uploader)
nametextOriginal filename
storage_pathtextSupabase Storage path (UUID-based)
file_typetextFile type
file_sizeintegerFile size in bytes
statusenum'pending' | 'processing' | 'ready' | 'error'
chunk_countintegerNumber of generated chunks
error_messagetextProcessing error details
created_attimestamptzUpload timestamp

document_chunks Document Chunks (Vector)

Text chunks with vector embeddings for similarity search. Uses pgvector vector(1536) with HNSW index. Embedding model is configurable in config/ai.ts (default: text-embedding-3-small).

Field Type Description
iduuid (PK)Chunk identifier
document_iduuid (FK)References documents(id) (CASCADE delete)
contenttextChunk text content
embeddingvector(1536)Embedding vector (model configurable via embeddingConfig.defaultModel)
chunk_indexintegerPosition within document
metadatajsonbChunk metadata (source, chunk index, total, embedding_model)
created_attimestamptzCreation timestamp

Database Functions

The schema includes several PostgreSQL functions for common operations.

Trigger Functions

Function Trigger Description
handle_new_user() on_auth_user_created Creates profile on signup. Sets is_admin=true if email matches admin setting.
handle_new_user_account() on_auth_user_created_account Creates personal account and owner membership on signup.
update_job_stats() job_runs_update_stats Updates job statistics after each run completes.

Credit Functions

Function Parameters Description
add_credits() p_account_id uuid, p_amount integer, p_source credit_source, p_reason text default null, p_metadata jsonb default '{}'::jsonb Atomically adds credits to an account, writes the matching credit_transactions ledger row, and returns the new balance. service-role only (REVOKE EXECUTE FROM authenticated, anon).
decrement_credits() p_account_id uuid, p_amount integer, p_reason text default null, p_metadata jsonb default '{}'::jsonb, p_source credit_source default 'ai_usage' Atomically deducts credits, writes the ledger row, and returns the new balance. Raises a CHECK (credits_balance >= 0) violation when the result would go negative — wrap calls in try/catch. service-role only.

These functions ensure atomic credit operations. Never update the credits_balance column directly; always use these RPC functions to maintain data integrity and the audit trail.

Utility Functions

Function Returns Description
get_user_accounts(user_uuid) setof accounts Returns all accounts a user belongs to
is_account_member(account_uuid, user_uuid) boolean Check if user belongs to an account
get_user_role_slug(account_uuid, user_uuid) text Get user's role in an account
user_has_permission(account_id, permission) boolean Check if current user has a specific permission
user_belongs_to_account(account_id) boolean RLS helper - check membership (security definer)
user_is_account_admin(account_id) boolean RLS helper - check if owner/admin (security definer)
sync_missing_profiles() void Creates profiles for auth.users without one
has_valid_license(p_account_id) boolean True if the account has any active, non-expired license. SECURITY INVOKER, callable from authenticated context.
get_active_license(p_account_id) setof licenses Returns the current active license row (if any) for the account.
mark_expired_licenses() integer Batch-marks expires_at < now() licenses as 'expired'. Called by the check-license-expiration daily job. Returns row count.
grant_license_credits(p_license_id) integer Grants the license's credits_included via add_credits (source 'license_purchase') and flips credits_granted = true. Idempotent on the boolean flag. service-role only.
generate_referral_code(p_account_id, p_length, p_alphabet) text Idempotent: returns the existing active code for the account, or generates a new one with collision-retry. service-role only.
apply_referral_code(...) uuid Attribution entry point — runs self-referral / same-owner / one-per-lifetime / IP rate-limit guards; inserts the referrals row. service-role only. See .claude/rules/referrals.md.
qualify_referral(p_referral_id, p_trigger, p_referrer_credits, p_referred_credits) void Calls add_credits on both sides with source='referral'. service-role only.
reverse_referral(p_referral_id, p_reason) void Per-side guarded decrement_credits — an insufficient balance on one side doesn't abort the other (details written to metadata). service-role only.
purge_error_logs(p_retention_days) integer Deletes error_logs rows older than the supplied retention window. Called by the daily purge-error-logs job. service-role only.
+ 12 affiliate RPCs Application workflow, link creation, click recording, attribution, conversion ledger, reversal, hold-period maturation, and aggregate stats. All service-role only. See .claude/rules/affiliates.md for the full registry.

Entity Relationships

auth.users (Supabase Auth) │ │ 1:1 (on signup trigger) ▼ profiles │ │ 1:N ▼ memberships ◄─── N:1 ───► accounts │ │ │ N:1 ├── 1:N ──► subscriptions ▼ ├── 1:N ──► payments roles ├── 1:N ──► licenses ├── 1:N ──► credit_transactions ├── 1:N ──► chat_sessions ──► chat_messages ├── 1:N ──► ai_requests ├── 1:N ──► documents ──► document_chunks ├── 1:N ──► api_keys ├── 1:N ──► invitations ├── 1:N ──► in_app_notifications ├── 1:N ──► referral_codes / referrals └── 1:N ──► affiliate_* (8 tables, see below)

Other domain tables grouped by ownership:

  • Standalone (global): app_settings, admin_logs, error_logs, pending_emails, jobs / job_runs / job_handlers.
  • User-scoped: user_consents, user_access_logs, push_subscriptions, notification_preferences, notification_log, account_deletion_requests.
  • CMS & content: cms_pages, cms_blocks, blog_categories, blog_tags, blog_post_tags, changelog_entries.
  • Referrals: referral_codes, referrals (account-scoped via the referrer).
  • Affiliates (8 tables): affiliate_tiers, affiliate_applications, affiliates, affiliate_links, affiliate_clicks (service-role only), affiliate_attributions, affiliate_conversions, affiliate_payouts. All account-scoped through the affiliate row.