> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mypopup.shop/llms.txt
> Use this file to discover all available pages before exploring further.

# Subdomain Routing

> Multi-tenant subdomain architecture for per-seller storefronts

## 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:

```typescript middleware.ts theme={null}
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:

```sql theme={null}
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:

```typescript app/layout.tsx theme={null}
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`:

```bash theme={null}
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

```bash theme={null}
npm run dev
```

Then visit:

* `http://teststore.localhost:3000` → Test Store
* `http://myshop.localhost:3000` → My Shop
* `http://demo.localhost:3000` → Demo Store

<Info>
  **Note:** Use `.localhost` (not just `localhost`) for subdomain routing to work locally.
</Info>

***

## 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 ✅

<Warning>
  Some hosting providers don't support wildcard subdomains on free tiers. Cloudflare Pages does!
</Warning>

***

## 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

```typescript theme={null}
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`:

```typescript theme={null}
// ✅ 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:

```sql theme={null}
-- 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

```typescript theme={null}
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

<AccordionGroup>
  <Accordion title="Subdomain not working locally">
    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
  </Accordion>

  <Accordion title="404 on subdomain in production">
    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)
  </Accordion>

  <Accordion title="Wrong store data showing">
    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)
  </Accordion>
</AccordionGroup>

***

## Example Flow

```mermaid theme={null}
sequenceDiagram
    participant User
    participant Edge
    participant DB
    participant Page

    User->>Edge: Visit shop123.mypopup.shop
    Edge->>Edge: Extract subdomain: "shop123"
    Edge->>DB: SELECT * FROM stores WHERE subdomain='shop123'
    DB-->>Edge: Return store data
    Edge->>Page: Rewrite with storeId param
    Page->>DB: Fetch products WHERE store_id=...
    DB-->>Page: Return store products
    Page-->>User: Render storefront
```

***

<Card title="Next Steps" icon="arrow-right" href="/architecture/payment-flow">
  Learn how payments are processed per store
</Card>
