Deployment bundle specification
The deployment bundle is the artifact a Seglamater operator hands to a paying customer to deploy or upgrade Chatalot. It captures the per-customer configuration that install.sh needs to land a working stack on the managed-update pipeline.
This document is the canonical specification of the bundle format. The machine-readable schema is at bundle-schema.json in this directory; a working example is at bundle-example.json.
Lifecycle
operator (mint) → customer (or AI agent) → install.sh → running stack
↓
long-lived bundle file
↓
used at install + future re-installs + DR rebuild
A bundle is minted once per customer instance by the vendor-side mint-client-bundle.sh script (the Forgejo registry token is created fresh each mint; the bundle is the durable record of that mint). It travels to the customer with the rest of the deployment package (zip archive containing bundle.json, WELCOME.md, LICENSE.txt, the cosign public key, and rendered email body). The customer or their AI agent runs install.sh --bundle bundle.json on the target VPS. install.sh consumes the bundle, generates per-instance secrets locally, pulls the pinned images, brings up the stack on the managed-update pipeline, and exits.
The bundle is kept by the customer for as long as they operate the instance. It is consulted on:
- Initial install
- DR rebuild on a fresh host (same instance, same data)
- Re-application after major host migration
- Support investigations (the operator asks for the
instance_idto correlate)
When a customer offboards, the registry token referenced by the bundle is revoked. The bundle file itself is no longer functional but still useful as an audit record for both sides.
Format
JSON, UTF-8, no BOM, single object at the root, keys in the order documented in the schema. No comments (JSON spec). Pretty-printed for human readability is fine; install.sh consumes either form.
The JSON Schema (bundle-schema.json) is the authoritative shape definition. install.sh validates incoming bundles against it before doing any work and refuses to proceed on schema violation.
Top-level fields
| Field | Why it exists |
|---|---|
schema_version |
Hard versioning; install.sh refuses bundles with versions it doesn't understand. v1 is the only published version. |
client_id |
URL-safe slug used in resource names — Forgejo token name suffix, bundle filename, log identifiers. Stable for the life of the customer relationship. |
client_name |
Display name for humans. Free-form. Shown in the welcome doc and admin UI. |
instance_id |
ULID for this specific deployment. Sortable. Surfaced to the customer for support reference. A customer with multiple instances has multiple bundles, each with its own instance_id. |
created_at |
When the bundle was minted. Used for "is this bundle stale?" warnings and audit trails. |
chatalot.* |
Settings written to the chatalot service environment during install. |
registry.* |
Per-instance read-only registry credentials and image paths. |
cosign.* |
Trust anchor for signed-release verification. |
manifest_host |
Base URL for release-channel manifests. |
support.* |
Support relationship metadata, surfaced in the welcome doc. |
See bundle-schema.json for the full constraints on each field.
What is a secret in the bundle
The only field that grants any access is registry.token. It is:
- A Forgejo
read:package-scoped access token - Scoped to a single client's image stream by token name
- Revocable in seconds (DB delete or web UI)
- Read-only — cannot push images, cannot read repos, cannot do anything else
Loss of a bundle file is a recoverable event. Procedure:
- Operator revokes the named token (
chatalot-<client_id>-readonly-registry) - Operator mints a new bundle with a new token
- Operator delivers the new bundle through the same email pipeline
- Customer re-runs
install.sh --bundle bundle-new.jsonon their host
There are no fields in the bundle whose loss would compromise the customer's data, the cryptographic identity of their instance, or the integrity of the chatalot release-signing chain. Per-instance JWT keys, the database password, the HMAC token between chatalot-server and the updater sidecar — all generated locally during install and never written to the bundle.
This is by design: the bundle is meant to be emailed, copied to a deploy host, archived in operator records, and possibly later seen by a third party during DR or audit. None of those workflows should require encryption or guarded handling.
Bundle delivery
The bundle JSON travels inside a zip archive named chatalot-deployment-<client_id>-<YYYY-MM-DD>.zip. The archive contents are:
chatalot-deployment-acme-corp-2026-04-26/
bundle.json ← this spec
WELCOME.md ← rendered from scripts/onboarding-templates/WELCOME.md.tpl
WELCOME.pdf ← printable version of WELCOME.md
LICENSE.txt ← rendered from scripts/onboarding-templates/LICENSE.txt.tpl
chatalot-public-key.pem ← convenience copy of the cosign public key (install.sh re-fetches and re-verifies regardless)
The zip is sent to the customer via email from noreply@seglamater.com with the rendered EMAIL.txt template as the body. The Reply-To header is support@seglamater.com.
install.sh consumption rules
When install.sh --bundle <path> runs:
- Schema validation. Validates the JSON against
bundle-schema.json. On failure, prints the schema violation and exits non-zero. - Trust anchor verification. Fetches
cosign.pubkey_url, computes its SHA-256, compares tocosign.pubkey_sha256. On mismatch, exits with a hard error referencing supply-chain compromise as the most likely cause. - Pre-flight reachability. Resolves
chatalot.public_url, confirms it points at the host's public IP (warns on mismatch), confirms port 443 is reachable. - Secrets emission. Writes
registry.tokentosecrets/registry_credsas Docker auth JSON. Savescosign.pubkey_url-fetched public key tosecrets/cosign_pub. Generates per-instance JWT keypair, DB password, HMACupdater_token, andtotp_encryption_keylocally — never reading any of these from the bundle. - Compose ingestion. Writes
chatalot.public_url,chatalot.admin_username,chatalot.update_channel,manifest_hostto the chatalot service env. Pins both image fields by digest from the latest channel manifest. - Stack up. Pulls images via
docker loginagainstregistry.{url,username,token}, runsdocker compose up -d, waits for healthchecks. - Admin instructions. Prints the registration URL and
admin_usernameso the customer registers the admin account within five minutes.
install.sh writes a copy of the bundle to secrets/bundle.json on the host (mode 0640) for future support reference and DR rebuilds. That copy contains the registry token. It is not committed to any source control.
Versioning policy
schema_version starts at 1. The version increments on:
- Removal of a required field
- Addition of a new required field
- Change in the meaning or constraint of an existing field
Backwards-compatible additions (new optional fields, new enum values whose default behaviour is sensible, looser constraints) ship inside schema_version: 1 without bump.
install.sh versions support a defined range of bundle versions. Older install.sh against newer bundle: refuse with a clear error advising operator to upgrade the install script. Newer install.sh against older bundle: support if the bundle is one major version behind; refuse otherwise.
Producing a bundle
Bundles are produced by mint-client-bundle.sh (vendor-side; not committed in this repo). That tool:
- Accepts customer info via flags
- Generates a fresh
instance_id(ULID, current timestamp prefix) - Calls the Forgejo API to mint a
read:packagetoken namedchatalot-<client_id>-readonly-registry - Files the token in Vaultwarden as
Forgejo Registry Token - chatalot-<client_id>-readonly(Deploy collection) - Files an onboarding ticket in Plane CHAT project
- Renders the bundle JSON from the operator inputs + minted token + canonical cosign SHA
- Validates the bundle against this schema
- Renders WELCOME.md, WELCOME.pdf, LICENSE.txt, EMAIL.txt from the templates at
scripts/onboarding-templates/ - Assembles the zip archive
- Sends the email via
send-bundle-email.sh(which talks to Proton Bridge on Seg-VPS)
Until mint-client-bundle.sh ships, a hand-built bundle is acceptable for the first customer. Validate it manually with python3 -m jsonschema -i bundle.json bundle-schema.json (or jsonschema-cli, or ajv-cli) before sending.
Security model summary
- Bundle is plain text. Anyone with the file can pull the customer's image stream until the token is revoked.
- The cosign public-key SHA pinning prevents a malicious manifest host from serving a different signing key during install.
- Per-instance secrets are generated on the host. The bundle never sees them. A bundle leak does not compromise the running instance's cryptographic identity.
- Token revocation is a one-line operation:
DELETE FROM access_token WHERE name = 'chatalot-<client_id>-readonly-registry'in the Forgejo DB, or via the web UI. - The bundle does NOT include the cosign signing private key or any vendor-side credential. Bundle exposure is per-customer; vendor-side compromise requires a separate attack surface.