Developers
Domains and routing
Hosts
| Host | Served from | What it is |
|---|---|---|
resell.store | app/* as is | Marketplace, 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:
- Service hosts. On
api.,docs.ormcp.it rewrites the path (above) and stops. - View switch.
?view=mocksets thers_viewcookie,?view=liveclears it, then it redirects to the same URL without the parameter. - 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. - Marketplace host.
/store/maya/dutch-ovenredirects tomaya.resell.store/dutch-oven, so store links can stay relative. Share images (opengraph-image,twitter-image) are served in place, because that's whereog:imagepoints. In mock mode, designed paths are rewritten to/mock/…. - Store host, dev only. On a full page load with no session cookie and no recent attempt, it starts the session handoff (below).
- 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.
Linking between zones
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:
| Helper | Use 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:
| Function | Production | Localhost |
|---|---|---|
siteUrl("/discover") | https://resell.store/discover | http://localhost:5689/discover |
storeUrl("maya", "/x") | https://maya.resell.store/x | http://maya.localhost:5689/x |
apiUrl("/me") | https://api.resell.store/v1/me | http://localhost:5689/api/v1/me |
docsUrl("/dev") | https://docs.resell.store/dev | http://localhost:5689/docs/dev |
mcpUrl("/buy") | https://mcp.resell.store/buy | http://localhost:5689/api/mcp/buy |
storeDomain("maya") | maya.resell.store | maya.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:
- The first page load on
maya.localhost:5689with no session goes to that store's/api/session/handoff?to=…, and the proxy setsrs_handofffor 10 minutes so it won't ask again. - The store's handoff route forwards to the marketplace's, where the session cookie is.
- 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. acceptverifies the token, sets the store's own cookie for the same session row, and redirects tonext. 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:5689and URLs usehttp. Any other root getshttps. - Stores are
{slug}.localhost:5689. Chrome, Firefox and Safari resolve*.localhostto your machine with no setup; some command-line tools don't, which is whyapiUrl,docsUrlandmcpUrluse paths on the root locally. - The session handoff above only runs when the root is
localhost.
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/claspandcarryNEXT_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.