Developers

Domains and routing

One app answers on resell.store and every subdomain of it. proxy.ts reads the host and decides which part of the app a request belongs to.

Hosts

HostServed fromWhat it is
resell.storeapp/* as isMarketplace, seller app, checkout, /docs and /api/*
{store}.resell.store/store/{store}/*One seller's store and its listings
api.resell.store/api/v1/*The public API. A leading /v1 is dropped first, so /v1/listings → /api/v1/listings
docs.resell.store/docs/*These docs (/_next assets pass through untouched)
mcp.resell.store/api/mcp/*The hosted MCP servers

All of this is one Render web service with two custom domains, resell.store and *.resell.store. The wildcard covers stores and the three service hosts alike. The root domain comes from NEXT_PUBLIC_ROOT_DOMAIN, so a copy on another domain works the same way.

What proxy.ts does

apps/app/proxy.ts is Next.js 16's middleware. It runs on every request except Next internals and static files, in this order:

  1. Service hosts. On api., docs. or mcp. it rewrites the path (above) and stops.
  2. View switch. ?view=mock sets the rs_view cookie, ?view=live clears it, then it redirects to the same URL without the parameter.
  3. Pass-through. Anything under /api/ or /mock/ is left alone, on every host. That's why the session routes and API calls work on store subdomains too.
  4. Marketplace host. /store/maya/dutch-oven redirects to maya.resell.store/dutch-oven, so store links can stay relative. Share images (opengraph-image, twitter-image) are served in place, because that's where og:image points. In mock mode, designed paths are rewritten to /mock/….
  5. Store host, dev only. On a full page load with no session cookie and no recent attempt, it starts the session handoff (below).
  6. Store host. Everything else is rewritten to /store/{store}{path} (or /mock/store/… in mock mode). The browser never sees /store/.

The proxy never checks who's signed in. Layouts and pages do that with requireUser() or getCurrentUser(). Tests for all of the above are in proxy.test.ts and lib/urls.test.ts.

Reserved names

www, app, api, docs and mcp are never stores (reserved in lib/urls.ts). www. and app. fall through to the marketplace. Hosts with more than one label before the root, like a.b.resell.store, aren't stores either.

A store page and a marketplace page are on different origins, so a client-side next/link between them can't work. Two helpers handle it:

HelperUse it for
<SiteLink href="/discover">A marketplace path. A next/link on the marketplace, an absolute <a> inside a store
<StoreLink store="maya" href="/dutch-oven">A path in a store. A next/link inside that store, an absolute <a> everywhere else

Both live in components/market/links.tsx and know where they are from StoreZone, which app/store/[store]/layout.tsx wraps around every store page. On the server, or for emails and redirects, build URLs with lib/urls.ts:

FunctionProductionLocalhost
siteUrl("/discover")https://resell.store/discoverhttp://localhost:5689/discover
storeUrl("maya", "/x")https://maya.resell.store/xhttp://maya.localhost:5689/x
apiUrl("/me")https://api.resell.store/v1/mehttp://localhost:5689/api/v1/me
docsUrl("/dev")https://docs.resell.store/devhttp://localhost:5689/docs/dev
mcpUrl("/buy")https://mcp.resell.store/buyhttp://localhost:5689/api/mcp/buy
storeDomain("maya")maya.resell.storemaya.resell.store (display only)

storeFromHost() and serviceFromHost() read a Host header; zoneUrl() checks that a URL points at the marketplace or one of its stores, which is how redirects after sign-in stay on resell.store.

Sign-in across subdomains

In production, Better Auth sets the session cookie on .resell.store (crossSubDomainCookies in lib/server/auth.ts), so every store reads the same session. The root and *.resell.store are trusted origins.

On localhost that can't work: browsers won't send a Domain=localhost cookie to maya.localhost. So in dev each store gets its own copy of the session through a one-time token:

  1. The first page load on maya.localhost:5689 with no session goes to that store's /api/session/handoff?to=…, and the proxy sets rs_handoff for 10 minutes so it won't ask again.
  2. The store's handoff route forwards to the marketplace's, where the session cookie is.
  3. Signed in there: it mints a Better Auth one-time token (single use, one minute) and redirects to maya.localhost:5689/api/session/accept?token=…&next=…. Signed out: straight back.
  4. accept verifies the token, sets the store's own cookie for the same session row, and redirects to next. Any failure just arrives signed out.

Because both cookies point at the same session row, signing out anywhere ends both (once the 5-minute cookie cache on the other host runs out).

On localhost

  • The root is localhost:5689 and URLs use http. Any other root gets https.
  • Stores are {slug}.localhost:5689. Chrome, Firefox and Safari resolve *.localhost to your machine with no setup; some command-line tools don't, which is why apiUrl, docsUrl and mcpUrl use paths on the root locally.
  • The session handoff above only runs when the root is localhost.
Terminal
curl http://localhost:5689/api/v1/market/stores/claspandcarry

# the same, the way api.resell.store is routed
curl -H "Host: api.localhost:5689" http://localhost:5689/v1/market/stores/claspandcarry
Keep NEXT_PUBLIC_ROOT_DOMAIN as localhost:5689 locally. A dotted dev root (like lvh.me) would share the cookie like production, but lib/urls.ts would then build https links that the dev server doesn't answer.

More on the API and MCP hosts is on API, docs and MCP.