I built a production-ready SaaS boilerplate - here's what I learned
Every SaaS boilerplate I tried had the same problem: auth worked, but multi-tenancy was fake. You know what I mean. The "multi-tenant" feature was just a userId filter in every query. No data isolation. No Row-Level Security. Just hope that no one forgets a WHERE clause. After building my third SaaS from scratch and copy-pasting the same auth, team management, and permissions code again, I decided to extract it properly. Not another todo app with Stripe - but the actual foundation I use in production. Today I'm open-sourcing it: saas-root. The Problem with Existing Boilerplates I've tried at least 10 SaaS boilerplates. Most fall into two categories: Category 1: Too Simple Basic auth with Supabase/NextAuth Single users table "Add your own multi-tenancy" (thanks, that's the hard part) Category 2: Too Complex 50+ tables on day one Kubernetes deployment config Enterprise features you'll never use $299 price tag What I actually needed was something in between: production-ready foundations without the bloat. What "Production-Ready" Actually Means After shipping real products, I learned that a production-ready foundation needs: 1. True Data Isolation (RLS) Not filters in your application code. Database-level security. Here's how I set up Row-Level Security for organizations: -- Enable RLS ALTER TABLE organizations ENABLE ROW LEVEL SECURITY; -- Users can only see organizations they belong to CREATE POLICY "Users can view their organizations" ON organizations FOR SELECT USING ( EXISTS ( SELECT 1 FROM organization_memberships WHERE organization_memberships.organization_id = organizations.id AND organization_memberships.user_id = auth.uid() ) ); -- Only owners can update organization settings CREATE POLICY "Owners can update organization" ON organizations FOR UPDATE USING ( EXISTS ( SELECT 1 FROM organization_memberships WHERE organization_memberships.organization_id = organizations.id AND organization_memberships.user_id = auth.uid() AND organization_memberships.role = 'owner' ) ); This runs at the database level. Even if your application code has a bug, the wrong user cannot see another organization's data. Period. 2. Authorization Beyond RBAC Simple role-based access (owner/admin/member) works for 80% of cases. But what about: "Members can only edit their own tasks" "Admins can invite, but not delete members" "Only the creator can archive a project" I use CASL for this. It lets you define granular permissions: // Define what each role can do const ownerAbilities = [ { action: 'manage', subject: 'Organization' }, { action: 'manage', subject: 'Member' }, { action: 'manage', subject: 'Project' }, ]; const memberAbilities = [ { action: 'read', subject: 'Organization' }, { action: 'read', subject: 'Member' }, { action: 'read', subject: 'Project' }, { action: ['create', 'update', 'delete'], subject: 'Task', conditions: { createdBy: '${user.id}' } }, ]; The conditions field is powerful - members can only modify tasks they created. This is evaluated at runtime and works with your existing queries. 3. Provider Abstraction Hardcoding Resend into your email service? What happens when you need to switch to SendGrid or AWS SES? I learned this the hard way. Now I use interfaces: export abstract class EmailProvider { abstract readonly name: string; abstract send(params: SendEmailParams): Promise; abstract sendTemplate(params: SendTemplateParams): Promise; } Switching providers means implementing this interface - not rewriting your entire email flow. Currently saas-root ships with: Resend - My default for simplicity SMTP - For self-hosted setups Console - For development (logs emails to terminal) Same pattern works for payments, file storage, push notifications. Technical Decisions Why NestJS + Next.js? Backend (NestJS): Structured architecture that scales Dependency injection out of the box Easy to test TypeScript native Frontend (Next.js 16): App Router with Server Components The React framework everyone knows Great deployment story (Vercel, self-hosted) Could you use Express? Sure. But you'll rebuild NestJS patterns anyway. Why Drizzle over Prisma? I used Prisma for years. Drizzle wins for me because: SQL-like syntax - If you know SQL, you know Drizzle No runtime engine - Just generates SQL Better type inference - Less as casting Faster migrations - drizzle-kit push is instant // Drizzle: Feels like writing SQL const members = await db .select() .from(memberships) .where(eq(memberships.organizationId, orgId)) .leftJoin(profiles, eq(memberships.userId, profiles.id)); Why Supabase? Supabase gives you: PostgreSQL with RLS built-in Auth with OAuth providers Realtime subscriptions Storage All with a generous free tier. For indie hackers building MVPs, it's the obvious choice. What's Included Feature Description Authentication Supabase Auth (email, Google, GitHub) Multi-tenancy Organizations with RLS Authorization CASL with roles + custom permissions Team Management Invitations, role changes, member list Email System Provider abstraction + templates Feature Flags Plan-based gating Projects & Tasks Example CRUD with soft delete UI Components 30+ shadcn/ui components Quick Start # Clone git clone https://github.com/ohenriquesilvar/saas-root cd saas-root # Install cd backend && npm install cd ../frontend && npm install # Start Supabase (local) supabase start # Configure env cp backend/.env.example backend/.env cp frontend/.env.example frontend/.env.local # Run migrations & start cd backend && npm run db:push && npm run start:dev cd ../frontend && npm run dev Visit http://localhost:3000. Register, create an organization, invite team members. What's NOT Included (By Design) Billing/Stripe - Different for every business model Background Jobs - Use BullMQ, Trigger.dev, or whatever fits Docker/K8s - Start simple, add when needed CI/CD - Every team has preferences These are features you add when you need them. The boilerplate gives you the foundation. Why Open Source? I've been building in public for a while. The indie hacker community has taught me a lot. This is my way of giving back. MIT license. No catch. Use it for your startup, client projects, whatever. If it saves you a few weeks, maybe drop a star on GitHub. That's all I ask. Links: GitHub: saas-root Full Documentation What features would you add? Let me know in the comments.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to