{
  "name": "deploy",
  "title": "Deploy Pipeline & gipity.yaml",
  "description": "The deploy pipeline: gipity.yaml manifest, phases, dev vs prod, checksums, and reading phase results",
  "guid": "sk_plat_dply",
  "category": "App development",
  "requiredTools": [
    "project_deploy"
  ],
  "content": "# Deploy Pipeline & gipity.yaml\n\n`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**.\n\n## The build loop\n\n```\ngipity add → edit files → gipity deploy dev --inspect → fix errors → repeat\n```\n\n`--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.\n\n## Commands\n\n```bash\ngipity deploy dev      # → https://dev.gipity.ai/<account>/<project>/\ngipity deploy prod     # → https://app.gipity.ai/<account>/<project>/\n```\n\n| Flag | Effect |\n|---|---|\n| `--inspect [path]` | After a successful deploy, run `page inspect` on the deployed URL (optionally URL + `path`) in the same command |\n| `--only <phases>` | Run only these phases, comma-separated (e.g. `--only database`) |\n| `--force` | Re-run every phase, ignore checksums |\n| `--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 |\n| `--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 |\n| `--source-dir <dir>` | Deploy from a directory other than the project root |\n| `--no-sync` | Skip the sync-up that normally runs before deploy |\n| `--json` | Machine-readable output (the `phases` array) |\n\n## gipity.yaml - the deploy manifest\n\nA 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.\n\nDeploy 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.\n\n```yaml\nversion: 1\n\ndeploy:\n  phases:\n    # Static frontend → CDN\n    - name: files\n      type: static\n      source: src\n\n    # SQL migrations → user database (idempotent, each runs once)\n    - name: database\n      type: sql\n      source: migrations\n      database: {{DATABASE}}\n\n    # Serverless functions → HTTP endpoints\n    - name: functions\n      type: functions\n      source: functions\n      function_definitions:\n        - name: list-items\n          auth: public\n          tables: [items]            # grant DB access; add fetch_domains: [...] to call out\n\n    # Automated workflows → reconciled into real workflows + scheduled\n    - name: workflows\n      type: workflows\n      source: workflows\n```\n\n### Phases\n\n| Phase `type` | Source | What it does |\n|---|---|---|\n| `static` | `src/` | Uploads frontend files to the CDN |\n| `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). |\n| `functions` | `functions/` | Deploys each `.js` file as an HTTP endpoint with its declared permissions |\n| `workflows` | `workflows/` | Parses each `.yaml` and creates/updates the workflow (idempotent), then arms its schedule |\n| `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. |\n| `services` | (declared inline) | Sets the **billing mode** of your app's AI services (`llm`, `image`, `transcribe`, …) - who pays when the service is called. Idempotent. |\n\nA `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:\n```yaml\n- name: services\n  type: services\n  service_definitions:\n    - service: llm\n      billing_mode: owner_pays   # you (the app owner) pay; no user login needed\n    - service: image\n      billing_mode: owner_pays\n    - service: transcribe\n      billing_mode: owner_pays\n```\n`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.\n\n> **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.\n>\n> `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.\n\nA `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:\n```yaml\n- name: records\n  type: records\n  tables:\n    - table: contact_messages\n      auth_level: public   # public | member (default) | user\n      searchable: false\n```\nOr flip one table without redeploying: `gipity records config <table> --auth public`.\n\nA 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`:\n```yaml\n- name: records\n  type: records\n  tables:\n    - table: deals\n      auth_level: user\n      fields:\n        - { name: name, type: text, required: true, title: true }\n        - { name: stage, type: select, options: { values: [Lead, Proposal, Won, Lost] } }\n        - { name: value, type: currency, label: Deal value }   # stored as JSONB { amountMicros, currencyCode }\n        - { name: close_date, type: date }\n```\nField 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.\n\nA `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.\n\n### function_definitions\n\nFunctions 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:\n\n| Field | Purpose |\n|---|---|\n| `name` | Function file name (without `.js`) |\n| `auth` | `public` (no auth), `user` (any signed-in user), `member` (owner-only: you + anyone you add to the project - use for admin/approval endpoints) |\n| `tables` | Database tables the function may read/write |\n| `fetch_domains` | External hosts the function may `fetch` - **required** to call app services like `a.gipity.ai` |\n| `services` | Declared app services (`llm`, `location`, ...) - documentation only; services are still called via `fetch` |\n\n### auth_branding (optional) - style ONLY the \"Sign in with Gipity\" page\n\n**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.\n\nBy 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:\n\n```yaml\nversion: 1\n\nauth_branding:                  # styles the Sign in with Gipity page only\n  app_name: Lilyboxd            # name shown (\"Sign in to continue to Lilyboxd\")\n  primary_color: \"#fea60b\"      # accent for buttons/headings - MUST be quoted (see note)\n  logo_url: https://media.gipity.ai/.../logo.png   # https only; replaces the Gipity wordmark\n  tagline: Your film diary      # optional one-line subtitle\n\ndeploy:\n  phases:\n    - { name: files, type: static, source: src }\n```\n\n| Field | Rules |\n|---|---|\n| `app_name` | 1-100 chars. Overrides the project name on the page. |\n| `primary_color` | `#RRGGBB` hex only (no 3-digit, no alpha). |\n| `logo_url` | `https://` only, ≤2048 chars. Omit to show the Gipity wordmark. |\n| `tagline` | ≤120 chars. |\n\nAll 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.\n\n> **Quote the hex color.** In YAML a bare `#` starts a comment, so `primary_color: #fea60b` parses as empty. Always quote it: `primary_color: \"#fea60b\"`.\n\n## Checksums - phases skip when unchanged\n\nOn 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.\n\nThe 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.\n\n## Read the phase results - every deploy\n\nEach phase reports a status. Do not move on until they're all clean:\n\n| Status | Meaning | Action |\n|---|---|---|\n| `ok` ✓ | Phase succeeded | None |\n| `warning` ⚠ | Deprecated or mis-shaped input (usually `gipity.yaml`); summary starts with \"FIX\" | Treat as a TODO - fix the file and redeploy |\n| `failed` ✗ | Phase did not complete | Fix before doing anything else - the deploy did not fully land |\n\nA `warning` is not informational. Leaving it unresolved means something is already broken or about to be.\n\n## Related skills\n\n- [app-debugging](app-debugging.md) - inspect the deployed page and read function logs\n- [app-development](app-development.md) - writing the functions the pipeline deploys\n- [app-database](app-database.md) - the database the pipeline migrates: `db` helper, migrations, table permissions\n- [app-testing](app-testing.md) - testing the deployed functions (`ctx.fn.call` / `callAs`, the isolated test DB)\n"
}
