A pack with a backend service. pack-terminal (PTY over NATS) as the reference.
A full-stack pack pairs a frontend React component with a backend service that runs as a pod alongside the shell. The frontend talks to its service over NATS — never direct HTTP, never direct TCP. pack-terminal is the reference: the browser subscribes to a PTY stream, the service spawns and owns the PTY.
┌──────────── Your pack ────────────┐│ ││ package/src/index.tsx ││ ↓ useNats().subscribe(...) ││ ││ ··· NATS bridge (SDK) ··· ││ ││ ↑ useNats().publish(...) ││ ││ service/src/index.ts ││ owns the PTY + publishes ││ stdout on your pack's subject │└───────────────────────────────────┘
Your pack owns subjects scoped to your app’s own namespace. Never subscribe to khal.internal.* or khal.catalog.* — those are platform plumbing and not part of the FDE surface. See useNats for the subject-naming guidance.
import { useNats } from '@khal-os/sdk';import { Terminal } from 'xterm';export default function TerminalPack() { const nats = useNats(); const term = new Terminal(); // Subscribe to stdout from the service nats.subscribe('terminal.stdout', (msg) => term.write(msg.data)); // Send keystrokes term.onData((data) => nats.publish('terminal.stdin', data)); return <div ref={(el) => el && term.open(el)} />;}
CI publishes the frontend npm package AND tags a Docker image.
2
Tag a release
CI packages the Helm sub-chart and pushes it to oci://ghcr.io/khal-os/charts/pack-terminal.
3
Install
A customer admin installs the pack — the platform deploys the service pod alongside the shell, injects env, and the frontend picks up the new pack on next load.
The platform manages pod lifecycle, restart policy, and secret injection. Your pack should be stateless at the process level — persist anything important through the platform’s data services, not process memory.