All posts
Product design

Building a SaaS App With Next.js: Architecture Decisions That Matter Early

Building a Next.js SaaS app: the early architecture calls on auth, multi-tenancy, server actions, data fetching and caching, with honest trade-offs for each.

By Mehdi Hassan7 min read

The short answer

A Next.js SaaS app should be architected around five early decisions: where authentication and session checks live, how tenants are isolated in the database, which data is fetched in server components, when to use server actions versus API routes, and what gets cached. Get tenancy and auth right first, because they're the hardest to change later.

Key takeaways

  • Tenancy and authorization are the two decisions that are expensive to reverse. Settle them in week one.
  • Check permissions next to the data, not only in middleware or the UI.
  • Fetch in server components by default; reach for client fetching for live, interactive views.
  • Server actions suit your own forms and mutations; keep API routes for webhooks and external clients.
  • Treat caching as opt-in for a logged-in app, and make every cached value tenant-scoped.

Part of Full-Stack Product Engineering for Startups: From Figma to Production

How should you architect a Next.js SaaS app?

Architect a Next.js SaaS app by deciding, in this order: your tenancy model, your authorization rule, your data-fetching pattern, how mutations work, and your caching policy. Everything else, from component libraries to folder names, is cheap to change later. These five aren't.

This guide is about logged-in products with accounts, workspaces and dashboards, not marketing sites. It's part of our hub on full-stack product engineering for startups, and it assumes you've picked a stack. If you haven't, start with our SaaS tech stack guide, or check whether Next.js is the right framework at all in Next.js vs Remix vs Vite SPA.

Decision

Default we recommend

Cost to change later

Tenancy model

Shared database, tenant ID on every row

High

Authorization

Checked in a data-access layer

High

Data fetching

Server components first

Medium

Mutations

Server actions for your UI, routes for webhooks

Low

Caching

Off by default for user data, opt in per query

Medium

How do you handle multi-tenancy in a Next.js SaaS architecture?

For most early SaaS products, the right multi-tenancy model is a shared database where every tenant-owned row carries a workspace or organization ID. It's the simplest to run, query and migrate. Separate schemas or databases per tenant are worth it only when contracts or regulation demand physical isolation.

  • Shared tables with a tenant ID: one schema, one migration path. The risk is a query that forgets the filter, so enforce it centrally or with Postgres row-level security.
  • Schema per tenant: stronger separation, but every migration runs once per tenant and tooling gets harder.
  • Database per tenant: the strongest isolation and the heaviest operations. Usually an enterprise requirement, not a starting point.

Model users and workspaces as a many-to-many relationship from day one, even if every user has one workspace today. Retrofitting "a user can belong to two teams" is one of the most common rewrites we see. The UX side of this, including roles and invites, is covered in designing multi-tenant SaaS: workspaces, roles and permissions.

Where should authentication and authorization live?

Authentication should be handled by a proven library or provider, and authorization should be checked as close to the data as possible. Middleware is useful for redirecting signed-out users, but it shouldn't be your only permission check, because server components, server actions and route handlers can each be reached directly.

The pattern we use is a small data-access layer: every function that reads or writes tenant data takes the current session, checks membership and role, and only then queries. Pages and actions call those functions instead of the database directly. It's dull, and it's what keeps a bug in one page from becoming a data leak. Choosing the provider itself is a separate decision, and the official Next.js documentation describes the moving parts well.

What a data-access layer looks like in practice

Create one folder for data functions, for example getProjects, createInvoice and removeMember. Each one starts by reading the session, loading the user's membership for the requested workspace and checking the role against the action. If any check fails, it throws before the query runs.

Components never import the database client. That single rule makes permissions reviewable in one place, makes it easy to add audit logging later, and lets you test authorization without rendering a page. With TypeScript, the session and role types also stop a developer from calling a function without the context it needs.

How should a Next.js App Router SaaS fetch data?

In an App Router SaaS, fetch data in server components by default. The query runs on the server, close to the database, the result streams to the browser as HTML, and no API layer is needed for your own pages. Use client-side fetching for views that change while the user watches.

  • Server components: lists, detail pages, settings and anything that loads once per visit.
  • Client fetching with a cache library: live dashboards, filters that update instantly, infinite scroll and polling.
  • Streaming with Suspense: wrap slow widgets so the page shell appears immediately and each panel fills in when ready.
  • Parallel queries: start independent queries together rather than awaiting them one after another, which is the most common cause of slow pages.

The trade-off is mental overhead. Your team has to know which side of the server and client boundary each component runs on. Typing the whole stack, as we argue in TypeScript for startups, catches most mistakes at that boundary before they ship.

Should you use server actions or API routes in a Next JS SaaS app?

Use server actions for mutations triggered by your own UI, such as saving a form or inviting a teammate, and use route handlers for anything called from outside: webhooks, mobile apps, public APIs and integrations. Server actions remove boilerplate; route handlers give you a stable, versionable contract.

Pros

  • Server actions: no hand-written fetch calls or endpoint wiring for forms
  • Server actions: work with progressive enhancement and pending states
  • Server actions: keep the mutation next to the UI that uses it

Cons

  • Server actions are public endpoints, so each one needs its own auth and input validation
  • Not a fit for third-party callers or a future mobile app
  • Easy to scatter business logic across components unless actions call a shared data layer

Validate every action's input with a schema library and run the same authorization check you'd run in an API route. Treat an action as a function anyone on the internet can call, because effectively it is.

How should caching work in a logged-in SaaS app?

In a logged-in SaaS app, treat caching as opt-in. Most data is private and changes often, so the safe default is to fetch fresh data per request and cache deliberately where it's measured to help. Every cached value must include the tenant in its key, or one customer can see another's numbers.

Good candidates for caching are expensive aggregates such as monthly reports, reference data shared by all tenants and anything read far more than written. After a mutation, revalidate the specific paths or tags it affects. Check the current Next.js caching documentation for your version, because the defaults have changed between major releases.

What can wait until after launch?

Plenty can wait: microservices, a separate API server, event queues, a custom design system and multi-region deployment. A single Next.js app with one Postgres database carries most SaaS products well past their first paying customers. Add complexity when a measured problem demands it, not because a diagram looks more serious.

What can't wait is the backend choice, covered in Supabase vs Firebase vs custom Postgres, and a navigation structure that will hold as features grow, which we cover in information architecture for SaaS apps. Both shape every screen you'll build.

Frequently asked questions

Is Next.js good for SaaS dashboards or only for marketing sites?
Next.js works well for SaaS dashboards. Server components handle data-heavy pages, client components handle interactive widgets, and you can host the marketing site and the app in one codebase or two. The main cost is the learning curve of the server and client split, which a team new to the App Router should budget time for.
Should the marketing site and the app share one Next.js project?
It depends on who edits them. One project is simpler for a small engineering team. If marketers need to publish pages without engineers, keep the marketing site separate, in a visual builder or with a headless CMS, and run the app on its own subdomain so releases don't block each other.
Do you need Postgres row-level security with Next.js?
You don't need it, but it's a strong safety net. Row-level security makes the database itself refuse rows from other tenants, so a missing filter in application code doesn't become a leak. It adds some complexity to queries and testing, so many teams pair a data-access layer in code with row-level security on the most sensitive tables.
Can you add multi-tenancy to an existing single-tenant app?
Yes, but it touches every table and query. You add an organization model, backfill a tenant ID on existing rows, move users into memberships and update every read and write to filter by tenant. It's a planned migration, not a quick patch, which is why we recommend building it in before the first customer signs up.

· CEO & Design Director

Copywriter, designer and developer, and founder of Designify Studio. Over five years, 60+ projects across 14+ industries, for startups that have raised $300M+.

Want us to do this for you?

We take craft seriously over at Designify Studio. We do deep research and do what we do best - pay attention to the smallest details and make sure we speak to the right audience.

Schedule Call