Deploying a site
Keys, Capsules, secrets, publishing and activation.
You deploy a site by packaging it into a deployment Capsule: one file that is signed, encrypted and addressed by its SHA-256 digest. A host (see Running a host) decrypts it, checks the signature, and starts serving the site for the domains you named.
Status. Single-host deployment, as described in this chapter, is what the project's own website uses. Official signed binaries and a hosting service do not exist yet.
The model
your machine / CI the host
----------------- --------
site folder holds: age identity (secret)
| crisptastic deploy trusted signer public keys
v Capsule store (folder or S3)
site.capsule.age --- publish ---> store --- notify ---> decrypt, verify signature,
(signed, then encrypted) unpack, switch routes
Three separate keys keep roles apart:
| Key | Who holds the private half | Purpose |
|---|---|---|
| Host identity (age X25519) | The host only | Decrypts Capsules. You encrypt to its public recipient (age1...). |
| Signing key (Ed25519) | You or your CI | Proves a release is yours. The host trusts only listed public keys. |
| Management client certificate (mTLS) | You or your CI | Lets you tell the host to activate a Capsule. |
Decryption and authorisation are independent. A Capsule that decrypts but is signed by an unknown key is rejected.
One-time setup
1. Create the host identity
Use the standard age tool; Crisptastic does not generate identities.
age-keygen -o /etc/crisptastic/server.agekey # on the host; keep secret
age-keygen -y /etc/crisptastic/server.agekey # prints the public recipient: age1...
2. Create a signing key
crisptastic capsule keygen --private release.pk8 --public release.pub
Keep release.pk8 offline or in your CI secrets; never put it in the site
folder. Copy release.pub (a raw 32-byte key, not text) to every host and pass
it with --trusted-signer. To rotate, add the new public key on every host,
restart them, then sign with the new key.
3. Management certificates
The host's management port uses mutual TLS. You need a CA, a server
certificate for the host, and a client certificate for the deployer. Crisptastic
does not generate these; use your own PKI or openssl. The server certificate's
subject alternative name must match the host name or IP you give to --peer.
Preparing the site
crisptastic deploy needs crisptastic.toml with at least:
[deployment]
domains = ["example.com", "www.example.com"]
and any [env] / [env_from_process]
values the site needs.
What gets packaged: every file under the site folder except a top-level
.git/ and .crisptastic/. Be aware that:
node_modules/,target/, notes and anything else in the folder go too. Keep the site folder clean, or deploy from a clean checkout.- A file or folder named
.envor starting.env.makes the build fail on purpose. Symbolic links also fail. - Limits: 10,000 files, 128 MiB per file, 512 MiB in total, and 256 MiB for the finished ciphertext.
- The host compiles your pages when the site is activated, so a page that
does not compile fails at activation. Run
crisptastic checkin CI before deploying.
Secrets
[env]
API_BASE = "https://api.example.com"
[env_from_process]
OPENAI_API_KEY = "OPENAI_API_KEY"
At deploy time the values are read from the environment of the deploy
command and sealed inside the Capsule. They are removed from the copy of
crisptastic.toml that gets packaged. On the host they live only in locked
memory and are never written to disk, logs or templates.
Rules:
- Names must be
[A-Z][A-Z0-9_]*(no lowercase) and values at most 64 KiB. - A variable missing from the deploy environment fails the build.
- All
[env]and[env_from_process]entries become secrets on a host, including harmless ones like theAPI_BASEabove, and pages cannot read them (context.envis empty). Only the experimental[llm],[mail]and[arbax]capabilities consume them. - The encrypted Capsule contains the secret values. Anyone who holds both the Capsule and a host identity can read them, so treat the Capsule store and the host identity as sensitive. Crisptastic never persists secrets on the host, but the archive does.
- Integrations such as Stripe and Auth0 read their
*_envsecrets from the host process environment, not from Capsule secrets (details).
On macOS you can keep values in the Keychain with macrun:
macrun set my-site production OPENAI_API_KEY
macrun run my-site production -- crisptastic deploy ./my-site ...
Build the Capsule
crisptastic deploy ./my-site \
--recipient age1yourhostrecipient... \
--signing-key ./release.pk8 \
--out ../capsules/my-site.age \
--publish-store-dir ../capsule-store
--recipientis required and repeatable (up to 32). Add every host that might ever run the site, because each host can only decrypt with its own identity.--outis required and must be outside the site folder.--publish-store-dir DIRcopies the result into a folder store;--publish-s3uses an S3 bucket instead.
It prints the Capsule's sha256 digest, the signer id, the domains, and the
names (never values) of the secrets. The digest is how every later step
refers to this release.
The output file is not reproducible byte for byte (encryption is randomised), so two builds of the same source have different digests.
Publish
If you did not use --publish-*, store the file separately:
crisptastic capsule publish ../capsules/my-site.age --store-dir ../capsule-store
crisptastic capsule pull <digest> --out restored.age --store-dir ../capsule-store
The store is content-addressed (capsules/sha256/<aa>/<digest>.age). The store
checks length and digest but cannot read the contents. Publishing identical
bytes twice is harmless.
For S3, set these in the environment of the process: CRISPTASTIC_CAPSULE_S3_BUCKET
(required), _REGION (or AWS_REGION), _ENDPOINT (for R2 and other
S3-compatible stores), _ACCESS_KEY (or AWS_ACCESS_KEY_ID), _SECRET_KEY
(or AWS_SECRET_ACCESS_KEY), and _PATH_STYLE for path-style addressing. Only
static key pairs are supported. The host needs read access and the deployer
write access to the bucket.
Activate
The host must be able to read the Capsule from its store (the same folder or bucket). Then:
crisptastic notify --peer host.internal:7443 --digest <sha256> \
--client-cert deployer.crt --client-key deployer.key --server-ca management-ca.crt
On success it prints "state": "active" with the domains. What the host does:
- fetches the ciphertext by digest and decrypts it with its identity;
- verifies the signature before reading the contents, against its
--trusted-signerkeys; - unpacks the public files into a new folder, moves secrets into locked memory, compiles the site and builds its runtime;
- only then switches the domain routes, atomically.
If any step fails, the response is an error with a code such as
capsule.signer_unauthorized, capsule.decrypt_failed or
deployment.runtime_build_failed, and the routes already serving are untouched.
Behaviours to know about:
- Replacing a site swaps the domains named by the new Capsule. A domain that the old Capsule served but the new one omits stays routed to the old version until the host restarts.
- Rollback is another
notifywith an earlier digest, as long as that object is still in the store. There is no history command. - Old unpacked folders of replaced deployments are not cleaned up on a standalone host.
- Sessions belong to the host, so activating a new version keeps visitors'
sessions. Sign-ins through the Wasm
arbaxcapability are the exception: they end on a redeploy or restart. - A standalone
notifysends only the digest. The cluster path additionally cross-checks length, domains and secret names; see Clustering.
Assets in production
By default, a deployed site's Capsule contains your asset files as ordinary project files. The preview asset publication workflow instead uploads assets to object storage or a CDN at deploy time and bakes their final URLs into the compiled site, so the host serves no asset bytes.
crisptastic assets plan ./my-site # show what would be uploaded; no network
crisptastic assets publish ./my-site --application-id <32 lowercase hex>
crisptastic deploy ./my-site --application-id <32 hex> ... # publishes automatically
Configured in crisptastic.toml under [asset_publication]:
[asset_publication.paths."/"]
provider = "cdn"
[asset_publication.providers.cdn]
type = "s3" # or "cloudflare-images"
bucket = "my-bucket"
region = "auto"
endpoint = "https://<account>.r2.cloudflarestorage.com"
public_base_url = "https://assets.example.com"
key_prefix = "my-site"
access_key_env = "S3_ACCESS_KEY"
secret_key_env = "S3_SECRET"
- Credentials come from the deployer's environment, never from the file or the Capsule.
- Objects are content-addressed and nothing is ever deleted, so retries are safe and rollback keeps working.
crisptastic-lock.jsonrecords what was published. Commit it: production builds require it.- With this mode on, the Capsule contains only the compiled site, the sanitized configuration and any Wasm components, not your template sources or assets.
- Asset URLs in HTML and CSS must be literal; expressions are rejected.
- Published assets are served from another origin, and browsers refuse
cross-origin fonts unless that bucket sends a CORS header. Set one on the bucket,
or embed fonts in your CSS as
data:URLs and allowdata:infont_src. - Image variants are requested as
<image-path>/<variant>; development serves the original image.
This workflow is labelled customer preview; its full release gate is open.
A deployment checklist
crisptastic check .passes, and every form works against a production host's CSRF rules (see Sessions and forms).[deployment] domainslists every domain, and DNS and TLS for them point at your reverse proxy.- Secret names are uppercase and exported in the deploy environment.
- Every host that may serve the site is listed as a
--recipient. crisptastic deploysucceeded; record the digest.- The Capsule is visible to the host's store.
crisptastic notifyreturnsactive.- Load each domain over HTTPS through the proxy.