Building Scalable SaaS Architecture with Next.js
A comprehensive guide to building production-ready SaaS applications with Next.js, covering authentication, multi-tenancy, billing, and deployment strategies.

Building Scalable SaaS Architecture with Next.js
Building a SaaS application requires careful consideration of architecture, scalability, and user experience. In this comprehensive guide, we'll walk through the essential components of building a production-ready SaaS with Next.js.
Core Architecture Overview
A modern SaaS application typically consists of several key layers:
┌─────────────────────────────────────────┐
│ Presentation Layer │
│ (Next.js App Router + UI) │
├─────────────────────────────────────────┤
│ Application Layer │
│ (API Routes, Server Actions, RPC) │
├─────────────────────────────────────────┤
│ Business Logic │
│ (Services, Validation, Rules) │
├─────────────────────────────────────────┤
│ Data Layer │
│ (Database, Cache, External APIs) │
└─────────────────────────────────────────┘
Project Structure
Here's a recommended project structure for a Next.js SaaS:
src/
├── app/
│ ├── (auth)/
│ │ ├── login/
│ │ └── register/
│ ├── (dashboard)/
│ │ ├── layout.tsx
│ │ ├── overview/
│ │ ├── settings/
│ │ └── billing/
│ ├── (marketing)/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── pricing/
│ └── api/
│ ├── webhooks/
│ └── [...]/
├── components/
│ ├── ui/
│ ├── forms/
│ └── layouts/
├── lib/
│ ├── auth/
│ ├── billing/
│ ├── db/
│ └── utils/
└── services/
├── user.service.ts
├── team.service.ts
└── billing.service.ts
Authentication Strategy
For SaaS applications, we recommend using a combination of:
- Session-based auth for web applications
- JWT tokens for API access
- OAuth for third-party integrations
Implementation with NextAuth.js
// lib/auth/config.ts
import { NextAuthConfig } from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
import Google from 'next-auth/providers/google';
import { verifyPassword } from './password';
import { getUserByEmail } from '@/services/user.service';
export const authConfig: NextAuthConfig = {
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
Credentials({
async authorize(credentials) {
const { email, password } = credentials;
const user = await getUserByEmail(email as string);
if (!user || !verifyPassword(password as string, user.passwordHash)) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
};
},
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role;
token.teamId = user.teamId;
}
return token;
},
async session({ session, token }) {
session.user.role = token.role;
session.user.teamId = token.teamId;
return session;
},
},
};
Multi-Tenancy Patterns
SaaS applications typically need to support multiple organizations (tenants). Here are the common patterns:
1. Database-per-Tenant (Most Isolated)
// lib/db/tenant.ts
export function getTenantDatabase(tenantId: string) {
return new PrismaClient({
datasources: {
db: {
url: `postgresql://user:pass@host/${tenantId}`,
},
},
});
}
2. Schema-per-Tenant (Balanced)
// Using Prisma with schema-per-tenant
export async function withTenantSchema<T>(
tenantId: string,
operation: (prisma: PrismaClient) => Promise<T>
): Promise<T> {
const prisma = new PrismaClient();
await prisma.$executeRawUnsafe(`SET search_path TO "${tenantId}"`);
try {
return await operation(prisma);
} finally {
await prisma.$disconnect();
}
}
3. Row-Level Security (Most Common)
// All queries include tenantId
export async function getProjects(teamId: string) {
return prisma.project.findMany({
where: { teamId }, // Row-level filtering
include: {
members: true,
tasks: true,
},
});
}
Billing Integration
Most SaaS applications use Stripe for billing. Here's a robust implementation:
// lib/billing/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export async function createCheckoutSession(
teamId: string,
priceId: string,
userId: string
) {
const team = await getTeam(teamId);
let customerId = team.stripeCustomerId;
if (!customerId) {
const customer = await stripe.customers.create({
email: team.billingEmail,
metadata: { teamId },
});
customerId = customer.id;
await updateTeamStripeCustomerId(teamId, customerId);
}
return stripe.checkout.sessions.create({
customer: customerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/dashboard/billing?success=true`,
cancel_url: `${process.env.APP_URL}/dashboard/billing?canceled=true`,
metadata: { teamId, userId },
});
}
Rate Limiting
Protect your API with rate limiting:
// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '10 s'),
analytics: true,
});
export async function rateLimitMiddleware(identifier: string) {
const { success, limit, reset, remaining } = await ratelimit.limit(identifier);
return {
success,
headers: {
'X-RateLimit-Limit': limit.toString(),
'X-RateLimit-Remaining': remaining.toString(),
'X-RateLimit-Reset': reset.toString(),
},
};
}
Performance Optimization
Caching Strategy
// lib/cache/index.ts
import { unstable_cache } from 'next/cache';
export const getTeamWithCache = unstable_cache(
async (teamId: string) => {
return prisma.team.findUnique({
where: { id: teamId },
include: { members: true, subscription: true },
});
},
['team'],
{ revalidate: 300, tags: ['team'] }
);
Database Query Optimization
// Efficient pagination
export async function getProjectsPaginated(
teamId: string,
cursor?: string,
limit = 20
) {
return prisma.project.findMany({
where: { teamId },
take: limit + 1,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { createdAt: 'desc' },
select: {
id: true,
name: true,
status: true,
createdAt: true,
_count: { select: { tasks: true } },
},
});
}
Deployment Checklist
Before going to production, ensure you have:
- Environment variables properly configured
- Database migrations applied
- SSL certificates configured
- Rate limiting enabled
- Error monitoring (Sentry, etc.)
- Log aggregation (Axiom, etc.)
- Automated backups
- Load testing completed
- Security audit performed
Conclusion
Building a SaaS with Next.js provides an excellent foundation for scalable, performant applications. Key takeaways:
- Start with a solid architecture - Plan your data model and API structure carefully
- Implement authentication early - Security should be built-in, not bolted-on
- Choose the right multi-tenancy model - Based on your isolation and scaling needs
- Integrate billing from day one - It's harder to add later
- Monitor and optimize - Use proper observability tools
Need help building your SaaS? Contact Ananas Studio for a consultation.
Ananas Studio
Software Development Team
Building modern software solutions at Ananas Studio. We specialize in React, Next.js, and scalable architectures.

