Building a Full-Stack App with Next.js and Supabase
Pair the Next.js App Router with Supabase and you get a genuinely full-stack app with no backend server to manage: a hosted Postgres database, authentication, file storage, and an excellent TypeScript SDK. Here's how the pieces fit, and the pitfalls worth dodging up front.
Two clients, one rule
Supabase gives you a browser client and a server client. The rule that keeps you out of trouble:
Only the publishable/anon key ever touches the browser. The service-role key bypasses Row Level Security entirely — keep it server-side, never in a
NEXT_PUBLIC_variable.
Querying the database
From a Server Component you query straight against Postgres:
const supabase = await createClient();
const { data: posts, error } = await supabase
.from("posts")
.select("id, title, published_at")
.eq("status", "published")
.order("published_at", { ascending: false });
if (error) throw error;
Row Level Security is the backend
This is the part people skip and regret. Enable RLS on every table, then write policies. Authorization lives in the database, not scattered through your route handlers.
alter table posts enable row level security;
-- Public reads only published rows
create policy "public reads published"
on posts for select
using (status = 'published');
-- Authors write only their own rows
create policy "authors write own"
on posts for insert
with check (author_id = auth.uid());
With RLS in place, the anon key is safe on the client — the database enforces access no matter what the query asks for.
Storage tied to auth
A public bucket serves images by URL; policies gate who can upload.
- Public read, so covers render for everyone.
- Authenticated write, so only you can upload.
- Swap to an S3-compatible provider later without touching your app code if you isolate the upload logic.
The pitfalls, ranked
- Using the service-role key on the client. It ignores RLS. Never do it.
- Forgetting to refresh the session. Use middleware/proxy to refresh the auth cookie on every request, or users get logged out at random.
- Skipping RLS "just for dev." Disabling it and forgetting to turn it back on is a critical hole. Enable it before launch, every table.
Why it's a good fit
You trade a whole backend tier for a Postgres database that enforces its own rules, an auth system that issues real sessions, and storage that's one policy away from correct. For a solo developer shipping a real product, that's a lot of leverage for very little operational weight.