# Deploy Pipeline & gipity.yaml

`gipity deploy` pushes your project live. It runs a multi-phase pipeline server-side and is the step most likely to surface mistakes - so deploy early and often, and **read the phase results every time**.

## The build loop

```
gipity add → edit files → gipity deploy dev --inspect → fix errors → repeat
```

`--inspect` runs the page-inspect report (console, failed resources, timing, overflow) on the live URL right after the deploy - one command instead of deploy-then-inspect. Deploy to `dev` while building; deploy to `prod` only when it works. See [app-debugging](app-debugging.md) for the inspect step and its standalone form.

## Commands

```bash
gipity deploy dev      # → https://dev.gipity.ai/<account>/<project>/
gipity deploy prod     # → https://app.gipity.ai/<account>/<project>/
```

| Flag | Effect |
|---|---|
| `--inspect [path]` | After a successful deploy, run `page inspect` on the deployed URL (optionally URL + `path`) in the same command |
| `--only <phases>` | Run only these phases, comma-separated (e.g. `--only database`) |
| `--force` | Re-run every phase, ignore checksums |
| `--optimize` | Force Vite build optimization on (it's already the default for prod; use this to optimize a dev deploy too). Only bundles `<script type="module">` references |
| `--no-optimize` | Skip build optimization, upload files as-is. The escape hatch for plain-HTML apps: a prod deploy optimizes by default, which aborts on a plain `<script src>` tag - pass this to deploy them as-is |
| `--source-dir <dir>` | Deploy from a directory other than the project root |
| `--no-sync` | Skip the sync-up that normally runs before deploy |
| `--json` | Machine-readable output (the `phases` array) |

## gipity.yaml - the deploy manifest

A project **without** `gipity.yaml` gets a plain static-file deploy (the `src/` folder → CDN). A project **with** `gipity.yaml` gets the full multi-phase pipeline. The `web-fullstack` and `api` templates ship one; `web-simple` does not.

Deploy is opt-in: it ships **only** the folders a phase names as its `source` (`src/` to the CDN, plus `migrations/`/`functions/` as backend). Anything else at the project root is kept but never deployed - that's by design. Put reference material you want to keep (diagrams, decks, notes) in `docs/` and throwaway scratch (conversions, intermediate output) in `tmp/` (sync-ignored, like `*_tmp/` and `.gipityscratch/`); neither bloats a deploy. The trap is a **bulky output dir dropped loose at the root** (`out/`, `vsd_out/`) - those aren't scratch, so they sync and re-hash on every deploy; move them to `docs/` (keep) or `tmp/` (toss). See the "Where files go" section of the Files-and-sync guidance.

```yaml
version: 1

deploy:
  phases:
    # Static frontend → CDN
    - name: files
      type: static
      source: src

    # SQL migrations → user database (idempotent, each runs once)
    - name: database
      type: sql
      source: migrations
      database: {{DATABASE}}

    # Serverless functions → HTTP endpoints
    - name: functions
      type: functions
      source: functions
      function_definitions:
        - name: list-items
          auth: public
          tables: [items]            # grant DB access; add fetch_domains: [...] to call out

    # Automated workflows → reconciled into real workflows + scheduled
    - name: workflows
      type: workflows
      source: workflows
```

### Phases

| Phase `type` | Source | What it does |
|---|---|---|
| `static` | `src/` | Uploads frontend files to the CDN |
| `sql` | `migrations/` | Auto-creates the database and runs each migration once (idempotent). It also re-runs seed files **only if the phase declares an explicit `seed: <dir>` key** - there is no automatic `seeds/` folder; `migrations/` is the only default source. To keep reference rows alive through `gipity test`'s DB reset, see `test.preserve` in [app-testing](app-testing.md). |
| `functions` | `functions/` | Deploys each `.js` file as an HTTP endpoint with its declared permissions |
| `workflows` | `workflows/` | Parses each `.yaml` and creates/updates the workflow (idempotent), then arms its schedule |
| `records` | (declared inline) | Exposes existing DB tables via Gipity Records - sets each table's `auth_level` (`public` = anonymous writes), `searchable`, primary key. Idempotent. Place AFTER the `sql` phase. |
| `services` | (declared inline) | Sets the **billing mode** of your app's AI services (`llm`, `image`, `transcribe`, …) - who pays when the service is called. Idempotent. |

A `services` phase declares **who pays** for each AI service - the in-source counterpart of the `project_settings` tool, so an app's billing choice ships with its `gipity.yaml` and is reproduced on every deploy (to a fresh project, a fork, dev or prod) instead of living only as out-of-band server state:
```yaml
- name: services
  type: services
  service_definitions:
    - service: llm
      billing_mode: owner_pays   # you (the app owner) pay; no user login needed
    - service: image
      billing_mode: owner_pays
    - service: transcribe
      billing_mode: owner_pays
```
`billing_mode` is `user_pays` (the safe-to-share default - each signed-in user spends their own credits) or `owner_pays` (you subsidize it; anonymous/logged-out callers and serverless functions work too). Billing is **project-level, not per-deploy-target** - one declaration applies to both dev and prod. This phase owns only the billing mode; finer per-service config (allowed models, providers, caps) is still set with `project_settings` and is preserved untouched. See [app-llm](app-llm.md) for the billing-mode details.

> **No-sign-in app? Set `owner_pays` on _every_ service it calls - billing is per-service.** An anonymous/logged-out call to a `user_pays` service returns `401 LOGIN_REQUIRED` (there's no signed-in user to bill). The common footgun: setting `llm: owner_pays` and leaving the rest at the `user_pays` default, so text generation works but read-aloud (`tts`) / `image` / `sound` / `music` silently 401 for anonymous visitors. Grep your client/function code for `/services/<name>` to list everything the app calls, and add one `service_definitions` line per service. (If you'd rather visitors pay, gate the feature behind "Sign in with Gipity" and keep `user_pays`.) The 401 body carries a `hint` field naming the offending service.
>
> `location` and `notify` are exempt from all of this: neither is credit-billed, so they need no `service_definitions` entry and never 401 an anonymous caller (`notify` sends are owner-billed automatically). `email` likewise needs no `service_definitions` entry (it's always owner-billed, never `user_pays`), though unlike those two it *is* credit-billed - one credit per recipient sent.

A `records` phase is the declarative, agent-free way to make a table publicly writable (e.g. a contact form) - the table is declared inline, not from a source dir:
```yaml
- name: records
  type: records
  tables:
    - table: contact_messages
      auth_level: public   # public | member (default) | user
      searchable: false
```
Or flip one table without redeploying: `gipity records config <table> --auth public`.

A table can also declare **field metadata** - every write through the Records API (REST, workflow `record` steps, bulk) then coerces and validates the declared fields, with self-correcting error messages; undeclared columns pass through untouched. `title: true` marks the field that names the record in the English audit summaries on `/history`:
```yaml
- name: records
  type: records
  tables:
    - table: deals
      auth_level: user
      fields:
        - { name: name, type: text, required: true, title: true }
        - { name: stage, type: select, options: { values: [Lead, Proposal, Won, Lost] } }
        - { name: value, type: currency, label: Deal value }   # stored as JSONB { amountMicros, currencyCode }
        - { name: close_date, type: date }
```
Field types: `text`, `textarea`, `number`, `boolean`, `date` (YYYY-MM-DD), `select` (`options.values`), `json`, and the composites `currency` (`{ amountMicros, currencyCode }` - accepts a plain number of whole units), `name` (`{ firstName, lastName }` - accepts a full-name string), `emails` / `phones` / `links` (primary + additional, accept a bare string), `address` (street/city/state/postcode/country/lat/lng). Composite columns must be JSONB. `relation` describes an existing FK column (`options: { table, display }`) - the value stays the raw id and Postgres enforces integrity. Presentation keys for generated UIs: `hide`, `section`, `help` (field order = declaration order). A `required` field is enforced on create. Validation errors are 400s that state what valid input looks like, so an agent (or user) can self-correct. `GET /api/<appGuid>/schema/<table>` returns the live columns merged with this metadata - the one contract UI generators consume.

A `workflows` phase makes automations part of the build loop: edit `workflows/*.yaml`, redeploy, and the workflow is created or updated in place (matched by name) and scheduled. See [workflow](workflow.md) for the YAML shape.

### function_definitions

Functions auto-deploy as **public** endpoints, and the deploy pipeline **auto-adds an entry** to `gipity.yaml` for any function it finds undeclared. You only edit entries to grant more than the default:

| Field | Purpose |
|---|---|
| `name` | Function file name (without `.js`) |
| `auth` | `public` (no auth), `user` (any signed-in user), `member` (owner-only: you + anyone you add to the project - use for admin/approval endpoints) |
| `tables` | Database tables the function may read/write |
| `fetch_domains` | External hosts the function may `fetch` - **required** to call app services like `a.gipity.ai` |
| `services` | Declared app services (`llm`, `location`, ...) - documentation only; services are still called via `fetch` |

### auth_branding (optional) - style ONLY the "Sign in with Gipity" page

**Scope:** this styles **only** the [Sign in with Gipity](app-auth.md) login/consent pages (server-rendered by Gipity). It does **not** theme your app's own UI - your app's look comes from your own CSS. So `tagline` / `primary_color` here change the sign-in screen, nothing else.

By default the sign-in page shows Gipity defaults (your project name, Gipity orange, the Gipity wordmark). Add a top-level `auth_branding:` block - a sibling of `deploy:`, not a phase - and `gipity deploy` syncs it to your project so that page matches your app:

```yaml
version: 1

auth_branding:                  # styles the Sign in with Gipity page only
  app_name: Lilyboxd            # name shown ("Sign in to continue to Lilyboxd")
  primary_color: "#fea60b"      # accent for buttons/headings - MUST be quoted (see note)
  logo_url: https://media.gipity.ai/.../logo.png   # https only; replaces the Gipity wordmark
  tagline: Your film diary      # optional one-line subtitle

deploy:
  phases:
    - { name: files, type: static, source: src }
```

| Field | Rules |
|---|---|
| `app_name` | 1-100 chars. Overrides the project name on the page. |
| `primary_color` | `#RRGGBB` hex only (no 3-digit, no alpha). |
| `logo_url` | `https://` only, ≤2048 chars. Omit to show the Gipity wordmark. |
| `tagline` | ≤120 chars. |

All fields optional - set only what you want to override; the rest fall back to Gipity defaults. It's **declarative**: the block is the source of truth, so it replaces the stored values on each deploy (delete a field to revert it to default). It's checksummed like a phase - unchanged blocks are skipped - and reported as an `auth_branding` line in the deploy results. A bad value (e.g. an invalid color) fails only the `auth_branding` step with a `FIX gipity.yaml` message; the rest of your deploy still lands.

> **Quote the hex color.** In YAML a bare `#` starts a comment, so `primary_color: #fea60b` parses as empty. Always quote it: `primary_color: "#fea60b"`.

## Checksums - phases skip when unchanged

On re-deploy, a phase whose inputs haven't changed is skipped (you'll see it reported as unchanged). Use `--force` to re-run everything, or `--only <phase>` to target one phase.

The database phase verifies its checksums still describe the actual database: if the database was dropped, recreated, or emptied since the last deploy, the next deploy detects it, reports `database was recreated - schema re-applied`, and re-runs all migrations (and any configured `seed:` files) instead of skipping them.

## Read the phase results - every deploy

Each phase reports a status. Do not move on until they're all clean:

| Status | Meaning | Action |
|---|---|---|
| `ok` ✓ | Phase succeeded | None |
| `warning` ⚠ | Deprecated or mis-shaped input (usually `gipity.yaml`); summary starts with "FIX" | Treat as a TODO - fix the file and redeploy |
| `failed` ✗ | Phase did not complete | Fix before doing anything else - the deploy did not fully land |

A `warning` is not informational. Leaving it unresolved means something is already broken or about to be.

## Related skills

- [app-debugging](app-debugging.md) - inspect the deployed page and read function logs
- [app-development](app-development.md) - writing the functions the pipeline deploys
- [app-database](app-database.md) - the database the pipeline migrates: `db` helper, migrations, table permissions
- [app-testing](app-testing.md) - testing the deployed functions (`ctx.fn.call` / `callAs`, the isolated test DB)

