# Deploy & hosting

Cartbase builds and hosts your storefront. You send the app's source files
— one snapshot of the whole project — and Cartbase turns it into a
running site: first on a **permanent preview URL**, then, when you press
**Publish** (or call the publish endpoint), on the store's live domain.
You never touch build servers, DNS, or hosting configuration; the platform
provisions all of it on your first deploy.

Two URLs exist per store, both created automatically:

| Surface | URL | What serves there |
|---|---|---|
| Preview | `https://preview--{store-slug}.cartbase.net` | The newest deploy, always — the link never changes |
| Live | `https://{store-slug}.cartbase.net` | The last **published** deploy, and nothing else |

A failed build never replaces what's live or what's on preview — the
previous successful deploy keeps serving. Custom domains for the live site
attach in the admin (Settings → Domains) and don't change anything on this
page.

**These are Admin API endpoints** — they operate the store, so they
authenticate with an admin session (sign-in cookie), not the storefront
headers the rest of this corpus uses. In practice you'll deploy through
the admin UI (**Storefront** in the sidebar), the Cartbase CLI, or an
agent connection — all three are wrappers over exactly these calls.
Users who belong to several stores name the target store with an
`x-barter-store: <client-id>` header.

## What a deploy is

A deploy is a **full snapshot**: the complete file tree of your storefront
app, uploaded in one request. Snapshot semantics cut both ways — a file
you stop sending is removed from the deployed site. There is no
partial-update mode; your working directory is the truth, every time.

Bundle rules (enforced server-side; a violation fails the whole deploy
with a readable `400` before anything is built):

- ≤ 1500 files, ≤ 3 MB per file, ≤ 25 MB total (base64-decoded sizes)
- `package.json` must exist at the bundle root — the app must be a
  buildable Next.js project (`next build`)
- File content travels base64-encoded (`content_base64`), so binary
  assets — images, fonts — work
- Never include: `node_modules/` (dependencies install at build time),
  `.next/` (build output is generated), `.git/`, any `.env*` file
- Not allowed in storefront bundles: `middleware.*`, `app/api/` or
  `pages/api/` routes, `vercel.json` — a storefront is pages plus the
  `@cartbase/storefront` package; server seams and hosting config are
  platform territory. The CORS proxy from the runbook is a
  `next.config` rewrite, which is fine.

Error codes you can hit: `empty_bundle`, `bundle_too_large`,
`invalid_bundle_file`, `invalid_path`, `blocked_path`, `duplicate_path`,
`invalid_encoding`, `file_too_large`, `missing_package_json`.

## Environment — provided, not configured

Hosted storefronts receive the three runbook inputs automatically at
build time; do **not** put them in the bundle (`.env*` files are blocked
anyway):

| Variable | Value |
|---|---|
| `NEXT_PUBLIC_BARTER_URL` | The store's API origin |
| `NEXT_PUBLIC_BARTER_CLIENT_ID` | The store's client id |
| `NEXT_PUBLIC_BARTER_PUBLISHABLE_KEY` | The store's publishable key, when one exists |

Only these public values ever reach a storefront build — secret keys are
never injected, so code that expects one is a design error.

## Deploy to preview

`POST /api/admin/storefront/deploys` — the one ingestion door. The first
call on a store also provisions its hosting (takes a few extra seconds);
every later call is just a deploy.

```bash
# doc-noexec — admin-session auth; run from an authenticated context.
curl -s -X POST "$BASE/api/admin/storefront/deploys" \
  -H "content-type: application/json" \
  -d '{
    "files": [
      { "path": "package.json",  "content_base64": "<base64>" },
      { "path": "app/layout.js", "content_base64": "<base64>" },
      { "path": "app/page.js",   "content_base64": "<base64>" }
    ],
    "message": "homepage copy update"
  }'
```

Response `201`:

```json
{
  "deploy": {
    "id": "sfd_01J…",
    "target": "preview",
    "status": "building",
    "url": "https://preview--my-store.cartbase.net",
    "message": "homepage copy update",
    "file_count": 3,
    "created_at": "2026-08-03T22:41:00.000Z"
  }
}
```

`message` (≤ 500 chars) is your label in the deploy history — write what
changed. `source` (`api` | `cli` | `mcp` | `admin` | `agent`) tags where
the deploy came from; omit it unless you're building tooling.

Statuses move `queued → building → ready` (or `failed` / `canceled`).
Poll the overview endpoint until the deploy you created is `ready`, then
open the preview URL. Builds of a small app land in roughly a minute;
`failed` rows carry a readable `error_message`.

## Overview — hosting status + deploy history

`GET /api/admin/storefront` returns the hosting record and the deploy
ledger, newest first:

```bash
# doc-noexec — admin-session auth.
curl -s "$BASE/api/admin/storefront"
```

```json
{
  "storefront": {
    "id": "sfp_01J…",
    "status": "ready",
    "production_url": "https://my-store.cartbase.net",
    "preview_url": "https://preview--my-store.cartbase.net",
    "last_error": null,
    "created_at": "…"
  },
  "deploys": [
    {
      "id": "sfd_01J…",
      "target": "preview",
      "status": "ready",
      "message": "homepage copy update",
      "url": "https://preview--my-store.cartbase.net",
      "source": "cli",
      "file_count": 3,
      "total_bytes": 1930,
      "error_message": null,
      "promoted_deploy_id": null,
      "created_at": "…",
      "updated_at": "…"
    }
  ]
}
```

`storefront` is `null` until the store's first deploy. `status:
"provisioning"` / `"failed"` (with `last_error`) describe hosting setup,
not builds; a failed provisioning resumes automatically on the next
deploy attempt.

## Publish — and rollback

`POST /api/admin/storefront/publish` promotes a **ready preview deploy**
to the live domain. This is the only path to production — nothing
deploys straight to live, no matter who's asking.

```bash
# doc-noexec — admin-session auth.
# Bare POST publishes the newest ready preview:
curl -s -X POST "$BASE/api/admin/storefront/publish"

# Naming an older deploy id IS the rollback mechanism:
curl -s -X POST "$BASE/api/admin/storefront/publish" \
  -H "content-type: application/json" \
  -d '{ "deploy_id": "sfd_01H…older…" }'
```

Response `201` is a new ledger row with `target: "production"` and
`promoted_deploy_id` pointing at the preview it promoted. Publishing is
near-instant — the snapshot was already built; the live domain switches
to it without a rebuild.

Rollback is not a separate feature: publish any earlier `ready` deploy
from the history and the live site is that snapshot again. Roll forward
the same way.

## Workflow summary

1. Build locally against your store (the [runbook](BUILD-A-STOREFRONT.md)).
2. Deploy → check the permanent preview URL.
3. Iterate — every deploy replaces preview, live is untouched.
4. Publish when it's right; the deploy history is your undo button.
