---
title: "Deploy"
url: https://services.dev/docs/operate/deploy
captured: [operate-deploy.txt]
---
# Deploy

URL: https://services.dev/docs/operate/deploy

> A deploy builds your folder into a version, checks it against live, serves it in test mode and runs your brand's own suite against it. deploy --live sends that exact version to buyers, all at once or in stages that compare it job for job against live and abort on their own, and every job already running keeps the version it started on.

A deploy answers one question: is this exact version ready for a buyer's money? Every version passes through these states, and only the last two take real orders.

| State | What it is | Who reaches it |
|---|---|---|
| `draft` | your folder, built into a version: a name, `2026-11-02.1`, and the sha256 of the whole bundle | no one yet: `check` runs, and `check --against live` diffs it |
| `tested` | the draft, served in test mode at your real address, with your suite run against it | every `sk_test_` key, your staff and the [simulated market](https://services.dev/docs/people/pool/simulate.md) |
| `staged` | the tested version, taking a share of new orders beside live | the buyers whose orders fall in its share |
| `live` | the tested version, promoted by its sha256 | every buyer, member and agent, on live keys |

## Deploy to test

`deploy` takes the folder to `tested`. A version that fails `check` or a suite case stays a draft, and the run names the case:

```bash
npx services.dev deploy
```

```txt title="magic values: none"
api.lawyer · 9 files · 6 Services, 1 Pool, rules 2026-10 and the brand
  version 2026-11-02.1 · sha256:4e1b77c09a3f5d21… · draft
  check   0 failed · against live 2026-10-19.3: 1 additive, 0 breaking
  test    https://api.lawyer in test mode · every sk_test_ key reaches 2026-11-02.1 now
  suite   268 passed · 0 failed · 83 live rows wait for deploy --live
✓ tested  2026-11-02.1 · live is still 2026-10-19.3 · npx services.dev deploy --live sends it
```

The suite is [the one your brand publishes](https://services.dev/docs/api/verify.md), so the test a buyer could run against you is the test your deploy runs first. [Test and live](https://services.dev/docs/operate/testing.md) covers what test mode simulates and where each mode serves.

## Deploy live

`deploy --live` promotes the tested version. It never rebuilds: the sha256 that passed is the sha256 that serves.

```bash
npx services.dev deploy --live
```

```txt title="magic values: none"
api.lawyer · 2026-11-02.1 · sha256:4e1b77c09a3f5d21… · tested 14 minutes ago
  plan    Team (services.dev) · your plan · every gated key covered
  stripe  acct_1Q8Lw4 · charges on · payouts on
  model   your model key · 9 AI steps, 8 pinned to a build
  warns   trademark-response.draft names only the family claude-x: its build can move under you · pin one, claude-x@<date>
  domain  api.lawyer · verified · certificate renews 2027-01-30
  suite   83 live rows, read-only: auth, audience and leak cases · 0 failed · nothing ordered, nothing charged
✓ live    https://api.lawyer · api, mcp, cli, sdk, site, docs and every app
  pinned  41 matters in flight stay on 2026-10-19.3 until they end · new orders take 2026-11-02.1
  event   version.live · 2026-10-19.3 retires when its last matter, clock and timer close
```

Five things are checked first, and each refusal names its cure:

| Checked | Refused with | Cure |
|---|---|---|
| each Service's plan, derived from its keys | OFFER, with `plan`, `because` and `cure` | upgrade, or `check --fit` for the rewrite that stays put: [going live on a gated key](https://services.dev/docs/money/plans.md#going-live-on-a-gated-key) |
| no breaking change under pinned jobs or keys | `breaking_change` | [Versions](https://services.dev/docs/operate/versions.md#what-counts-as-breaking) |
| Stripe connected, with charges and payouts on | `stripe_unconnected` | `services.dev connect stripe` |
| a model key for live, and a model on every AI step | `model_unset` | `services.dev model set` |
| `Brand.domain` verified, on Launch and up | `domain_unverified` | the two DNS records `services.dev domains` prints |

The live suite rows then run read-only, so the auth, audience and leak cases test live keys without ordering anything. A rename under jobs that still have the step ahead is refused:

```bash
npx services.dev deploy --live   # the same tree, with a step renamed
```

```txt title="magic values: none"
✗ breaking_change · services/trademark-response.ts:18 · draft → write
  pinned  37 matters on 2026-11-02.1 still have draft ahead of them
  cure    keep the step's name, ship it under a new Service id, or deploy --live --breaking: pinned matters keep their version, and keys pinned to it keep its shapes
  docs    https://services.dev/docs/operate/versions#what-counts-as-breaking
```

## Pin the model to a build

A step's `model` pins a family or a build. `model: 'claude-x'` pins the family, and the provider may move its build under you. `model: 'claude-x@2026-11-08'` pins the build, so a new build reaches buyers only through a deploy you make. `deploy --live` warns on every reviewed step pinned only to a family, as the run above does for `draft`. The [incident](https://services.dev/docs/operate/incidents.md#a-model-that-changed-overnight) is what a family pin allows.

## Live in stages

`--stages` sends a share of new orders to the new version and leaves the rest on live. Every job keeps its version for life, so the console compares the two cohorts job for job: send-back rate, gate refusals, AI a job and time to each step.

```bash
npx services.dev deploy --live --stages 25%,100% --hold 7d   # 16 Nov: the draft's brief argues a prior registration
```

```txt title="magic values: none"
api.lawyer · 2026-11-16.1 · sha256:b83e0f5a72c1d946… · tested 9 minutes ago
  checks  plan, stripe, model, domain and the live suite, as deploy --live · 0 failed
  staged  25% of new orders take 2026-11-16.1 for 7 days · the rest take live 2026-11-09.1 · version.staged
  judged  the two cohorts, job for job: send-back rate, gate refusals, AI a job, time to each step
  aborts  on its own, the first alarm on the new cohort: new orders go back to 2026-11-09.1 · version.aborted
```

A stage promotes when its window passes, its cohort holds at least 10 jobs and no alarm fires on it. On a breach it aborts on its own: new orders go back to live, the staged jobs finish on their version, and `version.aborted` names the measure that fell. The cohorts are judged the way [evals](https://services.dev/docs/operate/quality.md#score-a-change-before-it-ships) judge a change before it ships and [holdouts](https://services.dev/docs/operate/line.md#holdouts) judge a line after it moves: three readings of one record. Stages run on [Team](https://services.dev/docs/money/plans.md#team).

```bash
open https://api.lawyer/console/deploys   # 23 Nov, 14:00
```

```txt title="magic values: none"
Deploys · live 2026-11-09.1 · staged 2026-11-16.1 at 25%, day 7 of 7
  cohort      trademark responses · 17 on 2026-11-16.1 · 58 on 2026-11-09.1
  send-backs  0 of 17 (0%) · 2 of 58 (3%)
  refusals    0 · 0
  AI a job    $0.74 · $0.71
  to respond  p50 4h 10m · 4h 25m
✓ promoted    2026-11-16.1 to 100% · no alarm on its cohort, 17 jobs over the 10 a stage needs · version.promoted
```

## Freeze the dates that matter

apis.tax ships nothing in the week before the filing date. `Brand.freeze` names the window and what still runs inside it:

```ts title="brand.ts"
freeze: [{ from: '2027-04-08', to: '2027-04-16', allow: ['hold', 'rollback', 'respond'] }],
```

```bash
npx services.dev deploy --live   # apis.tax, 12 Apr, inside its freeze
```

```txt title="magic values: none"
✗ frozen · brand.ts:14 · Brand.freeze holds live deploys from 2027-04-08 to 2027-04-16
  runs    hold, rollback and respond, as the freeze allows
  cure    deploy after 2027-04-16, or deploy --live --break-freeze --reason "<why>": written to the audit log
```

`--break-freeze --reason` ships anyway, and the reason goes on the [audit log](https://services.dev/docs/operate/observe.md#the-audit-log).

## The console's deploys

```bash
open https://api.lawyer/console/deploys
```

```txt title="magic values: none"
Deploys · live 2026-11-02.1 since 2 Nov 15:12 · Team
  2026-11-02.1  live     sha256:4e1b77c0  suite 268 + 83 live · 1 additive · 212 matters since
  2026-10-19.3  pinned   sha256:9c02d1aa  9 matters · 18 clocks · 63 timers · retires when they close
  2026-10-19.2  retired  sha256:51f0b6e2  nothing pinned since 30 Oct
  actions   diff against live · roll back to 2026-10-19.3 · repin its 9 matters
```

The first live deploy, start to finish, with Stripe connected and the first payout: [take the first real job](https://services.dev/docs/start/live.md).

## Controls

| Call (CLI) | API | Event | On the console | Plan |
|---|---|---|---|---|
| `deploy` | `POST https://services.dev/v1/deploys` | `version.deployed`, `version.tested`, `version.failed` | `/console/deploys` | every plan |
| `deploy --live [--breaking]` | `POST https://services.dev/v1/deploys/:version/live` | `version.live` | `/console/deploys` | every plan |
| `deploy --live --stages <shares> --hold <span>` | `POST https://services.dev/v1/deploys/:version/live { stages, hold }` | `version.staged`, `version.promoted`, `version.aborted` | `/console/deploys` | [Team](https://services.dev/docs/money/plans.md#team) |
| `deploy --live --break-freeze --reason <why>` | `POST https://services.dev/v1/deploys/:version/live { break_freeze }` | `version.live` | `/console/audit` | [Team](https://services.dev/docs/money/plans.md#team) |
| `connect stripe` | `POST https://services.dev/v1/stripe/connect` | `stripe.connected` | `/console/keys` | every plan |
| `model set <key>` | `PUT https://services.dev/v1/model` | `model.set` | `/console/keys` | every plan |
| `domains` | `GET https://services.dev/v1/domains` | `domain.verified` | `/console/keys` | [Launch](https://services.dev/docs/money/plans.md#launch) |

---

- Up: [Run your Service](https://services.dev/docs/operate.md)
- Previous: [Run your Service](https://services.dev/docs/operate.md)
- Next: [Test and live](https://services.dev/docs/operate/testing.md)
