Skip to main content

Overview

Each seller gets their own branded subdomain on the platform:
seller1.mypopup.shop → Seller 1's Store
seller2.mypopup.shop → Seller 2's Store
shop123.mypopup.shop → Different Store
This is handled via Next.js middleware that detects the subdomain and routes to the appropriate store data.

How It Works

1. Middleware Detection

The middleware runs on every request and extracts the subdomain:
middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
  const hostname = req.headers.get('host') || '';
  const subdomain = hostname.split('.')[0];

  // Skip for main domain
  if (subdomain === 'mypopup' || subdomain === 'www') {
    return NextResponse.next();
  }

  // Fetch store by subdomain
  const store = await getStoreBySubdomain(subdomain);

  if (!store) {
    return NextResponse.rewrite(new URL('/404', req.url));
  }

  // Rewrite to dynamic route with store context
  const url = req.nextUrl.clone();
  url.searchParams.set('storeId', store.id);

  return NextResponse.rewrite(url);
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
};

2. Database Lookup

Supabase query to find store by subdomain:
SELECT
  id,
  subdomain,
  owner_id,
  business_name,
  brand_color,
  logo_url
FROM stores
WHERE subdomain = $1
  AND status = 'active'
LIMIT 1;

3. Store Context

The store data is passed to pages via URL params or React Context:
app/layout.tsx
import { StoreProvider } from '@/contexts/store-context';

export default async function RootLayout({
  children,
  searchParams
}: {
  children: React.ReactNode;
  searchParams: { storeId?: string };
}) {
  const store = await getStore(searchParams.storeId);

  return (
    <StoreProvider store={store}>
      {children}
    </StoreProvider>
  );
}

Local Development

Hosts File Setup

To test subdomains locally, add entries to /etc/hosts:
sudo nano /etc/hosts
Add these lines:
127.0.0.1   teststore.localhost
127.0.0.1   myshop.localhost
127.0.0.1   demo.localhost

Access Stores

npm run dev
Then visit:
  • http://teststore.localhost:3000 → Test Store
  • http://myshop.localhost:3000 → My Shop
  • http://demo.localhost:3000 → Demo Store
Note: Use .localhost (not just localhost) for subdomain routing to work locally.

Production Setup

DNS Configuration

Wildcard DNS Record:
Type: A
Name: *
Value: [Your Server IP]
TTL: Auto
Or with Cloudflare:
Type: CNAME
Name: *
Value: your-app.pages.dev
Proxied: ✅

Cloudflare Pages

Cloudflare automatically supports wildcard subdomains with Pages:
  1. Deploy to Cloudflare Pages
  2. Add custom domain: mypopup.shop
  3. Wildcard *.mypopup.shop works automatically ✅
Some hosting providers don’t support wildcard subdomains on free tiers. Cloudflare Pages does!

Subdomain Validation

Rules

Subdomains must:
  • Be 3-30 characters long
  • Contain only lowercase letters, numbers, and hyphens
  • Start with a letter or number
  • Not start or end with a hyphen
  • Be unique across all stores
export function isValidSubdomain(subdomain: string): boolean {
  const regex = /^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/;

  if (!regex.test(subdomain)) {
    return false;
  }

  // Reserved subdomains
  const reserved = ['www', 'api', 'admin', 'blog', 'docs', 'app'];
  if (reserved.includes(subdomain)) {
    return false;
  }

  return true;
}

Reserved Subdomains

These are not available for stores:
  • www, api, admin, app
  • blog, docs, help, support
  • mail, email, smtp, ftp
  • cdn, static, assets
  • test, staging, dev

Multi-Tenant Data Isolation

Database Queries

All queries must filter by store_id:
// ✅ CORRECT - Filtered by store
const products = await supabase
  .from('products')
  .select('*')
  .eq('store_id', storeId);

// ❌ WRONG - Returns ALL products from ALL stores!
const products = await supabase
  .from('products')
  .select('*');

Row-Level Security (RLS)

Supabase RLS policies enforce data isolation:
-- Products table RLS
CREATE POLICY "Users view own store products"
  ON products FOR SELECT
  USING (
    store_id IN (
      SELECT id FROM stores
      WHERE owner_id = auth.uid()
    )
  );

-- Public can view products from any active store
CREATE POLICY "Public view active store products"
  ON products FOR SELECT
  USING (
    store_id IN (
      SELECT id FROM stores
      WHERE status = 'active'
    )
  );

Performance Optimization

Caching Strategy

import { unstable_cache } from 'next/cache';

export const getStoreBySubdomain = unstable_cache(
  async (subdomain: string) => {
    const { data } = await supabase
      .from('stores')
      .select('*')
      .eq('subdomain', subdomain)
      .single();

    return data;
  },
  ['store-by-subdomain'],
  { revalidate: 60 } // Cache for 60 seconds
);

Edge Middleware

Middleware runs on the edge (Cloudflare/Vercel), making subdomain detection blazing fast:
User → Edge (detects subdomain) → Origin (serves store)
     ↑                           ↑
   ~10ms                       ~50ms

Troubleshooting

Make sure:
  1. You added the subdomain to /etc/hosts
  2. You’re using .localhost (e.g., test.localhost:3000)
  3. Middleware is enabled in middleware.ts
  4. Clear browser cache
Check:
  1. Wildcard DNS record is set (*.mypopup.shop)
  2. Store exists in database with that subdomain
  3. Store status is active
  4. Cloudflare proxy is enabled (orange cloud)
This is a critical bug - ensure:
  1. All database queries filter by store_id
  2. RLS policies are enabled
  3. Store context is passed correctly
  4. No caching issues (clear Redis/cache)

Example Flow


Next Steps

Learn how payments are processed per store