Alchemist: Deploys and Domains
How deployments work on the Alchemist platform and how to attach custom 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.
# .alchemist/deployment.yaml
resourceClass: medium # default: small
replicas: 2 # default: 1
env:
LOG_LEVEL: info
FEATURE_X: enabled| Field | Values | Notes |
|---|---|---|
resourceClass | small (default), medium, large | xl and xxl need approval first: email support@chipp.ai. |
replicas | 1 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. |
env | up to 50 keys, 4KB each | Applied 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:
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.