Skip to main content

Schema Overview

A map of the database tables and what each one stores.

All tables live in one file: supabase/schema.sql. That file is the single source of truth. Run it once when you set up Supabase (see Supabase Setup) and forget about it.

Supabase Table Editor with the full table list

The important tables

TableWhat it stores
usersUser profiles — name, email, avatar, role, is_admin flag, notification prefs
appsEvery submitted project — name, slug, URL, description, categories, status, upvotes, views
categoriesDirectory categories with sphere, slug, sort_order, icon, color
paymentsOne row per Stripe transaction — amount, provider, status, related app/user
ratings1–5 star reviews on projects
commentsComment threads on project pages
bookmarks"Save for later" per user
newsletterEmail newsletter subscribers
promotionsActive paid promo placements (banner, catalog, detail)
partnersSponsor / partner entries shown in sidebar
analyticsPage view + event data for the built-in dashboard
site_settingsGlobal knobs admins can flip without deploying (e.g. custom theme, banner)
changelogEntries that render on /changelog
email_notificationsLog of emails sent (for debugging / audit)
external_webhooksDiscord / Slack webhook configs, managed via admin UI
link_type_changesAudit trail when admins toggle backlink type
backlinksBacklink tracking data (dofollow/nofollow)
sidebar_contentAdmin-editable sidebar widgets

Security (RLS)

Every table has Row Level Security enabled. That means queries from the browser (using the anon key) are filtered by policies defined in schema.sql. Typical policies:

  • Anyone can read apps where status = 'live'
  • Users can insert/update their own rows (user_id = auth.uid())
  • Admins can do anything (role = 'admin')

Server code that uses getSupabaseAdmin() (service role) bypasses RLS — it sees everything.

AI Prompt· Debug an RLS policy

I'm getting this error when querying Supabase from {client-side | server-side | API route}:

\{paste the exact error, e.g. "permission denied for table apps" or "new row violates row-level security policy"\}

My query (sanitize any secrets):

\{paste the code calling db.xxx() or supabase.from().xxx()\}

User context at the time of the query: {logged-in as normal user | logged-in as admin | anonymous}.

Please:

  1. Read supabase/schema.sql and find the RLS policies on the relevant table.
  2. Explain which policy is rejecting the query and why.
  3. Tell me whether my client choice (getSupabaseClient anon key vs getSupabaseAdmin service role) is correct for the use case.
  4. If the policy itself is wrong, suggest the SQL to ALTER POLICY and paste it back.

Don't modify schema.sql yet — just show me the change first.

Timestamps — set by triggers

Every table has created_at and updated_at columns. Triggers set them automatically — don't include them in your insertOne() / updateOne() calls:

// GOOD — trigger handles it
await db.insertOne('apps', { name: 'Foo', slug: 'foo', status: 'pending' });
 
// WRONG — overrides the trigger, may break things
await db.insertOne('apps', { name: 'Foo', created_at: new Date() });

Admin field quirk

Two columns do similar things:

ColumnTypeChecked by
is_adminbooleanisAdmin() helper in code
roletextRLS policies in the DB

Keep them in sync when granting admin access. Set is_admin = true and role = 'admin'.

Tables in the schema but not in the db layer

A handful of tables are accessed via the Supabase client directly (not through db.find()):

  • changelog
  • email_notifications
  • site_settings
  • link_type_changes

They still have full RLS and follow the same patterns — just not wrapped in the Mongo-style API.

See also