Alchemist Operations

Alchemist: Deploys and Domains

How deployments work on the Alchemist platform and how to attach custom domains.

| View as Markdown
Hunter Hodnett
Hunter Hodnett CPTO at Chipp
| 1 min read
# alchemist # platform # deploys-and-domains

Every project has a Live URL on the platform’s shared domain (for example your-project.on.chipp.ai). You can also attach one or more custom domains from the Domains tab.

How deploys happen

A deploy is triggered automatically whenever:

  • The agent’s branch merges into your project’s default branch.
  • You click Redeploy in the project’s detail view.
  • You push a commit to the default branch from outside the platform (after ejecting, or via a deploy token).

Each deploy runs a build sandbox that produces a container image, then the platform’s rollout controller pushes that image to the live pod behind the same URL. Crossover is a few seconds; in-flight requests finish on the old pod.

Deploy pacing: rapid pushes coalesce

Pushes are never rejected and never queue up into a backlog of rollouts. Every push is accepted immediately, and if another deploy for the same project is already waiting, the newer one replaces it: only the newest commit gets built. The older row stays visible in the deploys list marked superseded, with a pointer to the deploy that replaced it, so you can see what happened rather than watching a push disappear.

Consecutive deploys for one project are spaced by a short window (about five minutes). Push three times in two minutes and you get one build of the newest commit, not three builds. Because the newest commit already contains the earlier ones, nothing is lost.

A failed build does not consume that window, so a retry (or a fix you push straight after a failure) is picked up right away instead of waiting.

Deployment configuration

Everything about how your app runs is controlled by an optional .alchemist/deployment.yaml file in your repo. Omit the file, or any field in it, and you get the defaults.

yaml
# .alchemist/deployment.yaml
resourceClass: medium      # default: small
replicas: 2                # default: 1
env:
  LOG_LEVEL: info
  FEATURE_X: enabled
FieldValuesNotes
resourceClasssmall (default), medium, largexl and xxl need approval first: email support@chipp.ai.
replicas1 to 5 self-serve (default 1)Above 5 total pods needs approval: email support@chipp.ai with your expected concurrency profile. Each pod is billed while it runs.
envup to 50 keys, 4KB eachApplied after the platform’s own variables, so your value wins on a name collision.

The concurrency threshold counts total pods, so a worker block counts toward it: replicas: 4 plus a worker is 8 pods and needs approval, while replicas: 2 plus a single-replica worker is 3 and does not.

The schema is strict: an unknown field fails the build rather than being ignored, so a typo surfaces immediately instead of silently doing nothing.

Anything that affects security posture (service account, secret references, network policy, namespace) is deliberately not settable from this file. Those are platform-controlled, so a bad or malicious push can never request elevated access for itself.

Background workers

By default one pod both serves your traffic and runs everything else your app does. If your app has real background work (scheduled jobs, a queue consumer, a long poll) you can move it into its own pod by declaring a worker block:

yaml
worker:
  replicas: 1              # default: same as `replicas`

Your traffic pod then receives WORKER_ROLE=api and the worker pod receives WORKER_ROLE=worker, both running the same image.

⚠️

Your app has to honor WORKER_ROLE before you declare this. Both pods run the same code, so if your background work is not gated on that variable it will run in both pods: every scheduled job fires twice, every queue is consumed twice. Gate the work on WORKER_ROLE === "worker", and treat the variable being absent as “run everything” so the app still behaves correctly when deployed without a worker.

Apps scaffolded from an Alchemist template now ship this gating already: a src/lib/worker-role.ts helper plus every background starter wrapped in if (runsBackgroundWork). If your repo has that file, you can declare a worker as-is.

If your repo was forked from a template before that landed, it will not have the file, and the gating did not reach you retroactively. Check for src/lib/worker-role.ts first: if it is missing, port the gating before declaring a worker, or your background work runs in both pods.

Why bother: a deploy restarts the pod it rolls, so background work sharing the traffic pod gets interrupted on every deploy, and a long job competes with request latency. A separate worker pod also gets a much longer shutdown grace so in-flight work can finish, and it rolls without touching the pod serving your users.

A worker is an additional running pod and is billed as one, on the same CPU and memory basis as your app pod.

Force a redeploy

If your Live URL stops responding or you want to ship a config change without a code change, open the project and click Redeploy. The platform resolves the current HEAD SHA of your default branch, enqueues a fresh deploy at that SHA, and returns the existing in-flight deploy if one is already running (so the button is safe to click twice).

Auto-retry on transient failures

Builds and rollouts sometimes fail for reasons that have nothing to do with your code — registry rate-limits, transient auth blips, a flapping upstream mirror. The platform automatically retries a failed deploy up to two times (three attempts total) before surfacing the failure to you.

While the auto-retry is in flight you’ll see an amber banner: “Build failed — auto-retrying. No action needed.” If all three attempts fail, a manual Retry Deployment button appears. At that point the failure is usually something the agent can fix — open a ticket describing the error.

Custom domains

Add a domain from the Domains tab. The platform handles certificate issuance and renewal automatically. Domains are billed at the registrar’s wholesale price plus a flat platform markup; the price you pay is shown at purchase and frozen on your invoice — future markup changes never re-bill historical purchases.