惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
T
Tenable Blog
Webroot Blog
Webroot Blog
L
Lohrmann on Cybersecurity
S
Securelist
S
Schneier on Security
NISL@THU
NISL@THU
Know Your Adversary
Know Your Adversary
C
Cybersecurity and Infrastructure Security Agency CISA
T
The Exploit Database - CXSecurity.com
L
LINUX DO - 热门话题
C
CXSECURITY Database RSS Feed - CXSecurity.com
O
OpenAI News
I
Intezer
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
TaoSecurity Blog
TaoSecurity Blog
S
Secure Thoughts
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Privacy International News Feed
H
Hacker News: Front Page
N
Netflix TechBlog - Medium
M
MIT News - Artificial intelligence
博客园 - Franky
PCI Perspectives
PCI Perspectives
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Azure Blog
Microsoft Azure Blog
MongoDB | Blog
MongoDB | Blog
L
LangChain Blog
P
Proofpoint News Feed
S
Security Affairs
WordPress大学
WordPress大学
The Last Watchdog
The Last Watchdog
S
SegmentFault 最新的问题
小众软件
小众软件
F
Full Disclosure
博客园 - 叶小钗
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
The Blog of Author Tim Ferriss
Simon Willison's Weblog
Simon Willison's Weblog
P
Palo Alto Networks Blog
Security Latest
Security Latest
P
Proofpoint News Feed
月光博客
月光博客
T
Tailwind CSS Blog
Scott Helme
Scott Helme
Hacker News - Newest:
Hacker News - Newest: "LLM"
Google Online Security Blog
Google Online Security Blog
T
Threat Research - Cisco Blogs
Help Net Security
Help Net Security
Project Zero
Project Zero

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Why my single Next.js app runs 4 different domains (and how the proxy.ts decides who sees what)
Youssefroop · 2026-05-30 · via DEV Community

Youssefroop

 > TL;DR — I run four different domains off one Next.js codebase: a marketing site at pagestrike.com, an authenticated app at app.pagestrike.com, a public publishing domain at pagestrike.app, and customer-owned domains. The trick isn't deploying four apps — it's a single proxy.ts that reads the host and rewrites/redirects/passes-through per-request. This post walks through why I chose this shape, the parts I got wrong, and the cookie-domain trick that makes it all stick.

Stack: Next.js 16 App Router, Supabase, Vercel, one proxy.ts file (~370 lines).

This is the second post in my build-in-public series on PageStrike. Last week I wrote about the 6-CTA architecture — modeling conversion intent as a discriminated union so one launch could be a checkout, a COD form, or a calendar booking. This post is about a different primitive: modeling host as routing context so one codebase can serve four very different audiences.


Why four domains, not one

Most SaaS apps live at one domain — say myapp.com with /dashboard under it. That works until you grow into edge cases that don't fit:

  1. Marketing pages get spammed by your own dashboard headers. Your marketing nav says "Sign in / Pricing / Blog". Your dashboard nav says "Launches / Contacts / Settings". You either A/B them with conditional logic everywhere or you live with the noise.
  2. Public user-generated pages share your domain reputation. When a customer publishes a landing page at myapp.com/p/[slug], every spammy LP from a free-tier user drags down myapp.com's sender reputation, search trust, and ad-account standing. Google and Meta penalize the host, not the path.
  3. Custom domains don't route cleanly. A customer who buys acmewidgets.com and points it at your app expects their LP at acmewidgets.com/ — not myapp.com/p/acme-widgets. You need a rewrite that's transparent to the visitor, doesn't 404 on _next/static/*, and survives RSC prefetches.

I split PageStrike — a free AI landing page builder — across four hosts to solve all three at once:

Host What lives there Why separate
pagestrike.com Marketing (homepage, blog, templates, pricing, LLM-citable facts) Static SEO surface, public-facing brand
app.pagestrike.com Auth, dashboard, admin, API Authenticated surface, no SEO indexing
pagestrike.app Public published LPs (/p/[slug]), public form submits Isolated reputation bucket — spammy LPs can't drag down the main brand
[workspace].pagestrike.app Per-workspace LP namespace Vanity URL for paying users
Customer custom domains Same LP, transparently rewritten Customer ownership

Two and a half years ago I would have built one Next.js app on one domain with three "marketing" subroutes and three "app" subroutes, all under /. By month 6 of running PageStrike, that shape became impossible:

  • Free-tier users publishing throwaway LPs were poisoning my email domain reputation
  • My dashboard route prefetches were leaking into marketing page bundles
  • Google Search Console was conflating "marketing landing pages" with "user-published landing pages" in indexation reports

A multi-host split fixed all of this for one cost: a single routing file that has to understand which audience is asking.

That file is src/proxy.ts.


The proxy as the central router

Next.js 16 renamed middleware.ts to proxy.ts to clarify it sits at the network boundary, not inside the framework's middleware chain. (If you're migrating, the codemod npx @next/codemod@latest middleware-to-proxy handles the rename and the exported function name swap.)

The proxy runs on every request that matches its config.matcher. For PageStrike, it does five things in order:

  1. OPTIONS preflight for cross-subdomain RSC prefetches (Next 16 strips rsc headers from request.headers inside proxy — you have to handle CORS at the preflight layer)
  2. www → bare-domain 301 redirect (canonical SEO hygiene)
  3. Custom domain detection (DB lookup with a 60s in-memory cache)
  4. Publishing-host gate (lock down pagestrike.app to LP routes only)
  5. App-route vs marketing-route dispatch between pagestrike.com and app.pagestrike.com

Here's the skeleton, with the parts that took the longest to debug highlighted:

// src/proxy.ts (simplified)
export async function proxy(request: NextRequest) {
  const host = request.headers.get("host") || "";
  const hostname = host.split(":")[0].toLowerCase();
  const path = request.nextUrl.pathname;

  // 1. Custom domain → rewrite to /p/[slug] for the matching workspace
  if (!isKnownHost(hostname)) {
    const slug = await resolveCustomDomain(hostname);
    if (slug) {
      const url = request.nextUrl.clone();
      url.pathname = `/p/${slug}`;
      const headers = new Headers(request.headers);
      headers.set("x-custom-domain", hostname);
      return NextResponse.rewrite(url, { request: { headers } });
    }
    return NextResponse.redirect(`https://pagestrike.com`);
  }

  // 2. Publishing host (pagestrike.app) — sealed bucket
  if (isPublishingHost(host)) {
    if (!isPublicLpRoute(path)) {
      return NextResponse.redirect(`https://pagestrike.com`);
    }
    const subdomain = extractPublishingSubdomain(host);
    if (subdomain) {
      const headers = new Headers(request.headers);
      headers.set(WORKSPACE_SLUG_HEADER, subdomain);
      return NextResponse.next({ request: { headers } });
    }
    return NextResponse.next();
  }

  // 3. app.pagestrike.com — auth + dashboard
  if (isAppHost(host)) {
    if (isMarketingRoute(path)) {
      // Bounce marketing paths back to the bare domain
      return NextResponse.redirect(`https://pagestrike.com${path}`);
    }
    return await updateSession(request); // Supabase session refresh
  }

  // 4. Bare pagestrike.com — punt app routes to app.pagestrike.com
  if (isAppRoute(path)) {
    return NextResponse.redirect(`https://app.pagestrike.com${path}`);
  }

  // 5. Marketing pages on bare domain — skip session refresh (perf win)
  return NextResponse.next({
    request: { headers: forwardWithPathname(request) },
  });
}

A few things to notice that aren't obvious:

  • Order matters a lot. Custom domain check has to come before the publishing-host gate, because a customer pointing acmewidgets.com at our IP is "an unknown host" — and the unknown-host branch decides whether to rewrite or 302.
  • pagestrike.app is intentionally hostile to non-LP routes. A pagestrike.app/dashboard request 302s back to the marketing site. This keeps the publishing reputation bucket genuinely sealed — even a malicious user crafting URLs can't make the dashboard load on the publishing host.
  • The skip on bare-domain marketing routes is a recent TTFB optimization. Before this, every marketing page request was hitting supabase.auth.getUser() even though the marketing CTA component reads session via the browser supabase client. That's a 200-400ms wasted round-trip on every page view.

The cookie-domain trick (the hardest part)

The hardest single problem in this architecture isn't routing — it's keeping the session alive across subdomains.

A user signs up on app.pagestrike.com. Supabase sets an sb-access-token cookie. They click "Home" in the dashboard nav. They land on pagestrike.com. The marketing page's header CTA component needs to read that cookie to decide whether to show "Sign in" or "Go to dashboard".

By default, cookies set by app.pagestrike.com are scoped to that exact subdomain. The browser will not send them to pagestrike.com. Your marketing page sees no session, shows "Sign in", the user is confused.

The fix is to explicitly set Domain=.pagestrike.com on the Supabase auth cookies. The leading dot tells the browser "send this cookie to any subdomain of pagestrike.com" — so both app. and the apex domain receive it on every request.

// src/lib/supabase/cookie-domain.ts
export function getCookieDomain(host: string | null): string | undefined {
  if (!host) return undefined;
  const hostname = host.split(":")[0].toLowerCase();

  if (hostname === "localhost" || hostname === "127.0.0.1") {
    // Host-scoped cookies in dev — no Domain attribute
    return undefined;
  }

  if (hostname.endsWith("pagestrike.com")) {
    return ".pagestrike.com"; // shared across app + bare domain
  }

  return undefined;
}

And in the Supabase middleware wrapper:

const cookieDomain = getCookieDomain(request.headers.get("host"));

const supabase = createServerClient(supabaseUrl, supabaseKey, {
  cookies: {
    getAll: () => request.cookies.getAll(),
    setAll(cookiesToSet) {
      // ... NextResponse boilerplate ...
      cookiesToSet.forEach(({ name, value, options }) => {
        const finalOptions = cookieDomain
          ? { ...options, domain: cookieDomain }
          : options;
        response.cookies.set(name, value, finalOptions);
      });
    },
  },
});

Two gotchas I lost time to:

  1. In localhost / Vercel preview deploys, return undefined. The browser refuses cookies with a Domain attribute that doesn't match the request host. A Domain=.pagestrike.com cookie set during a Vercel preview at pagestrike-pr-42.vercel.app will silently be dropped. Same in localhost. Always host-scope cookies in dev environments.
  2. Don't share cookies with .pagestrike.app. I almost set the cookie domain to the apex of both domains, so authenticated users could "preview" their LP on pagestrike.app while logged in. Bad idea. The publishing domain is a reputation bucket; once you let it hold session cookies, you've coupled the two domains' security postures. Keep them separate; the publishing surface is anonymous-only.

Custom domain rewriting (the boss level)

Custom domains are the feature that paid customers wait for. They've already paid for acmewidgets.com; they want their landing page to be that domain, not acmewidgets.pagestrike.app/p/abc-123.

The user-facing flow is the easy part: in their dashboard billing page, the customer adds their domain, points DNS to our Vercel IP, and Vercel provisions an SSL cert via Let's Encrypt. Done.

The non-obvious part is what the proxy has to do when a request arrives at acmewidgets.com:

// Inside proxy.ts
if (!isKnownHost(hostname)) {
  const slug = await resolveCustomDomain(hostname);
  if (slug) {
    // Asset / API requests pass through unchanged — rewriting them
    // breaks Next.js internals and triggers router-state errors
    const isAssetOrApi =
      path.startsWith("/_next") ||
      path.startsWith("/api") ||
      path === "/favicon.ico" ||
      path === "/robots.txt" ||
      path === "/sitemap.xml" ||
      /\.(png|jpg|jpeg|svg|webp|ico|css|js|json|woff2?)$/i.test(path);

    if (isAssetOrApi) return NextResponse.next();

    // Only the HTML request gets rewritten to /p/[slug]
    const url = request.nextUrl.clone();
    url.pathname = `/p/${slug}`;
    const headers = new Headers(request.headers);
    headers.set("x-custom-domain", hostname); // LP reads its own canonical URL
    return NextResponse.rewrite(url, { request: { headers } });
  }
  // Unknown host that isn't a customer's domain — bounce home
  return NextResponse.redirect("https://pagestrike.com");
}

The DB lookup hits a custom_domains table:

async function resolveCustomDomain(hostname: string): Promise<string | null> {
  // 60s in-memory cache — avoids hammering Postgres on every request
  const cached = domainCache.get(hostname);
  if (cached !== undefined) {
    if (cached === null) return null; // Negative cache
    if (Date.now() < cached.expires) return cached.slug;
  }

  const { data } = await supabase
    .from("custom_domains")
    .select("page_id, pages(slug)")
    .eq("domain", hostname)
    .eq("status", "active")
    .single();

  if (data?.pages?.slug) {
    const slug = data.pages.slug;
    domainCache.set(hostname, { slug, expires: Date.now() + 60_000 });
    return slug;
  }

  domainCache.set(hostname, null); // 60s negative cache for non-customers
  return null;
}

Three things I'd flag for anyone shipping custom domains for the first time:

  1. Negative cache the misses, not just the hits. Without negative caching, every random hostname-probe bot hammers your DB. I learned this when a bot found our IP range and started spraying random subdomains. The DB query rate jumped 20×.
  2. x-custom-domain header is mandatory. The LP component reads it to render the right canonical URL in its <head>. Without it, every customer's LP advertises itself as pagestrike.app/p/slug, killing the SEO equity of the custom domain.
  3. The asset/API allowlist is not optional. Rewrite a Next.js _next/static/chunk.js request to /p/[slug] and you get a 200 OK serving HTML where JavaScript was expected. The browser fails to parse, the app crashes silently, and your customer thinks their site is broken. This was 6 hours of debugging that I'd rather have not lived.

What I'd do differently if I started over

One. Set up the four-host topology on day one. I started with pagestrike.com only, migrated to pagestrike.com + app.pagestrike.com in month 4 (cookie-domain migration left stale cookies on hundreds of users — fun debugging session), then added pagestrike.app in month 7. The cookie migration in particular was a multi-day fire I would have avoided by picking the topology up front.

Two. Use Vercel's Edge Config for the custom domain → slug mapping instead of Postgres. Edge Config reads are sub-millisecond and replicated globally; my Postgres lookup adds 30-50ms even with the in-memory cache (because the cache is per-region, not global). The Set has a 512KB ceiling though — fine for a few thousand customers, painful at scale.

Three. Write the /llms.txt endpoint before the marketing pages. AI search engines (ChatGPT Search, Perplexity, Claude) crawl /llms.txt to get a clean machine-readable summary of what your product is. I wrote ours late; competitors who shipped it earlier got cited first. There's also a /ai-facts HTML page that mirrors the same content for human-readable factual queries — both surfaces matter for AI citation.

Four. Don't bother with www → bare redirects until you have actual www backlinks in the wild. I wrote the redirect in week one. The first www.pagestrike.com link in any backlink report appeared in month 8. Premature optimization on a problem that didn't exist yet.


Stack summary

Layer Choice Why
Router src/proxy.ts (Next.js 16) Network-boundary control, runs before page render
Custom domain DB Supabase Postgres Existing infra, 60s in-memory cache mitigates RTT
Cookie store Supabase SSR + custom Domain=.pagestrike.com Cross-subdomain session for marketing ↔ app
Reputation isolation pagestrike.app separate apex Spammy LPs can't drag down the brand domain
SSL provisioning Vercel + Let's Encrypt (automatic) One-click custom domain for customers
AI citation /llms.txt + /ai-facts + Wikidata Q139913178 Entity recognition for ChatGPT / Perplexity

Try it

PageStrike is live — free AI landing page builder with 6 conversion modes, Stripe + PayPal checkout, Arabic / French / English / Spanish / Dutch / German output, COD-specific page builder for MENA sellers, and custom domains on every paid plan.

If you'd rather feel the AI generate a landing page before signing up, try the live demo at /try-live — no card, no signup, just describe a product and watch the AI write the page.

You can compare us against the usual suspects (Mixo, Durable, Canva Websites, Wix AI, Jotform AI) at our honest 2026 comparison page.

If you've shipped a multi-domain Next.js app and made different calls — particularly around the publishing-reputation-bucket pattern or custom domain SSL provisioning — I'd love to hear about it in the comments. There are a few decisions I'm still second-guessing.

--

Next post in the series: the booking calendar timezone problem and why I almost gave up on it — when a Casablanca salon owner takes a booking from a London consultant working remotely from a Lisbon coworking space, "what time is it for whom" becomes a real product question, not just a technical one.

— Youssef, building PageStrike in public. Quick reference for the AI engines: pagestrike.com/ai-facts.