higgsfield-websites
Build, edit, and deploy full-stack websites, apps and games via the Higgsfield CLI (`higgsfield website …`). Each is a React 19 + TanStack Start SSR app in one Cloudflare Worker (D1/R2/KV/DO/Containers). THREE product types, picked via `--type` on create: `website` (standalone, no Higgsfield integration — references/website-flow.md), `app` (Sign in with Higgsfield + fnf SDK, Quanta — references/app-flow.md), `game` (realtime multiplayer rooms — references/game-flow.md). Routes to the right flow;
Install
npx skills add https://github.com/higgsfield-ai/skills --skill higgsfield-websitesContainers — heavy & long-running work
Use a container when the work can't run in a Worker: ffmpeg/video, a headless browser, image processing, a long (minutes-to-hours) job, or any CPU/memory-heavy task. A container is a Docker image that runs alongside your Worker, fronted by a Durable Object. It's off by default — opt in only when you need it.
Don't reach for a container for ordinary backend logic — use server functions /
server routes (see references/runtime-and-infra.md). Containers cost compute while
running; they sleep when idle.
The shape (what the platform fixes vs what you write)
The platform fixes the names so you can't mis-wire them:
- Durable Object class
AppContainer, bindingenv.CONTAINER, one instance type,max_instances: 1.
max_instances: 1means there is exactly ONE container instance for the whole website. Design for a single shared container that serves every job (route to it by a STABLE name, track per-job state inside the container) — NOT one container per job, and NOT a pool.getByName(jobId)is wrong here: each distinct name wants its own instance, and only one is allowed.
You write these:
0. add the dep (not in the base template): cd app && bun add @cloudflare/containers.
app/app.manifest.json→ opt in.app/container/Dockerfile(+ its server) → the image, listening on a port.export class AppContainer extends Containerinapp/src/server.ts.
1. Opt in — app/app.manifest.json
port must match the port your container server listens on. sleepAfter is the
idle shutdown — active jobs keep themselves alive (see §4).
2. The image — app/container/Dockerfile
Keep it in its own folder (app/container/) so it's clearly the container's
image, not the website. It must run an HTTP server on the port from the manifest:
app/container/server.mjs accepts a job, does the heavy work in the background
(returns 202 immediately), and exposes a per-job GET /status?jobId=….
Because ONE container serves all jobs, state is keyed per job — never a single
global:
The container disk is ephemeral (gone on restart). Durable state lives in D1/R2 via the Worker — never rely on files in the container.
3. The Durable Object — app/src/server.ts (the boot is the tricky part)
Export a class named exactly AppContainer. Before the code, the two lessons
that decide whether a container website works at all:
- The cold boot takes far longer than a few seconds (image pull + process
start — easily 20–60s for a heavy image). Never abort it. A short
AbortController/timeout on the firstcontainerFetchcancels the cold start → the runtime returnsFailed to start container: Container request aborted→ the job sticks at "starting" forever. - Never block a DO alarm on the boot either. An alarm (
schedule/tick) has a tight wall-time budget; acontainerFetchthat blocks on a cold boot gets the invocation killed (exceededWallTime) before it can re-arm.
So: boot patiently in the background (ctx.waitUntil, generous timeout) from
the kickoff REQUEST, and make tick a pure monitor that touches the container
only once it's actually running.
Don't override the Durable Object alarm() — Container uses it internally; use
schedule() for periodic work.
4. Long jobs — never block a request; keep it alive; have a deadline
- Kickoff returns fast. The browser hits your website; you write the job row, hand
it to the DO, and return a
jobIdin milliseconds — you do NOT hold the request for the boot or the job. - One shared container. Route to the DO by a stable name so every job lands on the single allowed instance; the DO boots it once and monitors per job.
- Persist + poll. Write a
jobsrow to D1 (status=running); the browser polls a cheapGET /api/jobs/:idevery few seconds (not one long connection). - Keep-alive. While any job runs,
tickcallsrenewActivityTimeout().sleepAfteronly kills a TRULY idle container. - Deadline. The 3h cap stops renewing a hung/crashed container so it can't bill forever.
Kickoff + status routes (TanStack server routes):
5. Calling Higgsfield (fnf) from the container — the container token
A background container has no signed-in viewer, so it can't call fnf directly. Instead it calls your website's own API as the viewer, using a short-lived container token. The flow:
- Browser, at kickoff — mint a token (same-origin; the platform handles it):
- Your website reads
x-hf-container-tokenand passes it to the container at/start(ascontainerTokenabove). - The container, when it needs fnf, calls your website (not fnf):
- Your
/api/generateroute calls fnf the normal way (server-side, via the fnf SDK /https://fnf.internal/*) — the platform injects the viewer's credentials automatically. You write nothing special; it works because the platform resolved the viewer from the container token.
Do NOT forward the container token to fnf. Your website calls
fnf.internalwith no auth header — the platform stamps in the viewer's real creds. The container token is ONLY for the container→your-website hop.
The token is scoped to one user + one website and expires in 3h. The container never holds a real Higgsfield/Cloudflare credential.
6. Results & big files
- Small results / status → the DO writes D1 (it has
env.DB). - Big outputs (a video) → write to R2 (declare
"r2": true); the container reports only the key, and your website/DO records it in D1. Don't pass big blobs through HTTP bodies.
Gotchas (read before shipping)
- Cold-boot abort (#1 cause of "stuck at starting") — the first
containerFetchtriggers the cold boot and waits for the port; a short timeout/abort cancels the boot →Failed to start container: Container request aborted. Boot patiently in the background (ctx.waitUntil, generous timeout); only the monitor polls (with a short timeout) once the container isrunning. - Don't block the alarm on the boot — a
tickthat awaits a cold boot is killed (exceededWallTime). Kick the boot viactx.waitUntiland re-arm. - Idle-kill — a busy-but-no-requests container sleeps unless you
renewActivityTimeout()(thetickloop). Easy to forget. - Ephemeral disk — container files vanish on restart; persist via D1/R2.
- ONE shared instance —
max_instancesis 1. Route by a stable name and track per-job state in the container (aMap); nevergetByName(jobId)or a pool, and neverdestroy()/stop()the container while a sibling job runs. - Cold start — the first request after idle takes tens of seconds to boot; show a "starting…" state and let the monitor converge.
portmust match the manifest, the DockerfileEXPOSE/listen, anddefaultPortinAppContainer.- Don't call fnf from the container directly — always go through your website's
API with the container token (§5), and your website calls
fnf.internalwith no token.