Skip to content

[WIP] Add dynamic subdomain routing middleware for multi-tenancy#48

Closed
Copilot wants to merge 2 commits intomainfrom
copilot/implement-subdomain-routing
Closed

[WIP] Add dynamic subdomain routing middleware for multi-tenancy#48
Copilot wants to merge 2 commits intomainfrom
copilot/implement-subdomain-routing

Conversation

Copy link
Contributor

Copilot AI commented Nov 25, 2025

  • Analyze existing codebase structure and middleware
  • Review Prisma schema for Store model
  • Set up development environment
  • Update Prisma schema with subdomain and customDomain fields on Store model
  • Implement extended middleware for subdomain-based routing with caching
  • Create store layout and homepage routes (src/app/store/[slug]/*)
  • Create store-not-found page for invalid subdomains
  • Create ProductGrid component for storefront
  • Run type-check and lint validation
  • Build verification
Original prompt

This section details on the original issue you should resolve

<issue_title>[Phase 1] Dynamic Subdomain Routing</issue_title>
<issue_description>## Priority: P0 (Critical)
Phase: 1 - E-Commerce Core
Epic: Storefront
Estimate: 4 days
Type: Story

Context

Implement Next.js middleware for subdomain-based multi-tenancy routing (e.g., vendor1.stormcom.app, vendor2.stormcom.app) with custom domain support (vendor.com).

Acceptance Criteria

  • Middleware detects subdomain and loads corresponding store data
  • Store data available in all storefront routes (/store/[slug]/*)
  • Custom domain CNAME configuration works (DNS verification)
  • Proper 404 page for non-existent stores
  • Works locally with hosts file setup (vendor1.localhost:3000)
  • Subdomain rewriting preserves query parameters and hash fragments
  • Caching strategy implemented (10-minute store data TTL)

Technical Implementation

1. Middleware Extension (middleware.ts)

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { getToken } from 'next-auth/jwt';

// Existing NextAuth middleware export
export { default } from 'next-auth/middleware';

// Extend middleware to handle subdomains
export async function middleware(req: NextRequest) {
  const url = req.nextUrl;
  const hostname = req.headers.get('host') || '';

  // 1. Extract subdomain
  const subdomain = extractSubdomain(hostname);

  // 2. Skip subdomain logic for:
  // - Root domain (www.stormcom.app, stormcom.app)
  // - Admin routes (/dashboard, /api)
  // - Auth routes (/login, /signup)
  if (
    !subdomain ||
    subdomain === 'www' ||
    url.pathname.startsWith('/dashboard') ||
    url.pathname.startsWith('/api') ||
    url.pathname.startsWith('/login')
  ) {
    return NextResponse.next();
  }

  // 3. Load store by subdomain (with caching)
  const store = await getStoreBySubdomainOrDomain(subdomain, hostname);

  if (!store) {
    // Redirect to 404 for invalid subdomain
    return NextResponse.rewrite(new URL('/store-not-found', req.url));
  }

  // 4. Rewrite URL to store route with slug
  // vendor1.stormcom.app/products → /store/vendor1/products
  const storeUrl = new URL(`/store/${store.slug}${url.pathname}`, req.url);
  storeUrl.search = url.search; // Preserve query params

  // 5. Pass store data via headers
  const response = NextResponse.rewrite(storeUrl);
  response.headers.set('x-store-id', store.id);
  response.headers.set('x-store-slug', store.slug);
  response.headers.set('x-store-name', store.name);

  return response;
}

/**
 * Extract subdomain from hostname
 * Examples:
 * - vendor1.stormcom.app → vendor1
 * - vendor1.localhost → vendor1
 * - vendor.com → vendor (custom domain)
 */
function extractSubdomain(hostname: string): string | null {
  // Remove port if present
  const host = hostname.split(':')[0];

  // Development: vendor1.localhost
  if (host.endsWith('.localhost')) {
    return host.replace('.localhost', '');
  }

  // Production: vendor1.stormcom.app
  const parts = host.split('.');
  if (parts.length >= 3) {
    // vendor1.stormcom.app → vendor1
    return parts[0];
  }

  // Custom domain: vendor.com
  // Treat entire domain as subdomain
  if (parts.length === 2) {
    return parts[0];
  }

  return null;
}

/**
 * Get store by subdomain or custom domain
 * Implements caching to reduce database queries
 */
async function getStoreBySubdomainOrDomain(subdomain: string, hostname: string) {
  // Check cache first (10-minute TTL)
  const cacheKey = `store:${hostname}`;
  const cached = storeCache.get(cacheKey);
  if (cached) return cached;

  // Query database
  const store = await prisma.store.findFirst({
    where: {
      OR: [
        { subdomain: subdomain },
        { customDomain: hostname.split(':')[0] }
      ],
      deletedAt: null
    },
    select: {
      id: true,
      slug: true,
      name: true,
      subdomain: true,
      customDomain: true,
      organizationId: true
    }
  });

  if (store) {
    storeCache.set(cacheKey, store, 600); // 10 minutes
  }

  return store;
}

// Simple in-memory cache (use Redis in production)
class SimpleCache {
  private cache = new Map<string, { data: any; expires: number }>();

  set(key: string, data: any, ttl: number) {
    this.cache.set(key, {
      data,
      expires: Date.now() + (ttl * 1000)
    });
  }

  get(key: string) {
    const entry = this.cache.get(key);
    if (!entry) return null;
    if (Date.now() > entry.expires) {
      this.cache.delete(key);
      return null;
    }
    return entry.data;
  }
}

const storeCache = new SimpleCache();

// Protect admin routes (existing NextAuth config)
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/settings/:path*',
    '/team/:path*',
    '/projects/:path*',
    '/((?!api|_next/static|_next/image|favicon.ico).*)', // All other routes for subdomain handling
  ]
};

2. Store Routes (`src/app/...


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

@vercel
Copy link

vercel bot commented Nov 25, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
stormcomui Error Error Nov 25, 2025 2:52am

Co-authored-by: syed-reza98 <71028588+syed-reza98@users.noreply.github.com>
@AshrafAbir
Copy link
Contributor

@copilot start

@AshrafAbir AshrafAbir closed this Nov 27, 2025
@github-project-automation github-project-automation bot moved this from Backlog to Done in StormCom Nov 27, 2025
@AshrafAbir AshrafAbir deleted the copilot/implement-subdomain-routing branch November 27, 2025 13:18
@syed-reza98 syed-reza98 mentioned this pull request Feb 28, 2026
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Phase 1] Dynamic Subdomain Routing

3 participants