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.
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.
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.
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.