Back to Blog
Architecture·Next.jsSaaSTypeScript

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.

Ananas Studio
5 min read
Building Scalable SaaS Architecture with Next.js

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:

  1. Session-based auth for web applications
  2. JWT tokens for API access
  3. 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:

  1. Start with a solid architecture - Plan your data model and API structure carefully
  2. Implement authentication early - Security should be built-in, not bolted-on
  3. Choose the right multi-tenancy model - Based on your isolation and scaling needs
  4. Integrate billing from day one - It's harder to add later
  5. Monitor and optimize - Use proper observability tools

Need help building your SaaS? Contact Ananas Studio for a consultation.

Tags:Next.jsSaaSArchitectureTypeScriptFull Stack
A

Ananas Studio

Software Development Team

Building modern software solutions at Ananas Studio. We specialize in React, Next.js, and scalable architectures.

Related Articles

Continue reading with these related posts