Skip to content
Twilight React Playground
ثيم رائدaren

twilightMiddleware

middlewareAdvancedserver

The request middleware every theme lists first in app/start.ts; it opens the private, per-request context the rest of the engine reads.

import { twilightMiddleware } from '@salla.sa/twilight-theme-engine/tanstack';

In plain words

One server serves every shopper of every store that points at your theme, often at the same moment. Each request needs its own notes: is this visitor signed in, which store did the address name, which host did it arrive on.

A middleware is a function that runs on the server for every request, before any page code. twilightMiddleware() reads the request's cookies and address, writes those notes into a context that belongs to this request only, and lets the request continue. Everything after it (the root route, loaders, head functions, the API client) reads that context.

You add it once, first in the requestMiddleware list of app/start.ts, and never call it anywhere else.

Signature

function twilightMiddleware(options?: {
  cookieName?: string;   // default 'token': the cookie holding the customer's auth token
}): AnyRequestMiddleware  // from @tanstack/react-start

Example

app/start.ts
import { createStart } from '@tanstack/react-start';
import { twilightMiddleware, earlyHintsMiddleware } from '@salla.sa/twilight-theme-engine/tanstack';

export const startInstance = createStart(() => ({
  // First: every middleware after it can read and write the request context.
  requestMiddleware: [twilightMiddleware(), earlyHintsMiddleware()],
}));

How it behaves

  • It runs the rest of the request inside runWithTwilightContext({ authToken, scope, storeId, storeBase, versionId, requestHost }, …), so every other field starts from its default for each request, and hands { authToken } to TanStack as middleware context. It fetches nothing: the store settings, the locale and the translations are loaded afterwards by the root route that createTwilightRootRoute() sets up.

  • What it reads: the token cookie (the auth token), the scope cookie (the selected market or branch, as JSON), and the host from the request URL, falling back to the Host header.

  • storeId comes from ?storeId= or the salla_sid cookie, storeBase from the first path segment, and versionId from ?versionId= or the salla_vid cookie. They are read only on preview.salla.design and loopback hosts such as localhost. On any other host the hostname is the store, and all three stay null.

  • The context lives in AsyncLocalStorage from node:async_hooks, which is why the server runtime needs the nodejs_compat compatibility flag (the reference theme and this playground both set it). If that import fails, the engine silently falls back to one module-level store shared by every request (src/twilight/context.ts).

  • Middleware of your own that reads or writes the context goes after it in the list. The playground adds one, pinPlaygroundStore in packages/playground/app/start.ts: when getTwilightContext() shows no storeId and no storeBase, it calls updateTwilightContext({ storeId: '1510890315' }).

  • cookieName changes only the cookie this middleware reads. The root route falls back to the token cookie, which is also the one TwilightProvider writes after a sign-in, so leave the default.

Gotchas

  • Without it nothing renders on the server: createRouter() and the root route both write into the context, and outside a request context that throws [Twilight] No request context. Wrap with runWithTwilightContext(). Keep it in app/start.ts.

  • Never copy the playground's store pin into a theme. The playground is served from an address that is not a store, so it names Salla's demo store itself. A theme serves many stores, and on a merchant's own domain the hostname is the store: a pinned id would show the same shop on every domain that points at the theme.

  • docs/getting-started/04-theme-anatomy.md says it resolves the store, locale, direction and theme settings. It resolves only what the request names; the locale and settings come from the root route, which runs after it.

Related

Source and docs