Skip to content
Getting Started

The Supabase way to build

Which building block to reach for, and when.

Supabase is more than hosted Postgres. It's a set of composable building blocks you grow into. Start with a database, add more features as you need them, and keep as much logic in Postgres as possible.

The Architecture page tells you what the pieces are. This page tells you which piece to reach for, how to uses it as a building block, and when. Each section explains what you are adding and for what purpose, the mental model to hold, and a common mistake to avoid. The linked guides show you how to build each piece.

The progression at a glance#

Follow the path from the first need you have. Each step names the block to add and the idea to keep in mind, and opens that section.

You don't need all of these things to start. Most apps start with a database and add as needed. Over time, many apps use all of them.

Start with a database: Postgres + API#

Every Supabase project is a full Postgres database, and Supabase generates a REST API from your schema automatically. Design your tables well and you have a working backend with no server code.

Mental model: your schema is your API. The shape of your data is the shape of what clients can do.

Enough on its own for: internal tools, prototypes, and read-heavy apps.

Any table a browser or mobile client can reach still needs Row Level Security, even for public data. Reserve unprotected tables for server-side callers. A single-user or trusted-client assumption is not a security boundary.

Avoid hand-writing a REST server that only forwards create, read, update, and delete calls to the database. That rebuilds what PostgREST already generates for you. Put your effort into the schema instead.

The Next.js quickstart allows public read on its sample table so the page can load. Return here before that table holds private rows. A publishable key only reaches what Row Level Security allows.

Add users: Auth#

When your app needs to know who is making a request, add Supabase Auth. It manages users and issues a signed JWT that carries the caller's identity.

Mental model: auth issues a verifiable identity (a JWT) that the database itself can read. Other blocks use that identity for per-user behavior.

Trigger: any multi-user app, personalization, or anything that must be private to an account.

Once email and password sign-in works, add social login for providers such as Google and GitHub.

Scope data to each user: Row Level Security#

After you have users, each user needs to see only their own data. With Row Level Security (RLS), you write that rule once, as a policy on the table, and Postgres enforces it on every request made with your publishable (anon) or user keys. The secret (service_role) key bypasses RLS, so keep it server-side and enforce authorization there yourself.

Mental model: authorization is data, enforced by Postgres, not logic scattered across your app. The policy lives next to the rows it protects.

Trigger: users must not see each other's rows, multi-tenant apps, and anything where a leaked query would leak data.

Handle files and uploads: Storage#

For uploads, images, and documents, Storage gives you S3-compatible object storage that keeps its metadata in Postgres, so you secure files with RLS, the same model you use for your rows.

Mental model: files are secured by RLS too, but with their own policies on the storage.objects table. They don't inherit your app-table policies, and a public bucket serves files to anyone.

Trigger: user avatars, attachments, media, and any binary a user owns.

Write and review Storage policies alongside your data policies. They don't sync automatically, and a public bucket or a missing policy exposes files regardless of your table rules.

When you serve images, resize them with image transformations.

Add server-side logic: Edge Functions#

Some logic can't live in the client, because it needs secrets, and isn't a good fit for the database, because it calls third-party APIs or reacts to events. That's what Edge Functions are for: TypeScript functions you deploy at the edge.

Mental model: put data-shaped logic in Postgres (triggers, database functions, and RPC) and integration-shaped logic at the edge (webhooks, third-party calls, and anything holding a secret key). For logic that's really about your data, prefer a Postgres function, closer to the data and easier to secure.

Trigger: payment webhooks, calls to an external API, sending email, and any code that must hold a credential the browser can't see.

Reflect changes live: Realtime#

When your UI has to reflect changes as they happen, Realtime streams database changes, broadcasts messages, and tracks presence over WebSockets.

Mental model: subscribe to the database instead of polling it. The client reacts to change; it doesn't ask "anything new?" on a timer.

Trigger: dashboards that update live, chat, collaboration, presence indicators, and multiplayer.

Avoid polling loops. A setInterval that re-fetches every few seconds wastes requests, adds latency, and still misses fast changes. Subscribe once and let the change come to you.

Authorize Broadcast and Presence with Realtime Authorization. Those channels use policies on realtime.messages.

Serve real traffic: Harden for production#

Before real traffic, confirm the app is secure and ready to scale. Run the advisors, review every RLS policy, pool connections with Supavisor, set up observability and backups, and check query and index performance.

Mental model: production readiness is verification, not new features. Confirm the blocks you already added are configured correctly for traffic.

Trigger: launch, production users, and production data.

Where to go next#

  • New here? Start with a framework quickstart and come back to add blocks as you need them.
  • Working locally? Use the Supabase CLI.
  • Want the component-level picture? See Architecture.
  • Ready to build a specific piece? Follow the Build it links in each stage above.