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
userstable - "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<SendEmailResult>;
abstract sendTemplate<T>(params: SendTemplateParams<T>): Promise<SendEmailResult>;
}
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
ascasting -
Faster migrations -
drizzle-kit pushis 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:
What features would you add? Let me know in the comments.
Top comments (0)