Skip to content
Fusion

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.tsBun + the Azure CLI, so it runs the same on Windows and macOS/Linux (no bash). Drive it through package.json:

ScriptWhat it does
bun run deployfull first deploy: provision → build + push image → Bicep → migrate
bun run deploy:infraprovision Azure (resource group, ACR, Bicep)
bun run deploy:apprebuild, push the image, roll the Container App
bun run deploy:dbrun migrations against the Azure database
bun run deploy:seedseed data (runs scripts/seed.ts if present)
bun run deploy:e2esmoke-check the deployed app
bun run deploy:hatchetrebuild + 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.

VarSecret?Default
RESOURCE_GROUPnofusion-example-rg
LOCATIONnoswedencentral
ACR_NAMEnofusionexacr (globally unique)
APP_DOMAINnoempty → Azure default domain
PG_ADMIN_PASSWORDyesgenerated — deploy:db needs the same value
BETTER_AUTH_SECRETyesgenerated

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.extensionsPOSTGIS,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.