Supabase Development

Supabase development, treated as Postgres engineering rather than a Firebase clone.

You’ve already chosen Supabase. What you need next is an opinion about Row Level Security, connection pooling and vector indexes, not a marketplace renting you a body for forty hours. This page covers the decisions that decide whether the project survives its first ten thousand users: how policies get written so they use their indexes, which runtime talks to which port, and where service_role is allowed to appear at all.

If you’re still choosing the stack rather than confirming it, How we build web applications covers the framework and hosting decisions this page assumes are settled. And if this is really an AI feature with a database attached, our AI consulting engagement is the better door.

What a Supabase engagement leaves in your codebase.

A demo with RLS switched off will return whatever you ask it for. This build gets judged six months later, when a new engineer changes one policy without leaking another tenant’s rows.

A policy set, and the tests that prove it

RLS on every table holding user data, policies split by command rather than one catch-all FOR ALL, and tests that run each policy as a real signed-in user.

A migration history that is the schema

Every change lands as a file in version control, exercised against a branch database first. Nothing exists only because somebody clicked it in the dashboard.

A connection strategy, written per runtime

Which processes use the pooler in transaction mode, which need a session, which hold a direct connection. Get this wrong and it shows up as intermittent timeouts at 9am, not as a review comment.

Auth wired to actual authorisation

Supabase Auth answers who you are. Roles, org membership and plan tier become tables, with only what belongs in the token surfaced through a custom access token hook.

A boundary map for functions, triggers and jobs

What runs in a Deno Edge Function, what in a route handler, what belongs in a trigger, and what belongs in a queue row drained by pg_cron. Each one carries its reason.

Storage policies and a measurable pgvector layer

Object policies on storage.objects mirroring the table policies, signed URLs with real expiry, and retrieval recall measured against a labelled question set.

Engineering position

Six things that separate a Supabase build from a Supabase demo.

auth.uid() inside a policy runs once per row until you wrap it

A policy written as using (user_id = auth.uid()) re-evaluates that function for every candidate row; the planner has no licence to hoist it. Write using (user_id = (select auth.uid())) and it becomes an InitPlan, evaluated once for the statement. At five thousand rows the difference is invisible. At five million it’s a gateway timeout, and it arrives all at once rather than gradually. Add TO authenticated so anonymous requests get refused before evaluation, and split policies by command: one FOR ALL policy whose WITH CHECK was copied from its USING clause is how a user writes a row into a tenant they can’t read.

Every policy is a WHERE clause, so every policy needs an index

Postgres folds RLS into your query rather than layering it above. using (tenant_id = (select auth.jwt() ->> 'tenant_id')) is a predicate the planner has to satisfy on every read, and an unindexed tenant_id hands each authenticated user a sequential scan. A policy on org_members that queries org_members fails outright with infinite recursion detected, so move that lookup into a SECURITY DEFINER function, declared STABLE with SET search_path = ''. And on the record: below Series A, absent a contractual isolation requirement, shared schema with a tenant column beats database-per-tenant. Per-tenant databases buy you migrations that run N times and a connection budget that scales with customer count.

service_role does not bypass a policy, it bypasses the model

The service key maps to a Postgres role with BYPASSRLS. One server action instantiated with it and every policy on every table stops applying for that request. That’s the authorisation layer switched off rather than bent. It usually happens because a developer hit a policy that was wrong and reached for the key that silenced the error. Use the anon key with the user’s JWT for anything user-scoped, including on the server, where the session comes off the cookie. Keep service_role to paths no user request can reach, never behind a NEXT_PUBLIC_ prefix, and list every file that holds it.

Serverless plus Postgres is connection exhaustion without a pooler

A small instance tops out in the low hundreds of connections, each one a backend process holding memory, while serverless scales to hundreds of concurrent invocations by design. Supavisor transaction mode on port 6543 returns the connection after every statement, which is what your functions should use. It costs you prepared statements, so postgres-js needs prepare set to false, and it costs you session state, so SET, LISTEN and NOTIFY, and advisory locks held across statements don’t survive. Session mode on 5432 suits a long-lived container and migration tooling. Decide per runtime rather than once per project.

Where code lives is an architecture decision, not a preference

Edge Functions run Deno on their own deploy clock, which is what you want for third-party webhooks whose URLs shouldn’t move every release and for jobs that belong near the database. Route handlers are right when the code shares your application’s types, session and validation. Triggers are excellent for invariants like updated_at, an audit row or a denormalised counter, and poor as business logic: a trigger reaching out over pg_net has no retries and no way to test the transaction that fired it. Realtime is not a queue either. Postgres Changes is an at-most-once broadcast with no replay after a reconnect, so use it to say something changed, then refetch. Work that has to happen exactly once belongs in a table read with FOR UPDATE SKIP LOCKED.

pgvector is a real vector store if you choose the index deliberately

IVFFlat builds fast but clusters rows into lists, so it wants representative data present at build time, and its recall drifts as you insert until somebody reindexes. HNSW builds slower and costs more memory, holds recall as the table grows, and can be built on an empty table; m and ef_construction are fixed at build, while ef_search trades recall against latency per query. Filtering is the trap nobody warns you about. An approximate index answers nearest neighbours overall, then Postgres applies your tenant predicate to that result, so a narrow filter returns fewer rows than you asked for. Use partial indexes per tenant, or plain exact search when the candidate set is small. An indexed vector column also stops at 2,000 dimensions.

The vector half leads straight into the case for retrieval over fine-tuning for most product features and into how we scope AI automation that reads your own data.

When not to use it

Five projects we wouldn’t put on Supabase.

We build on this stack and we’re still talking you out of it five times. The offline case in particular is a mobile architecture question long before it’s a database one.

Writes that must be fast on three continents

One primary Postgres, with read replicas as an option. Reads move closer to users; writes do not. If that’s your hot path, pick a distributed database before the schema exists.

Analytics sharing the transactional instance

Wide aggregate scans against the instance serving your app will starve it, and another index will not save you. Replicate to a warehouse and let the database stay transactional.

Offline-first is the product, not a nicety

There’s no built-in conflict resolution for a client that’s been offline a week. Use a sync engine built for it: PowerSync or ElectricSQL over Postgres. Merge semantics do not bolt on afterwards.

Nobody on your side will own a database

Postgres becomes yours: migrations, indexes, the connection budget, the plan for when a table gets large. That’s a feature when somebody will hold it and a liability when nobody will.

Everything already lives in one cloud’s private network

If the architecture requires that the database never be reachable from the public internet, a managed Postgres in that same account with IAM authentication leaves you fewer exceptions to write down.

How it runs

What is fixed, and what pushes a Supabase build up.

Your repo and your cloud from commit one

Schema, policies, functions and policy tests are files in your repository from the first migration, not screenshots in a thread.

A staging URL by the end of week one

Deployed and clickable on a branch database, so migrations get exercised somewhere safe. A build and a short written note every Friday.

Decisions in writing

Every use of service_role, every trigger, every boundary between an Edge Function and a route handler, recorded with its reasoning.

Fixed scope, quoted before kickoff

Change requests are priced and confirmed in writing before anyone starts. Database work grows a tail of small discoveries, and we quote that tail rather than absorbing it.

We quote fixed scope against a published rate card rather than selling hours, and that’s the real difference between this page and the staffing marketplaces. Platforms start at $28,000 and MVPs at $24,000. The minimum for a build is $7,500, a retainer starts at $2,500 a month, and most projects land between $18,000 and $45,000. Those published figures are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff.

The number of tables holding user data moves the price most, then how many distinct roles the policies have to encode. Whether existing data is migrating in matters, and so does whether the product needs realtime or vector search. Same reasoning we apply to what an MVP actually costs. Send us the schema, or just who’s allowed to see what, and you get the policy surface back before you get a price.

FAQ

The questions that decide it

Two things. Wrap the call in a scalar subquery, using (user_id = (select auth.uid())), so Postgres evaluates it once per statement instead of once per row. Then index the column the policy filters on, because Postgres folds RLS into your query as a WHERE clause rather than applying it above. TO authenticated lets anonymous requests skip evaluation entirely.

It’s Postgres, so you outgrow it the way you outgrow any single-primary Postgres: multi-region writes, or analytics competing with the application for one instance. Neither arrives early. What kills projects sooner is treating it as a Firebase clone, with no migration history, policies clicked into the dashboard, and service_role used everywhere.

Supabase Auth when you want email, OAuth and magic links wired to your own tables cheaply. A dedicated provider such as Clerk or WorkOS when enterprise SSO or SCIM provisioning is on the roadmap. Either way, authentication is not authorisation: roles and membership belong in your schema, and the policies read them from there.

Edge Functions when the code has to deploy independently of the frontend: third-party webhooks whose URLs you don’t want to re-point every release, scheduled jobs, work that belongs near the database. Route handlers when it shares your application’s types, session and validation. Using both is fine. Using both without a written rule produces two half-finished webhooks.

pgvector covers the large majority of application search, and keeping embeddings beside the rows they describe removes an entire class of sync bugs. Choose HNSW over IVFFlat unless index build time genuinely dominates, and measure recall against a labelled question set rather than judging it by feel. A dedicated store earns its place past tens of millions of vectors.

We quote fixed scope against the published rate card once we understand the schema and the policy surface. Platforms start at $28,000, MVPs at $24,000, and the minimum for a build is $7,500. Most projects land between $18,000 and $45,000. Those figures are estimates in US dollars, not quotations; the quote that follows scoping is fixed in writing before kickoff. What moves the number is table count, how many roles the policies encode, and whether data is migrating in.

Send the schema. Get the policy surface back first.

Tell us who’s allowed to see what and we’ll map it to policies before we quote. Fixed scope, fixed price, start date within two working days. Email edgebrainstudios@gmail.com or call the number below.