Building a Centralized Action for Large-Scale SaaS with Next.js

Published: (December 4, 2025 at 03:49 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Architecture Overview

In large SaaS projects, handling server actions individually in every module creates code duplication.
The centralized mutation engine consolidates all API mutations into one reusable function. It handles authentication, authorization, shop/subdomain context, request payloads, fetch execution, and cache revalidation.

Core Utilities

  • logger – Structured logging for debugging and monitoring.
  • resolveModelName – Converts route names like saved-attributesaved_attribute.
  • hasPermission & parseAccessRules – Implements RBAC to check if a user can perform a specific action.
  • getBaseUrlWithSubdomain – Resolves the correct API base URL based on the shop subdomain.

Authentication Handling

if (requireAuth) {
  const accessToken = cookieStore.get("accessToken")?.value;
  if (!accessToken) {
    return { message: "You are not authorized to perform this action!" };
  }
  // Validate JWT access token...
}

Shop / Subdomain Context

if (requireShopId) {
  const subdomain = cookieStore.get("shopSubdomain")?.value;
  if (!subdomain) {
    return { message: "Shop identifier not found!" };
  }
  // Use subdomain for multi‑tenant routing...
}

Role‑Based Access Control (RBAC)

const rulesRaw = cookieStore.get("accessRules")?.value;
const rules = parseAccessRules(rulesRaw);
const model = resolveModelName(route.split("/")[1]);
const action = methodToAction[method];

if (!hasPermission(rules, model, action)) {
  return { success: false, message: "Permission denied" };
}

Maps HTTP methods → CRUD actions.

Dynamic Payload Handling

let body: string | undefined = undefined;

if (method === "DELETE" && ids?.length) {
  body = JSON.stringify({ ids });
} else if (method !== "DELETE" && data) {
  body = data;
}

Supports single‑ID deletion, bulk deletion, and POST/PUT payloads.

Fetch Execution

const response = await fetch(url, {
  method,
  headers,
  body,
  cache: "no-store",
});
const result = await response.json();

Executes server‑side mutations with fresh data (no-store).

Cache Revalidation

paths.forEach(path => typeof path === "string" && revalidatePath(path));
tags.forEach(tag => typeof tag === "string" && revalidateTag(tag));

Automatically revalidates Next.js paths or tags after a mutation.

Error Handling & Logging

try {
  // ...mutation logic...
} catch (error) {
  logger(error);
  return { success: false, message: "An error occurred during the mutation." };
}

Standardizes error handling.

Benefits

  • Reusability – Single mutation handler for all models.
  • Security – Global RBAC enforcement.
  • Multi‑Tenant Ready – Handles shop‑specific subdomains.
  • Consistency – Unified logging, error handling, and cache revalidation.
  • Scalability – Easy to extend as the SaaS grows.

Usage Example

await mutation({
  route: "/customer",
  method: "POST",
  data: JSON.stringify({ name: "John Doe" }),
  pathToRevalidate: "/customers",
  tagsToRevalidate: ["customer‑list"],
});

Handles authentication, RBAC, payload, and cache invalidation in one call.

Flow Diagram

┌────────────────────┐
│  Client / Frontend │
│ (React / Next.js)  │
└─────────┬──────────┘


┌────────────────────┐
│  Server Action Call│
│   mutation(params) │
└─────────┬──────────┘

 ┌────────┴─────────┐
 │                  │
 ▼                  ▼
┌───────────────┐ ┌───────────────┐
│ AUTHENTICATION│ │ SHOP / SUBDOMAIN│
│ check JWT token│ │ get shop identifier│
│ from cookies   │ │ from cookies      │
└───────┬───────┘ └───────┬───────┘
        │                 │
        └───────┬─────────┘

        ┌─────────────────────┐
        │ ROLE‑BASED ACCESS   │
        │ CONTROL (RBAC)      │
        │ hasPermission()     │
        │ parseAccessRules()  │
        └───────┬─────────────┘

        ┌─────────────────────┐
        │ Construct Headers   │
        │ - Authorization     │
        │ - x‑app‑identifier   │
        └───────┬─────────────┘

        ┌─────────────────────┐
        │ Determine Payload   │
        │ - DELETE ids array  │
        │ - POST / PUT data   │
        └───────┬─────────────┘

        ┌─────────────────────┐
        │ FETCH Request       │
        │ method, headers,    │
        │ body                │
        └───────┬─────────────┘

        ┌─────────────────────┐
        │ Response Handling   │
        │ - parse JSON        │
        │ - log errors        │
        └───────┬─────────────┘

   ┌───────────────┬───────────────┐
   ▼               ▼               ▼
┌───────┐     ┌───────┐        ┌───────┐
│ PATH  │     │ TAG   │        │ CLIENT│
│ REVAL │     │ REVAL │        │ UI    │
└───────┘     └───────┘        └───────┘

Takeaways

  • Centralized server actions reduce boilerplate and improve maintainability.
  • RBAC ensures consistent security across all API mutations.
  • Subdomain‑aware multi‑tenant architecture supports SaaS scalability.
  • Automatic cache revalidation guarantees the UI always displays fresh data.

Implementing a centralized mutation engine in a large‑scale SaaS project dramatically improves code maintainability, security, and scalability. By consolidating authentication, RBAC, multi‑tenant subdomain handling, payload management, and cache revalidation into a single server action, developers can:

  • Eliminate repetitive code across multiple endpoints.
  • Enforce consistent security with dynamic permission checks.
  • Support multi‑tenant architectures efficiently via subdomain‑aware API calls.
  • Ensure UI consistency by automatically revalidating Next.js paths and tags.
  • Scale easily as the SaaS grows, adding new models and actions with minimal friction.
Back to Blog

Related posts

Read more »

RCE Vulnerability in React and Next.js

Article URL: https://github.com/vercel/next.js/security/advisories/GHSA-9qr9-h5gf-34mp Comments URL: https://news.ycombinator.com/item?id=46136026 Points: 26 Co...