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.

The important tables
| Table | What it stores |
|---|---|
users | User profiles — name, email, avatar, role, is_admin flag, notification prefs |
apps | Every submitted project — name, slug, URL, description, categories, status, upvotes, views |
categories | Directory categories with sphere, slug, sort_order, icon, color |
payments | One row per Stripe transaction — amount, provider, status, related app/user |
ratings | 1–5 star reviews on projects |
comments | Comment threads on project pages |
bookmarks | "Save for later" per user |
newsletter | Email newsletter subscribers |
promotions | Active paid promo placements (banner, catalog, detail) |
partners | Sponsor / partner entries shown in sidebar |
analytics | Page view + event data for the built-in dashboard |
site_settings | Global knobs admins can flip without deploying (e.g. custom theme, banner) |
changelog | Entries that render on /changelog |
email_notifications | Log of emails sent (for debugging / audit) |
external_webhooks | Discord / Slack webhook configs, managed via admin UI |
link_type_changes | Audit trail when admins toggle backlink type |
backlinks | Backlink tracking data (dofollow/nofollow) |
sidebar_content | Admin-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
appswherestatus = '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.
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:
- Read supabase/schema.sql and find the RLS policies on the relevant table.
- Explain which policy is rejecting the query and why.
- Tell me whether my client choice (
getSupabaseClientanon key vsgetSupabaseAdminservice role) is correct for the use case.- 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:
| Column | Type | Checked by |
|---|---|---|
is_admin | boolean | isAdmin() helper in code |
role | text | RLS 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()):
changelogemail_notificationssite_settingslink_type_changes
They still have full RLS and follow the same patterns — just not wrapped in the Mongo-style API.
See also
- Supabase Setup — create project, run schema
- Database Layer API — how to query from code