Skill: Runtime And Infra

Use this for TanStack Start routing, SSR, server functions, server routes, Cloudflare Worker runtime, D1/R2, Durable Objects, and deployment-facing changes. If the task touches current user, login, logout, or /api/user, also read references/auth.md.

Stack

SSR Safety

Server-Only Code

Supercomputer Design Mode

Server Routes

Use TanStack Start server routes for browser-safe API proxies such as /api/user:

// app/src/routes/api/user.ts
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/api/user')({
  server: {
    handlers: {
      GET: async () => {
        return Response.json({ ok: true })
      },
    },
  },
})

Server routes are part of the same Worker. Do not add Hono/Express or a second backend process.

app/src/routeTree.gen.ts is GENERATED — normally you don't hand-write it. After adding a route (app/src/routes/api/user.ts, app/src/routes/api/media/upload.tsx, …) the TanStack Router plugin regenerates it on bun run dev/bun run build; a stale tree that imports a nested route but never registers it as a child makes /api/user etc. look like backend failures when the route was simply never registered.

But the deploy build typechecks against the COMMITTED tree. createFileRoute("/api/user") type-checks its path string against routeTree.gen.ts, and the CI build runs tsc and vite in parallel — so tsc sees whatever route tree you committed, before Vite regenerates it. If you add a route, commit a stale tree, and can't run the toolchain in the sandbox (no bun, or the npm registry is blocked), CI fails with TS2345: '"/api/user"' is not assignable to keyof FileRoutesByPath. Two ways out, in order:

  1. Regenerate locally and commit the result — run bun run dev (or build) so the plugin rewrites routeTree.gen.ts, then commit it. Preferred.
  2. Hand-register the route when you genuinely can't run the toolchain. The file is deterministic: add the *RouteImport, the route const, all three route maps, the FileRoutesByPath module augmentation, and the rootRouteChildren entry — mirroring an existing route exactly. Then it typechecks and the plugin will just reproduce the same tree on the CI build.

Binary Upload Routes

Do not send files through JSON server-function input. Browser File, Blob, ArrayBuffer, Uint8Array, base64 strings, and byte arrays must not be serialized into createServerFn data. This can produce Maximum call stack size exceeded, huge payloads, and corrupted upload sources.

For uploads, use an app-local multipart route:

// app/src/routes/api/media/upload.ts
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/api/media/upload')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const form = await request.formData()
        const file = form.get('file')
        if (!(file instanceof File)) {
          return Response.json({ ok: false, code: 'missing_file' }, { status: 400 })
        }

        const bytes = new Uint8Array(await file.arrayBuffer())
        return Response.json({
          ok: true,
          contentType: file.type,
          size: bytes.byteLength,
        })
      },
    },
  },
})

Client code must use FormData and must not set the content-type header:

const form = new FormData()
form.append('file', file)
await fetch('/api/media/upload', { method: 'POST', body: form })

For generation flows, upload first and return a small media reference/id. The later generation JSON request should contain prompt/settings/media refs only, never raw bytes.

Route handlers must declare the HTTP methods the browser will use. For example, a generation proxy route must expose POST, not only GET:

// app/src/routes/api/generate.ts
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/api/generate')({
  server: {
    handlers: {
      POST: async ({ request }) => {
        const body = await request.json()
        return Response.json({ ok: true, body })
      },
    },
  },
})

If a generated website shows Method Not Allowed, first check whether the browser is POSTing to a route that only registered GET, or whether a mutation was built as a GET server function. This commonly breaks generation submit/cost/media flows.

Cloudflare Bindings

Live Data Warning

There is one deploy and one set of D1 and R2 resources backing it. Every migration or data change hits live production data directly. env.HF_ENV is always "production" on deployed builds; it does not give you a separate database or bucket to test against.

Durable Objects

If app/app.manifest.json declares "durableObject": "ClassName", also export the class from app/src/server.ts:

export class ClassName extends DurableObject {
  // ...
}

Containers/code sandboxes are not deployable through this template yet.

SEO Infrastructure

Every site with a public face must include:

  1. robots.txt — TanStack server route at /robots.txt returning User-agent: * / Allow: / / Sitemap: <origin>/sitemap.xml. Drop-in: app/src/routes/robots.txt.ts.
  2. sitemap.xml — TanStack server route at /sitemap.xml enumerating all public page routes. Drop-in: app/src/routes/sitemap.xml.ts.
  3. Canonical URLs — every page route's head() must include links: [{ rel: 'canonical', href: '<absolute URL>' }].
  4. Security headers — apply applySecurityHeaders() to every response — see ## Worker Security below.
  5. No trailing slash/pricing/ must 301 to /pricing. Handle in server.ts before the SSR handler.

These are not optional for deployed sites. The SEO audit (references/seo.md#audit) checks them before deploy.

Worker Security

Every Worker must follow these constraints. Load references/security.md#worker-hardening for the full rules.

  1. No global mutable state. Module-level variables are shared across requests in the same V8 isolate. Never store request-scoped data at module scope.
  2. Cryptographic randomness only. Use crypto.randomUUID() and crypto.getRandomValues(). Never Math.random() for IDs, tokens, or nonces.
  3. No hardcoded secrets. The platform injects auth via the outbound Worker. Website code never handles platform tokens directly. For your OWN secrets (API keys, etc.), set them with higgsfield website secrets set <website_id> --name … --value … and read them server-side as bindings().SECRET_NAME — never hardcode them in source.
  4. Security headers on every response. Apply applySecurityHeaders() (frame-ancestors allowlist, no X-Frame-Options) — see references/security.md#worker-hardening.
  5. Validate server function inputs. createServerFn inputs come from the client. Always validate shape and types before processing.