# guv.yaml reference

> Every field of the Guv manifest, generated from the JSON Schema.

Guv manifest, version 1. One file at the repository root describes every service, database, volume, domain,
cron job and environment of a project. Secrets never live in this file; it holds only references
(`secret://name`). References of the form `${{ … }}` resolve at deploy time. Removing a database or volume from the
file never deletes it: `guv plan` shows it as orphaned and it is retained.

Add this line to the top of the file and most editors will autocomplete and check it as you type:

```yaml
$schema: https://guv.dev/schema/v1.json
```

The schema itself is published at [https://guv.dev/schema/v1.json](https://guv.dev/schema/v1.json). Check a file locally with `guv validate`, see what would change with `guv plan`, and make it so with `guv apply`.

## The smallest useful file

```yaml
# The smallest useful guv.yaml: one web service, built by auto-detection, deployed from the default branch
# to a single production environment.
$schema: https://guv.dev/schema/v1.json
version: 1
project: my-app

services:
  web:
    port: 3000
```

## A complete example

A real-world manifest for a care provider: a web app, a private API, a worker, a nightly job, highly available Postgres with masking for previews, and a spend cap.

```yaml
# guv.yaml
$schema: https://guv.dev/schema/v1.json
version: 1
project: primecarers
region: lon1                       # cell selection is ours, region is the user's
residency: uk-only                 # enforced for compute, volumes, backups, logs
classification: special-category   # turns on PII log masking, branch masking, dedicated pool eligibility

environments:
  production:
    branch: main
    protection: { require_approval: true }        # deploys/changes by agents need human OK
  staging:
    branch: develop
  preview:
    pull_requests: true
    ttl: 72h
    spend_cap: 5GBP
    databases: branch-masked                     # ZFS clone + masking rules below

defaults:
  sleep_when_idle: false
  resources: { cpu: 1, memory: 1GB }

services:
  web:
    root: apps/web
    watch: ["apps/web/**", "packages/ui/**", "pnpm-lock.yaml"]
    build:
      builder: railpack                           # railpack | dockerfile | image
      # dockerfile: apps/web/Dockerfile
    start: pnpm --filter web start
    port: 3000
    healthcheck: { path: /healthz, timeout: 10s }
    pre_deploy: pnpm --filter db migrate deploy
    domains:
      - www.primecarers.co.uk
      - primecarers.co.uk
    replicas: { min: 2, max: 6, scale_on: { cpu: 70%, concurrency: 80 } }
    resources: { cpu: 2, memory: 2GB }            # caps; billed on actual use
    env:
      NODE_ENV: production
      DATABASE_URL: ${{ databases.main.pooled_url }}
      REDIS_URL: ${{ databases.cache.url }}
      INTERNAL_API: http://${{ services.api.internal_host }}:8080
      STRIPE_SECRET_KEY: secret://stripe-secret-key     # value set via CLI/UI, never in git
      NEXT_PUBLIC_POSTHOG_KEY: { value: phc_xxx, public: true }  # explicitly allowed in client bundle

  api:
    root: services/api
    build: { builder: dockerfile, dockerfile: services/api/Dockerfile }
    port: 8080
    private: true                                  # no public domain; internal network only
    resources: { cpu: 1, memory: 1GB }
    env:
      DATABASE_URL: ${{ databases.main.url }}
      JWT_SIGNING_KEY: secret://jwt-signing-key

  rota-worker:
    root: services/rota
    start: node dist/worker.js
    private: true
    volumes:
      - name: rota-cache
        mount: /data

  nightly-reports:
    root: jobs/reports
    cron: "0 2 * * *"                              # Europe/London
    timezone: Europe/London
    timeout: 30m

databases:
  main:
    engine: postgres
    version: "18"
    plan: ha
    storage: 50GB
    backups: { pitr: 30d }
    extensions: [pgvector, pg_trgm]
    maintenance_window: "Sun 02:00-04:00"
    masking:                                       # used for preview/branch clones
      clients.full_name: fake_name
      clients.nhs_number: null
      clients.date_of_birth: shift_days(±30)
      carers.email: hash_email
  cache:
    engine: valkey
    plan: standard

volumes:
  rota-cache: { size: 5GB, ha: false }

alerts:
  - name: api-errors
    when: logs(service=api, level=error) > 20/min
    notify: [email:ops@primecarers.co.uk, webhook:https://hooks.slack.com/...]

spend:
  monthly_cap: 400GBP
  on_cap: sleep-non-production
```

## Top-level fields

| Field | Type | What it does |
| --- | --- | --- |
| [`$schema`](/docs/manifest/schema/) | string | URL of this schema, for editor autocomplete. |
| [`version`](/docs/manifest/version/) * | `1` | Manifest format version. |
| [`project`](/docs/manifest/project/) * | [name](/docs/manifest/#name) | Project slug. |
| [`name`](/docs/manifest/name/) | string | Human-friendly project name shown in the dashboard. |
| [`region`](/docs/manifest/region/) | `lon1` | Region the project runs in. |
| [`residency`](/docs/manifest/residency/) | `uk-only`, `uk-eu` | Where compute, volumes, backups and logs may be stored. |
| [`classification`](/docs/manifest/classification/) | `standard`, `personal-data`, `special-category` | Data classification. |
| [`include`](/docs/manifest/include/) | list of string | Globs of per-service files (guv.service.yaml) to merge into this manifest. |
| [`environments`](/docs/manifest/environments/) | map of names to object | Environments by name. |
| [`defaults`](/docs/manifest/defaults/) | object | Defaults applied to every service before its own settings. |
| [`services`](/docs/manifest/services/) | map of names to object | Services by name. |
| [`databases`](/docs/manifest/databases/) | map of names to object | Managed databases by name. |
| [`volumes`](/docs/manifest/volumes/) | map of names to object | Persistent volumes by name, mounted by services through services.&lt;name>.volumes. |
| [`variables`](/docs/manifest/variables/) | [env](/docs/manifest/#env) | Shared variables available to every service in every environment, and to references as ${{ env.NAME }}. |
| [`alerts`](/docs/manifest/alerts/) | list of object | Alert rules. |
| [`spend`](/docs/manifest/spend/) | object | Monthly spend cap for the whole project. |
| [`canvas`](/docs/manifest/canvas/) | object | Presentation-only dashboard layout. |

\* required

## Value formats

These formats are used throughout the manifest.

### name

Lowercase name (letters, digits and hyphens, starting with a letter, up to 63 characters).

Pattern: `^[a-z][a-z0-9-]{0,62}$`

Examples: `web`, `rota-worker`

### duration

Duration such as `10s`, `30m`, `72h`, `30d` or `1h30m`.

Pattern: `^([0-9]+(ms|s|m|h|d))+$`

Examples: `10s`, `72h`

### size

Size in MB, GB or TB (powers of 1000 for billing, 1024 for limits).

Pattern: `^[0-9]+(\.[0-9]+)?\s?[MmGgTt][Bb]$`

Examples: `512MB`, `2GB`

### money

Amount in pounds sterling, written `400GBP`, `5.50 GBP` or `£400`.

Pattern: `^(£\s?[0-9]+(\.[0-9]{1,2})?|[0-9]+(\.[0-9]{1,2})?\s?GBP)$`

Examples: `400GBP`, `£10`

### percent

Percentage, as `70%` or a number from 1 to 100.

Examples: `70%`, `80`

### hostname

A fully qualified hostname you control. `*.example.com` needs DNS delegation for certificates.

Pattern: `^(\*\.)?([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$`

Examples: `www.primecarers.co.uk`

### value

A variable value: a literal; a reference `${{ databases.<n>.url|pooled_url|host|port|user|password }}`,
`${{ services.<n>.internal_host|public_url }}` or `${{ env.NAME }}`; or `secret://name` for a secret set separately
(`guv secrets set name`). A value that references a secret is itself secret. Use the object form to mark a value
public (allowed in client bundles) or available at build time.

Examples: `production`, `${{ databases.main.pooled_url }}`, `secret://stripe-secret-key`, `{"value":"phc_xxx","public":true}`

### env

Environment variables by name.

Examples: `{"NODE_ENV":"production","DATABASE_URL":"${{ databases.main.pooled_url }}"}`
