# 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. ```yaml # .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: ```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. > **Warning:** **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.