npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

next-subdomain-router

v1.1.1

Published

Subdomain routing for Next.js 16 proxy.ts

Readme

next-subdomain-router

Subdomain-based routing for Next.js 16 using proxy.ts.

next-subdomain-router allows you to map subdomains (e.g. app.example.com) to internal App Router paths (e.g. /sites/app) with support for:

  • Static subdomain routing
  • Dynamic subdomain routing
  • Local development (*.localhost)
  • Internal route protection
  • Configurable 404 handling (response or rewrite)

📝 Changelog

V1.1.1 (Fixes)

  • afterProxy now runs consistently for all final responses, including rewrites
  • Supabase integration guidance updated for public homepage handling
  • README corrected to show afterProxy(request, response) argument order

v1.1.0

  • Added beforeProxy and afterProxy callbacks for custom middleware logic before and after subdomain routing

✨ Features

  • 🔁 Rewrite subdomains to App Router paths
  • ⚙️ Static and dynamic routing support
  • 🧪 Works in development (*.localhost)
  • 🔒 Optional blocking of direct internal routes
  • 🧩 Fully configurable behaviour
  • 🚫 No middleware hacks - built for proxy.ts

📦 Installation

npm install next-subdomain-router

1. Create your route structure

Inside your Next.js app/ directory, create a folder to hold subdomain routes.

⚠️ Do NOT use underscore-prefixed folders (e.g. _sites). Next.js treats them as private and they are not routable.

app/
  sites/
    app/
      page.tsx
    test/
      page.tsx
    [subdomain]/
      page.tsx   # (optional for dynamic routing)

At the root of your project:

import { createSubdomainRouter } from "next-subdomain-router";

export const proxy = createSubdomainRouter({
	rootDomain: "example.com",
	internalHiddenRoutePrefix: "sites",

	subdomains: {
		app: "app",
		test: "test",
	},

	enableDynamicSubdomainRouting: true,

	denyDirectAccess: true,
	developmentHostname: "localhost:3000",

	notFoundStrategy: "response", // or "rewrite"
	notFoundRewritePath: "/404", // only used if strategy = "rewrite"
});

export const config = {
	matcher: ["/((?!_next|favicon.ico|robots.txt|sitemap.xml).*)"],
};

3. Configure allowed dynamic subdomains (optional)

When using enableDynamicSubdomainRouting: true with a [subdomain] dynamic route folder, you can restrict which subdomain values are accepted using allowedDynamicSubdomains. This acts as a whitelist for the params.subdomain value:

export const proxy = createSubdomainRouter({
	// ... other config
	enableDynamicSubdomainRouting: true,
	allowedDynamicSubdomains: ["user", "account", "profile"], // Only these values are allowed for [subdomain]
});

Your route structure:

app/
  sites/
    [subdomain]/
      page.tsx  # Receives params.subdomain

| Request | Result | | --------------------- | --------------------------------------------------------------------- | | user.example.com | ✅ Routes to /sites/[subdomain] with params.subdomain = "user" | | profile.example.com | ✅ Routes to /sites/[subdomain] with params.subdomain = "profile" | | admin.example.com | ❌ 404 (not in allowed list) |

When allowedDynamicSubdomains is not specified, all subdomains (except reserved ones) are allowed dynamically. This option acts as an allowlist to control which subdomain slugs can be used.


Incoming requests are intercepted in proxy.ts, and the hostname is analysed:

| Request | Internal Route | | ------------------- | ---------------------------------- | | app.example.com | /sites/app | | test.example.com | /sites/test | | alpha.example.com | /sites/alpha (dynamic) | | example.com | / (or rewritten if configured) |

The user never sees the internal route - rewrites happen transparently.


Works out of the box with localhost.

| Request | Result | | ---------------------- | -------------- | | app.localhost:3000 | /sites/app | | test.localhost:3000 | /sites/test | | alpha.localhost:3000 | /sites/alpha |


⚙️ Configuration

rootDomain (required)

Your production domain.

rootDomain: "example.com";

internalHiddenRoutePrefix

Internal route prefix used for rewrites. Default: "sites"

internalHiddenRoutePrefix: "sites";

subdomains

Static mapping of subdomains to routes.

subdomains: {
  app: "app",
  blog: "blog",
}

enableDynamicSubdomainRouting

Enable fallback dynamic routing. Default: false

enableDynamicSubdomainRouting: true;

Requires:

app/sites/[subdomain]/page.tsx

allowedDynamicSubdomains

Whitelist specific subdomains for dynamic routing. When specified, only these subdomains will be allowed. Default: undefined (all subdomains allowed)

allowedDynamicSubdomains: ["user", "account", "profile"];

reservedSubdomains

Subdomains that should never be dynamically routed. Default: ["www"]

reservedSubdomains: ["www", "api"];

denyDirectAccess

Blocks direct access to internal routes. Default: false

denyDirectAccess: true;

| URL | Result | | ----------------- | -------- | | /sites/app | ❌ 404 | | app.example.com | ✅ works |

developmentHostname

Used for local subdomain detection. Default: "localhost:3000"

developmentHostname: "localhost:3000";

rewriteRootPath & rootSubdomain

Rewrite the root domain to a subdomain route. Default: false

rewriteRootPath: true,
rootSubdomain: "app"

| Request | Result | | ------------- | ------------ | | example.com | /sites/app |

notFoundStrategy & notFoundRewritePath

Controls behaviour for unknown subdomains. Default strategy: "response"

"response" (default)

notFoundStrategy: "response";

Returns a raw 404 response.

"rewrite"

notFoundStrategy: "rewrite",
notFoundRewritePath: "/404"

Rewrites to a route inside your app.

beforeProxy (optional)

Async callback executed before subdomain routing. Useful for custom authentication, header validation, or early request termination.

beforeProxy: async (request: NextRequest) => {
	// Custom logic before routing
	if (!request.headers.get("authorization")) {
		return NextResponse.redirect(new URL("/unauthorized", request.url));
	}
	return null; // Continue with normal routing
};

If this function returns a NextResponse, that response is sent immediately and routing is skipped.

afterProxy (optional)

Async callback executed after subdomain routing, before the response is sent. Useful for modifying response headers, setting cookies, or logging.

afterProxy: async (request: NextRequest, response: NextResponse) => {
	// Custom logic after routing
	response.headers.set("x-subdomain-routed", "true");
	return response;
};

🧪 Example

createSubdomainRouter({
	rootDomain: "example.com",
	internalHiddenRoutePrefix: "sites",

	subdomains: {
		app: "app",
	},

	enableDynamicSubdomainRouting: true,
});

| URL | Route | | ----------------- | ------------ | | app.example.com | /sites/app | | foo.example.com | /sites/foo |


🔐 Supabase Setup

Integrate next-subdomain-router with Supabase for authentication across subdomains:

import { NextRequest, NextResponse } from "next/server";
import { createSubdomainRouter } from "next-subdomain-router";
import { updateSession } from "@/lib/supabase/proxy";

export const proxy = createSubdomainRouter({
	rootDomain: "example.com",
	internalHiddenRoutePrefix: "sites",
	subdomains: {
		app: "app",
	},
	enableDynamicSubdomainRouting: true,

	beforeProxy: async (request: NextRequest) => {
		const { pathname } = request.nextUrl;

		const isPublicPath =
			pathname === "/" ||
			pathname.startsWith("/sign-in") ||
			pathname.startsWith("/sign-up");

		if (isPublicPath) {
			return;
		}
	},

	afterProxy: async (request, response) => {
		return await updateSession(request, response);
	},
});

export const config = {
	matcher: ["/((?!_next/|favicon.ico|robots.txt|sitemap.xml|.*\\..*).*)"],
};

Features:

  • ✅ Supabase session validation before routing
  • ✅ Automatic redirect to /sign-in for unauthenticated requests
  • ✅ Cookie management across subdomains
  • ✅ Protected API routes and auth endpoints

Environment Variables:

NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-supabase-key

▲ Vercel Setup (Wildcard Subdomains)

To use next-subdomain-router on Vercel with real subdomains (e.g. app.domain.com, test.domain.com), you must configure wildcard domains correctly.

This is required for subdomain routing to work in production.


⚠️ Important

Wildcard subdomains will NOT work on:

*.vercel.app ❌

You must use a custom domain.


1. Add Domains in Vercel

Go to your project → Settings → Domains

Add:

yourdomain.com
*.yourdomain.com

After adding, Vercel will show a configuration status.


2. Configure DNS (Recommended: Nameservers)

From experience, the most reliable method is to use Vercel DNS via nameservers.

Update your domain nameservers to:

ns1.vercel-dns.com
ns2.vercel-dns.com

This allows Vercel to:

  • automatically manage wildcard routing
  • issue SSL certificates for *.yourdomain.com
  • avoid common DNS misconfiguration issues

3. Wait for SSL Provisioning

After configuration, Vercel will show:

Generating SSL Certificate...

Wait until this changes to:

Valid Configuration ✅

4. Verify Setup

Once ready, test:

app.yourdomain.com
test.yourdomain.com
alpha.yourdomain.com

All subdomains should resolve to your Vercel project.


5. Update Your Router Config

Make sure your router uses your real domain:

createSubdomainRouter({
	rootDomain: "yourdomain.com",
	internalHiddenRoutePrefix: "sites",
	enableDynamicSubdomainRouting: true,
});

6. Common Issues

❌ Subdomains return Vercel 404

  • Domain not added in Vercel
  • SSL not finished generating

❌ Subdomains do not resolve

  • Nameservers not updated
  • DNS propagation not complete

❌ Assets (CSS/JS/images) fail to load

  • Ensure your proxy excludes:
    • /_next/
    • files with extensions (e.g. .js, .css, .svg)

🧠 Notes

  • proxy.ts runs before routing - ensure assets are not rewritten
  • Wildcard domains are essential for dynamic subdomain routing
  • Using Vercel nameservers avoids most DNS-related issues

✅ Summary

| Step | Required | | ------------------------------------ | -------- | | Add *.domain in Vercel | ✅ | | Configure DNS / nameservers | ✅ | | Wait for SSL provisioning | ✅ | | Use custom domain (not vercel.app) | ✅ |


Once configured, next-subdomain-router will work seamlessly with Vercel deployments.


⚠️ Important Notes

❌ Do NOT use _sites

app/_sites/app/page.tsx ❌

This will NOT work - Next.js ignores underscore-prefixed folders.

Use:

app/sites/app/page.tsx ✅

Proxy runs before routing

This library works via proxy.ts, which runs before the App Router resolves the request.

  • It rewrites requests
  • It does not render UI
  • It does not use notFound()

📄 License

MIT


👤 Author

Created by zacaw99