- Long-running work that can’t live in a React component
- Integrations with external APIs from a trusted environment
- Shared state across users in the same org
- Anything that requires secrets the frontend must not see
File layout
A minimal pack with a backend looks like this:pack-terminal is the reference. Its service entrypoint is service/src/index.ts — a Bun process that connects to NATS, registers subject handlers, and also serves /healthz and /readyz on a local port.
Talking to the frontend
NATS is the primary transport between your frontend and your service. Your frontend usesuseNats (or useService); your service uses the standard nats Node client.
khal.<orgId>.<packId>.<action>. See pack-terminal/service/src/handlers/spawn.ts for a real spawn/input/output flow.
Talking to external APIs
Your service is a regular Bun process. Usefetch, any npm client library, or raw TCP — whatever fits. Just pull credentials from environment variables, never from the frontend. See environment config for how secrets reach your service.
Health checks
The platform probes your service. Expose/healthz (liveness) and /readyz (readiness) over HTTP:
pack-terminal’s healthcheck.sh just checks that the port is listening.
Lifecycle
The platform runs your service alongside your pack’s frontend, and terminates it when the pack is uninstalled. You don’t manage pods, restart policies, or rollout — you just:- Build a container image that runs your service.
- Publish it.
- Reference it from
khal-app.json.
Graceful shutdown
Listen forSIGTERM and drain cleanly. NATS has a drain() method for flushing in-flight messages:
Full-stack pattern
For an end-to-end example — frontenduseNats subscribing to output from a backend NATS handler — read pack-terminal’s source. The frontend hook useNatsPty and the service’s spawn handler together show the full request/event loop.
What’s next
Environment config
Declare env vars and secrets your service needs; let the platform inject them.
Publish your pack
Push → CI → published npm + Docker + Helm.