DEV Community

Gazzel16
Gazzel16

Posted on

Next.js + Supabase Starter Template (Prisma, Redux, Swagger)

Why I open-sourced this:

I kept rebuilding the same setup for every new project — Next.js API routes, Prisma, Supabase, folder structure, and deploy steps. It was repetitive, and I wasted time deciding where code should live instead of building features.

So I packaged the pattern I actually use into one repo: layered backend, Redux for CRUD, Swagger docs, and a clear path to Vercel + Supabase Cloud.

I open-sourced it so I (and anyone else) can clone → customize → ship without starting from scratch every time.

If it saves you even one weekend of wiring, it was worth sharing.

What's included:

  • Next.js 16 (App Router) + TypeScript + Tailwind
  • Supabase Postgres (local Docker for dev, cloud for prod)
  • Prisma 7 with layered API: route → controller → repository
  • Redux Toolkit for client state and CRUD thunks
  • OpenAPI + Swagger UI at /docs
  • Reference CRUD module (categories) — copy for new entities
  • Deploy to Vercel + Supabase Cloud — no separate backend server
  • Clone, customize your schema, and ship

If this template helps you, leave a like on this post and a star on the repo — it helps others find it and keeps me motivated to improve it.

Repo: https://github.com/Gazzel16/nextjs-supabase-starter

Thanks for reading!

Top comments (3)

Collapse
 
vollos profile image
Pon

I've cloned a handful of Supabase starters that turned out to be someone's folder preferences with a readme stapled on. This isn't that. The route to controller to repository split and the copy-me reference CRUD module are the parts that save the weekend you're promising. Nice work shipping it.

One thing I'd flag, specifically because it's a template other people will build on top of: the moment Prisma sits in front of Supabase, you're connecting straight to Postgres with a privileged connection string, which means row-level security never runs. RLS only fires on the PostgREST path with the anon or authenticated key, not on a direct Prisma connection. So in this architecture the entire question of "can this user touch this row" lives in your route and controller layer, not in the database. If the reference CRUD module is the thing everyone copies, it's worth making sure that pattern checks ownership on every read and write. Otherwise every entity someone generates from it inherits an IDOR by default: change the id in the request, get back someone else's record.

Repo's public, so if it's useful I'm happy to run my scan over it and share whatever it surfaces, no strings. I read a lot of Next.js and Supabase code, and the Prisma-versus-RLS gap is the one thing I'd want spelled out right in a template like this, so people copying it know where the authorization has to live.

Collapse
 
gazzel16 profile image
Gazzel16

Thanks for the great feedback on the architecture. You’re right about Prisma bypassing Supabase RLS.

I’ll update the reference module to enforce authorization right in the controller/repository layer by always filtering by userId to prevent IDORs.

I'd love for you to run that scan over the repo! Please feel free to open an issue or PR with whatever it finds, and any tips on calling this out in the README. Thanks for looking out!

Collapse
 
vollos profile image
Pon

Cloned it and read through, and the fix landed clean. Every categories query now scopes by user_id in the repository, findById/update/delete all filter on it, and the controller never trusts a user_id from the body. That closes the IDOR on the Prisma path.

There is a second door though, and it's one your own SECURITY.md half-points at. Your table says the Supabase Data API path (anon/auth key) is protected by RLS. But none of your migrations ever run ENABLE ROW LEVEL SECURITY or create a policy, and the Prisma tables land in the public schema. If the anon key reaches PostgREST — and that key ships in the browser bundle — public.user (emails) and public.categories are readable and writable straight over /rest/v1, skipping your controller entirely. The app-layer fix is correct but it only guards the door your API uses; the REST door is still open.

Easy to check on your own project: hit /rest/v1/user with just the anon key and see if rows come back. If they do, the fix is one line per table — enable RLS and add an owner policy (auth.uid() = user_id), or revoke the anon/authenticated grants entirely if you never use that path. Might be worth a line in the README too, since anyone copying the template inherits the same gap. Happy to share the exact SQL here if it helps. Nice work turning the first one around fast.