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:

KeyWho holds the private halfPurpose
Host identity (age X25519)The host onlyDecrypts Capsules. You encrypt to its public recipient (age1...).
Signing key (Ed25519)You or your CIProves a release is yours. The host trusts only listed public keys.
Management client certificate (mTLS)You or your CILets 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 .env or 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 check in 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 the API_BASE above, and pages cannot read them (context.env is 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 *_env secrets 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
  • --recipient is 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.
  • --out is required and must be outside the site folder.
  • --publish-store-dir DIR copies the result into a folder store; --publish-s3 uses 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:

  1. fetches the ciphertext by digest and decrypts it with its identity;
  2. verifies the signature before reading the contents, against its --trusted-signer keys;
  3. unpacks the public files into a new folder, moves secrets into locked memory, compiles the site and builds its runtime;
  4. 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 notify with 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 arbax capability are the exception: they end on a redeploy or restart.
  • A standalone notify sends 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.json records 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 allow data: in font_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

  1. crisptastic check . passes, and every form works against a production host's CSRF rules (see Sessions and forms).
  2. [deployment] domains lists every domain, and DNS and TLS for them point at your reverse proxy.
  3. Secret names are uppercase and exported in the deploy environment.
  4. Every host that may serve the site is listed as a --recipient.
  5. crisptastic deploy succeeded; record the digest.
  6. The Capsule is visible to the host's store.
  7. crisptastic notify returns active.
  8. Load each domain over HTTPS through the proxy.