Deploy
Fusion targets two very different environments.
Azure (cloud)
Infrastructure-as-code lives in example/infra/ (Bicep): an Azure Container
App running the TanStack Start server, a PostgreSQL Flexible Server, a
Storage account with a private uploads blob container (fusion-storage's Azure
Blob driver, wired in through AZURE_STORAGE_* automatically), logs to Log
Analytics, and the image pulled from a container registry via a managed identity
(no registry passwords).
The deploy runner is infra/deploy.ts — Bun + the Azure CLI, so it runs the
same on Windows and macOS/Linux (no bash). Drive it through package.json:
| Script | What it does |
|---|---|
bun run deploy | full first deploy: provision → build + push image → Bicep → migrate |
bun run deploy:infra | provision Azure (resource group, ACR, Bicep) |
bun run deploy:app | rebuild, push the image, roll the Container App |
bun run deploy:db | run migrations against the Azure database |
bun run deploy:seed | seed data (runs scripts/seed.ts if present) |
bun run deploy:e2e | smoke-check the deployed app |
bun run deploy:hatchet | rebuild + roll the agent's worker container (autonomous agent) |
First time:
az login
bun run deploy # build → provision → image → app → migrate (each deploy rebuilds)After that, use the targeted commands. Put configuration in
example/.env.deploy.local (gitignored — copy .env.deploy.example); the deploy
commands load it automatically, and it overrides .env.local. You can also set
any value as a shell env var. Defaults are sensible; the two secrets are generated
and written into .env.deploy.local automatically on the first run, so later runs
reuse them.
| Var | Secret? | Default |
|---|---|---|
RESOURCE_GROUP | no | fusion-example-rg |
LOCATION | no | swedencentral |
ACR_NAME | no | fusionexacr (globally unique) |
APP_DOMAIN | no | empty → Azure default domain |
PG_ADMIN_PASSWORD | yes | generated — deploy:db needs the same value |
BETTER_AUTH_SECRET | yes | generated |
AI is not provisioned — it's env-only. To enable it in the cloud, set
OLLAMA_BASE_URL or OPENROUTER_API_KEY as Container App environment (AI).
Proactive agent (autonomous mode)
The proactive agent runs manually out of the box (the
/sandbox/agent "run a tick" button, or "kolla nu" in the home chat). To run it
autonomously on a schedule, the Bicep can stand up a self-hosted
Hatchet — opt in with DEPLOY_HATCHET=true. That provisions a second
Container App (a pod of four containers sharing localhost: the Hatchet engine — the
scheduler — plus RabbitMQ, a one-shot token-writer, and the agent's worker
container, which is what actually runs the agent) plus a dedicated hatchet database
on the same Flexible Server. bun run deploy:hatchet rebuilds the image and rolls just
the worker.
The agent's pgvector memory needs the VECTOR Postgres extension, so the Bicep
allow-lists it via azure.extensions — POSTGIS,VECTOR, and POSTGIS,VECTOR,BTREE_GIST
when Hatchet is deployed (Hatchet's migration needs btree_gist). The worker is
bundled for the Node/Nitro image by scripts/build-worker.ts (chained into
build), since the deployed image carries no Bun or source tree.
Custom domain
APP_DOMAIN is the public domain the app is served on (e.g. fusion.p4o.se, no
scheme). When set, the Bicep makes it the canonical BETTER_AUTH_URL and adds it to
the auth trusted origins — the Azure default *.azurecontainerapps.io host
stays trusted too, so health checks and direct access keep working. Without it, auth
would reject requests coming in on the custom domain.
To bind the domain on the Container App itself, also set APP_DOMAIN_CERT_NAME to
the managed certificate Azure issues when you add the hostname (Container App →
Custom domains, which needs the DNS + verification records first). The Bicep
then declares the binding (SNI SSL), referencing that existing cert rather than
re-issuing it — so deploy:infra preserves the hostname instead of dropping it when
it re-applies the ingress. Leave APP_DOMAIN_CERT_NAME empty if the domain is
fronted by a Front Door / Application Gateway instead (nothing to bind on the app).
Air-gapped (RDF)
Some deployments run on a closed network with no access to GitHub Packages or any
registry. There the build is vendored: the @tikab-interactive/* package
tarballs and the Docker images are packed into a single .tar, carried in, and
brought up with docker compose — no network calls at install or run time. This
is a routine delivery path, not a special case.