Architect and deploy a production-grade Micro-SaaS platform with Next.js 16, Better-Auth, Drizzle ORM, and multi-tenant security controls.
Learn how to build zero-trust FastMCP servers in Python and harden Next.js 16 AI agent architectures against indirect prompt injection, tool poisoning, and unauthorized tool calls (2026 Maste
25 min read
TypeScript, Next.js App Router, SQL databases.
Next.js 16, Better-Auth, Drizzle ORM, Node.js 20+
Master Better-Auth setup, Next.js 16 edge session middleware, Drizzle ORM multi-tenant schemas, and BOLA/IDOR protection.
Building a scalable, production-ready Micro-SaaS platform in 2026 requires more than basic user authentication. Modern platforms demand zero-trust multi-tenancy, high-performance edge session validation, strict CSRF/CORS protections, and resilient database access control.
This masterclass details how to architect and deploy a production-grade Micro-SaaS engine using Next.js 16 App Router, Better-Auth (the premier modern TypeScript authentication framework), Drizzle ORM / SQLite / PostgreSQL, and custom edge middleware rate-limiting.
To build a secure Micro-SaaS platform, we must start from first principles: Authentication is Identity Verification; Multi-Tenancy is Boundary Enforcement.
Imagine a high-end multi-tenant office building:
Legacy SaaS architectures historically relied on stateless JWTs stored in LocalStorage. This pattern exposes two severe failure modes:
Better-Auth resolves these trade-offs by utilizing httpOnly, SameSite=Lax/Strict session cookies paired with hyper-fast database session lookups, giving you absolute session control without compromising edge performance.
Below is the architectural blueprint governing client requests, Next.js 16 Edge Middleware, Better-Auth session evaluation, and multi-tenant data access.
[ Client Browser ]
|
| 1. HTTPS Request + Cookie (better-auth.session_token)
v
+-------------------------------------------------------------------+
| Next.js 16 Edge Middleware (middleware.ts) |
| - Rate Limiter (Token Bucket algorithm) |
| - Fast Session Header Extraction |
+-------------------------------------------------------------------+
|
| 2. Pass request if within rate limit
v
+-------------------------------------------------------------------+
| App Router Server Components & API Routes (/api/...) |
| |
| +---------------------------------------------------------------+ |
| | Better-Auth Handler (auth.api.getSession) | |
| | - Queries Database / In-Memory Session Cache | |
| | - Validates Organization Membership & Active Status | |
| +---------------------------------------------------------------+ |
+-------------------------------------------------------------------+
|
| 3. Scoped Query (WHERE tenant_id = active_org_id)
v
+-------------------------------------------------------------------+
| PostgreSQL / SQLite Database (Drizzle ORM Schema) |
| Tables: users, sessions, accounts, organizations, members, data |
+-------------------------------------------------------------------+
A robust schema must enforce foreign key integrity and isolate tenant data at the storage layer. Below is the full Drizzle ORM schema defining user accounts, session state, organization boundaries, and tenant-scoped application records.
// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
// Primary User Table
export const user = sqliteTable('user', {
id: text('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
emailVerified: integer('emailVerified', { mode: 'boolean' }).notNull(),
image: text('image'),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
});
// Database-Backed Session Table
export const session = sqliteTable('session', {
id: text('id').primaryKey(),
expiresAt: integer('expiresAt', { mode: 'timestamp' }).notNull(),
token: text('token').notNull().unique(),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
ipAddress: text('ipAddress'),
userAgent: text('userAgent'),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
activeOrganizationId: text('activeOrganizationId'),
});
// OAuth Accounts Table
export const account = sqliteTable('account', {
id: text('id').primaryKey(),
accountId: text('accountId').notNull(),
providerId: text('providerId').notNull(),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
accessToken: text('accessToken'),
refreshToken: text('refreshToken'),
idToken: text('idToken'),
accessTokenExpiresAt: integer('accessTokenExpiresAt', { mode: 'timestamp' }),
refreshTokenExpiresAt: integer('refreshTokenExpiresAt', { mode: 'timestamp' }),
scope: text('scope'),
password: text('password'),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
updatedAt: integer('updatedAt', { mode: 'timestamp' }).notNull(),
});
// Organization / Tenant Table
export const organization = sqliteTable('organization', {
id: text('id').primaryKey(),
name: text('name').notNull(),
slug: text('slug').unique(),
logo: text('logo'),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
metadata: text('metadata'),
});
// Tenant Membership & Access Control List
export const member = sqliteTable('member', {
id: text('id').primaryKey(),
organizationId: text('organizationId').notNull().references(() => organization.id, { onDelete: 'cascade' }),
userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }),
role: text('role').notNull(), // 'owner' | 'admin' | 'member'
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
});
// Tenant-Scoped SaaS Application Resource (e.g., API Keys or Projects)
export const project = sqliteTable('project', {
id: text('id').primaryKey(),
organizationId: text('organizationId').notNull().references(() => organization.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
apiKeyHash: text('apiKeyHash').notNull(),
createdAt: integer('createdAt', { mode: 'timestamp' }).notNull(),
});
Create the server-side auth configuration with full organization support enabled.
// src/lib/auth.ts
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { organization } from 'better-auth/plugins';
import { db } from '@/db';
import * as schema from '@/db/schema';
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: 'sqlite',
schema: schema,
}),
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
},
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID || '',
clientSecret: process.env.GITHUB_CLIENT_SECRET || '',
},
},
plugins: [
organization({
allowUserToCreateOrganization: true,
}),
],
advanced: {
useSecureCookies: process.env.NODE_ENV === 'production',
cookiePrefix: 'sentinel_saas',
},
});
Expose Better-Auth endpoints dynamically within the App Router directory structure.
// src/app/api/auth/[...all]/route.ts
import { auth } from '@/lib/auth';
import { toNextJsHandler } from 'better-auth/next-js';
export const { GET, POST } = toNextJsHandler(auth);
Initialize the reactive client SDK for Next.js client components.
// src/lib/auth-client.ts
import { createAuthClient } from 'better-auth/react';
import { organizationClient } from 'better-auth/client/plugins';
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',
plugins: [organizationClient()],
});
export const { useSession, signIn, signOut, signUp, useActiveOrganization } = authClient;
Protect sensitive SaaS dashboard routes at the edge before hitting rendering or server logic.
// src/middleware.ts
import { NextResponse, type NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
const sessionToken = request.cookies.get('sentinel_saas.session_token')?.value;
const { pathname } = request.nextUrl;
// 1. Protect Dashboard and Organization Routes
if (pathname.startsWith('/dashboard') || pathname.startsWith('/api/tenant')) {
if (!sessionToken) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/tenant/:path*'],
};
Every API route interacting with database entities MUST enforce organization scoping to eliminate BOLA / IDOR (Broken Object Level Authorization) vulnerabilities.
// src/app/api/tenant/projects/route.ts
import { NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { headers } from 'next/headers';
import { db } from '@/db';
import { project } from '@/db/schema';
import { eq, and } from 'drizzle-orm';
export async function GET(request: Request) {
// 1. Verify Active Session
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session || !session.session.activeOrganizationId) {
return NextResponse.json({ error: 'Unauthorized or No Active Organization Selected' }, { status: 401 });
}
const activeOrgId = session.session.activeOrganizationId;
// 2. Execute Strictly Scoped Query
const orgProjects = await db.select().from(project).where(eq(project.organizationId, activeOrgId));
return NextResponse.json({ success: true, projects: orgProjects });
}
When operating a Micro-SaaS in production, you must address real-world edge cases and potential attack vectors:
SameSite=Lax (or SameSite=Strict) on authentication cookies.POST, PUT, DELETE), ensure origin verification headers (Origin and Referer) match your domain in Next.js middleware.organization_id supplied in request payloads or URL parameters alone! Always cross-reference the client's session state (session.activeOrganizationId) against the database membership table before executing queries./api/auth/sign-in and /api/auth/sign-up to restrict attempts to 5 requests per minute per IP address.| Architectural Dimension | Legacy JWT in LocalStorage | NextAuth.js (v5) | Better-Auth (2026 Masterclass) |
|---|---|---|---|
| Session Storage | Client-Side Storage | Cookie / JWT Hybrid | Cookie + DB Session Table |
| Instant Revocation | Impossible without CRL | Delayed (Until Expiry) | Immediate (< 1ms DB Check) |
| Multi-Tenancy ACL | Manual Implementation | Requires Complex Callbacks | Native Plugin Support |
| XSS Resilience | Vulnerable (Exfiltrates Tokens) | High (httpOnly Cookie) | Maximum (httpOnly + Prefix) |
| Edge Compatibility | High | Partial | Full Next.js 16 App Router Support |
| TypeScript Support | Partial | Good | 100% Strictly Typed Schemas |
WHERE organization_id = activeOrgId) in database queries to permanently neutralize BOLA/IDOR risks.Authored by Syed Zada Abrar — Founder & Lead Researcher, Andrax Pentester.
Share this tutorial
Sign in to leave a comment.