Data model
This page explains the shape of the database and the reasoning behind it. For migrations and the Supabase clients, see Database & models.
The core tables
A single migration, supabase/migrations/0001_initial_schema.sql, creates everything:
users, mirrorsauth.userswith profile and role fields.subscriptions, links a user to their Stripe customer and subscription with a status.payments, one row per invoice or charge.email_verificationsandpassword_resets, short-lived tokens with expiry.blog_posts, content with SEO fields and a published flag.
Why mirror auth.users
Supabase keeps its own auth.users table. ShipVeryFast adds a public.users table and a trigger that creates a matching row whenever someone signs up. This gives you a normal table to join against and extend, without touching the auth schema.
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();Row Level Security is the boundary
RLS is enabled on every table. A user can read and update only their own profile, and read only their own subscriptions and payments. Published blog posts are public. Because the rule lives in the database, it holds no matter which client makes the query, so a leaked key or a buggy route cannot read another tenant's data.
The service role bypasses RLS
SUPABASE_SERVICE_ROLE_KEY ignores RLS by design, for trusted server work. Use the anon client for anything driven by user input, and keep the service key server-side only.