DEV Community

Cover image for Deep Dive: Handling Multi-Tenancy Subdomains and DB Isolation without breaking Next.js 16
Alejandro Olivar
Alejandro Olivar

Posted on

Deep Dive: Handling Multi-Tenancy Subdomains and DB Isolation without breaking Next.js 16

The Boring Portfolio Problem

Let's be honest. Most developer portfolios look exactly the same: a clean minimalist template, a grid of generic projects, and a bulleted list of tech stacks.

When I built my portfolio, I wanted to showcase real-world production engineering. Instead of pushing 10 tiny code sandboxes, I decided to focus on deep-dive case studies.

This is the breakdown of Äbasto, a full-stack, multi-tenant B2B SaaS for warehouse and POS management that handles data isolation, dynamic subdomains, and an automated grace-period subscription model under a single pnpm workspace monorepo.

🏗️ The Architecture Stack

he project is structured as a scalable monorepo using pnpm workspaces:

  • Frontend: Next.js 16 App Router, Zustand (state persistence), and Tailwind CSS v4.

  • Backend: NestJS 11, TypeORM, and PostgreSQL.

🛡️ Challenge 1: Tenant Data Isolation at DB Level (PostgreSQL RLS)

When building a B2B SaaS where multiple independent warehouses manage inventory, global multi-tenancy leaks are your worst nightmare. Adding WHERE warehouse_id = X to every database query is prone to human error and scale bugs.

The Solution: Row-Level Security (RLS)
I delegated data isolation directly to PostgreSQL using Row-Level Security.

Every database transaction runs securely isolated. A custom JwtAuthGuard in NestJS intercepts the request, decodes the tenant data, and injects session variables directly using SQL SET LOCAL commands:

// A high-level view of injecting session context dynamically
async function injectTenantContext(queryRunner: QueryRunner, warehouseId: string) {
  // Safe runtime execution within the request transaction block
  await queryRunner.query(`SET LOCAL app.current_warehouse_id = '${warehouseId}'`);
}
Enter fullscreen mode Exit fullscreen mode

In the DB layer, tables enforce isolation natively:

ALTER TABLE inventory ENABLE ROW LEVEL SECURITY;

CREATE POLICY warehouse_isolation_policy ON inventory
    USING (warehouse_id = NULLIF(current_setting('app.current_warehouse_id', true), ''));
Enter fullscreen mode Exit fullscreen mode

This means even if a developer forgets to filter by warehouse in a frontend component, PostgreSQL will completely block cross-tenant data leaks.

🌐 Challenge 2: Dynamic Multi-Tenant Subdomains in Next.js 16

wanted every warehouse owner to have their own distinct subdomain (e.g., my-store.lvh.me:3000).

The Solution: Dynamic Rewrite Proxy
Instead of cluttering the system with a heavy middleware.ts, I utilized a specialized server-side proxy.ts execution block in Next.js 16. It dynamically reads the Host header and rewrites paths directly:

export function handleSubdomainRewrite(requestHeaders: Headers) {
  const host = requestHeaders.get('host'); // e.g., 'bodega-x.lvh.me:3000'
  const subdomain = host.split('.')[0];

  // Bypass reserved internal system paths natively
  if (subdomain === 'admin' || subdomain === 'www') {
    return null; 
  }

  // Perform an internal server rewrite to the dynamic store template
  return `/store/${subdomain}`;
}
Enter fullscreen mode Exit fullscreen mode

The Catch: Server-Side Token Security
o prevent identity spoofing, the proxy reads the token cookie (configured with a root domain scope domain=.lvh.me), decodes the JWT payload on the server side, and natively verifies if the token's authorized warehouse matches the requested subdomain. If there is a mismatch, it triggers an immediate rewrite redirect to /no-access.

⏳ Challenge 3: Automated Lock-Out & Subscription Engine

A true SaaS needs to handle monetization and enforcement without blocking access to historical data arbitrarily. I designed a customized multi-state subscription model with an embedded 3-day grace period.

Active State -> [Expiration Date] -> 3-Day Grace Period (Banners) -> Fully Locked POS Screen
Enter fullscreen mode Exit fullscreen mode

The Backend Enforcement Guard
We built a centralized SubscriptionGuard applied globally to all mutable endpoints (POST, PATCH, DELETE) across the products, inventory, and supplier components:

@Injectable()
export class SubscriptionGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req = context.switchToHttp().getRequest();
    const { expiresAt, gracePeriodDays } = req.user; // Appended by auth verification

    const absoluteDeadline = new Date(expiresAt);
    absoluteDeadline.setDate(absoluteDeadline.getDate() + (gracePeriodDays || 3));

    if (new Date() > absoluteDeadline) {
      throw new ForbiddenException('Subscription completely expired. Write operations locked.');
    }
    return true; // GET endpoints remain open cleanly
  }
}
Enter fullscreen mode Exit fullscreen mode

The Frontend Reaction Flow

  • Within 5 days of expiration: A contextual Amber SubscriptionBanner pops up in the Dashboard.

  • During Grace Period: An Orange warning stays fixed.

  • Past Grace Period: A full-screen SubscriptionLock overlay takes over the POS component with a pre-configured WhatsApp manual link (wa.me) leveraging a centralized support module to handle instant payment updates.

📨 Challenge 4: Transactional Communications via Resend & Manual WhatsApp Fallbacks

To ensure smooth operational communication without overhead costs, I implemented a hybrid Dual-Channel Notification System:

1. Channel A (Automated Transacational Mail): A global NotificationsModule hooks into backend services using the Resend SDK. It triggers beautifully designed, dark Neobrutalist HTML templates on critical events:

  • sendWelcomeEmail: Sends temporary credentials and system subdomain links immediately upon setup.

  • sendSubscriptionExpiringEmail: Triggered daily at noon via a NestJS @nestjs/schedule CRON job with clean in-memory de-duplication (Set).

  1. Channel B (Manual WhatsApp Flows): For payment tracking, the SuperAdmin dashboard incorporates manual communication helpers that parse tenant states dynamically into contextual text reminders, generating a frictionless single-click chat initiation link.

🧠 Key Takeaways

Building Äbasto proved that your portfolio doesn't need to be an archive of 20 unmaintained projects. Dedicating your space to full-scale engineering breakdowns showcases:

  • Deep comprehension of DB performance and security models.

  • Familiarity with server-side network engineering architecture (proxies, domain parsing).

  • Product mindset implementation (subscription gates, user retention design).

What does your current portfolio project stack look like? Are you team Single-DB isolation or separated clusters? Let's discuss in the comments below!

Top comments (0)