{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://guv.dev/schema/v1.json",
  "title": "guv.yaml",
  "description": "Guv manifest, version 1. One file at the repository root describes every service, database, volume, domain,\ncron job and environment of a project. Secrets never live in this file; it holds only references\n(`secret://name`). References of the form `${{ … }}` resolve at deploy time. Removing a database or volume from the\nfile never deletes it: `guv plan` shows it as orphaned and it is retained.\n",
  "type": "object",
  "required": [
    "version",
    "project"
  ],
  "additionalProperties": false,
  "patternProperties": {
    "^x-": {
      "description": "Extension fields, ignored by Guv.",
      "examples": [
        {
          "x-team": "platform"
        }
      ],
      "x-guv-since": "1"
    }
  },
  "examples": [
    {
      "version": 1,
      "project": "my-app",
      "services": {
        "web": {
          "port": 3000
        }
      }
    }
  ],
  "properties": {
    "$schema": {
      "type": "string",
      "description": "URL of this schema, for editor autocomplete.",
      "examples": [
        "https://guv.dev/schema/v1.json"
      ],
      "x-guv-since": "1"
    },
    "version": {
      "const": 1,
      "description": "Manifest format version. Breaking changes ship as version 2 with `guv migrate`.",
      "examples": [
        1
      ],
      "x-guv-since": "1"
    },
    "project": {
      "$ref": "#/$defs/name",
      "description": "Project slug. Created on first apply if it does not exist.",
      "examples": [
        "primecarers"
      ],
      "x-guv-since": "1"
    },
    "name": {
      "type": "string",
      "maxLength": 100,
      "description": "Human-friendly project name shown in the dashboard. Defaults to the slug.",
      "examples": [
        "Primecarers"
      ],
      "x-guv-since": "1"
    },
    "region": {
      "type": "string",
      "enum": [
        "lon1"
      ],
      "default": "lon1",
      "description": "Region the project runs in. Guv chooses the cell inside the region.",
      "examples": [
        "lon1"
      ],
      "x-guv-since": "1"
    },
    "residency": {
      "type": "string",
      "enum": [
        "uk-only",
        "uk-eu"
      ],
      "default": "uk-only",
      "description": "Where compute, volumes, backups and logs may be stored. `uk-only` data never leaves the UK, including backups.",
      "examples": [
        "uk-only"
      ],
      "x-guv-since": "1"
    },
    "classification": {
      "type": "string",
      "enum": [
        "standard",
        "personal-data",
        "special-category"
      ],
      "default": "standard",
      "description": "Data classification. `special-category` turns on PII log masking, makes masking mandatory on database branches and makes the project eligible for dedicated pools.",
      "examples": [
        "special-category"
      ],
      "x-guv-since": "1"
    },
    "include": {
      "type": "array",
      "description": "Globs of per-service files (`guv.service.yaml`) to merge into this manifest. Each file defines one service (see `$defs/serviceFile`).",
      "items": {
        "type": "string",
        "minLength": 1
      },
      "examples": [
        [
          "apps/*/guv.service.yaml"
        ]
      ],
      "x-guv-since": "1"
    },
    "environments": {
      "type": "object",
      "description": "Environments by name. If omitted, a single `production` environment deploys from the repository's default branch.",
      "propertyNames": {
        "$ref": "#/$defs/name"
      },
      "additionalProperties": {
        "$ref": "#/$defs/environment"
      },
      "examples": [
        {
          "production": {
            "branch": "main",
            "protection": {
              "require_approval": true
            }
          },
          "staging": {
            "branch": "develop"
          }
        }
      ],
      "x-guv-since": "1"
    },
    "defaults": {
      "$ref": "#/$defs/defaults",
      "description": "Defaults applied to every service before its own settings.",
      "x-guv-since": "1"
    },
    "services": {
      "type": "object",
      "description": "Services by name. A service with `cron` is a scheduled job; one with no `port` and no `cron` is a worker.",
      "propertyNames": {
        "$ref": "#/$defs/name"
      },
      "additionalProperties": {
        "$ref": "#/$defs/service"
      },
      "examples": [
        {
          "web": {
            "root": "apps/web",
            "port": 3000
          }
        }
      ],
      "x-guv-since": "1"
    },
    "databases": {
      "type": "object",
      "description": "Managed databases by name. Each environment gets its own instance (or a branch, for previews).",
      "propertyNames": {
        "$ref": "#/$defs/name"
      },
      "additionalProperties": {
        "$ref": "#/$defs/database"
      },
      "examples": [
        {
          "main": {
            "engine": "postgres",
            "plan": "standard"
          }
        }
      ],
      "x-guv-since": "1"
    },
    "volumes": {
      "type": "object",
      "description": "Persistent volumes by name, mounted by services through `services.<name>.volumes`.",
      "propertyNames": {
        "$ref": "#/$defs/name"
      },
      "additionalProperties": {
        "$ref": "#/$defs/volume"
      },
      "examples": [
        {
          "uploads": {
            "size": "10GB"
          }
        }
      ],
      "x-guv-since": "1"
    },
    "variables": {
      "$ref": "#/$defs/env",
      "description": "Shared variables available to every service in every environment, and to references as `${{ env.NAME }}`.",
      "x-guv-since": "1"
    },
    "alerts": {
      "type": "array",
      "description": "Alert rules.",
      "items": {
        "$ref": "#/$defs/alert"
      },
      "examples": [
        [
          {
            "name": "api-errors",
            "when": "logs(service=api, level=error) > 20/min",
            "notify": [
              "email:ops@example.co.uk"
            ]
          }
        ]
      ],
      "x-guv-since": "1"
    },
    "spend": {
      "$ref": "#/$defs/spend",
      "description": "Monthly spend cap for the whole project.",
      "x-guv-since": "1"
    },
    "canvas": {
      "$ref": "#/$defs/canvas",
      "description": "Presentation-only dashboard layout. It never changes deployed resources or cost.",
      "examples": [
        "canvas:\n  layout: manual\n  direction: LR\n  nodes:\n    services.web: { x: 0, y: 0 }\n    services.api: { x: 320, y: 0 }\n    cron.nightly-report: { x: 320, y: 200, collapsed: true }\n    databases.main: { x: 640, y: 0 }\n  groups:\n    - { name: backend, label: Back end, nodes: [services.api, cron.nightly-report], colour: blue }\n  viewport: { x: 0, y: 0, zoom: 1 }"
      ],
      "x-guv-since": "1"
    }
  },
  "$defs": {
    "name": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9-]{0,62}$",
      "description": "Lowercase name (letters, digits and hyphens, starting with a letter, up to 63 characters).",
      "examples": [
        "web",
        "rota-worker"
      ],
      "x-guv-since": "1"
    },
    "duration": {
      "type": "string",
      "pattern": "^([0-9]+(ms|s|m|h|d))+$",
      "description": "Duration such as `10s`, `30m`, `72h`, `30d` or `1h30m`.",
      "examples": [
        "10s",
        "72h"
      ],
      "x-guv-since": "1"
    },
    "size": {
      "type": "string",
      "pattern": "^[0-9]+(\\.[0-9]+)?\\s?[MmGgTt][Bb]$",
      "description": "Size in MB, GB or TB (powers of 1000 for billing, 1024 for limits).",
      "examples": [
        "512MB",
        "2GB"
      ],
      "x-guv-since": "1"
    },
    "money": {
      "type": "string",
      "pattern": "^(£\\s?[0-9]+(\\.[0-9]{1,2})?|[0-9]+(\\.[0-9]{1,2})?\\s?GBP)$",
      "description": "Amount in pounds sterling, written `400GBP`, `5.50 GBP` or `£400`.",
      "examples": [
        "400GBP",
        "£10"
      ],
      "x-guv-since": "1"
    },
    "percent": {
      "description": "Percentage, as `70%` or a number from 1 to 100.",
      "oneOf": [
        {
          "type": "string",
          "pattern": "^[0-9]{1,3}%$"
        },
        {
          "type": "integer",
          "minimum": 1,
          "maximum": 100
        }
      ],
      "examples": [
        "70%",
        80
      ],
      "x-guv-since": "1"
    },
    "hostname": {
      "type": "string",
      "maxLength": 253,
      "pattern": "^(\\*\\.)?([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z][a-z0-9-]{0,61}[a-z0-9]$",
      "description": "A fully qualified hostname you control. `*.example.com` needs DNS delegation for certificates.",
      "examples": [
        "www.primecarers.co.uk"
      ],
      "x-guv-since": "1"
    },
    "envValue": {
      "description": "A variable value: a literal; a reference `${{ databases.<n>.url|pooled_url|host|port|user|password }}`,\n`${{ services.<n>.internal_host|public_url }}` or `${{ env.NAME }}`; or `secret://name` for a secret set separately\n(`guv secrets set name`). A value that references a secret is itself secret. Use the object form to mark a value\npublic (allowed in client bundles) or available at build time.\n",
      "anyOf": [
        {
          "type": "string",
          "maxLength": 32768
        },
        {
          "type": "number"
        },
        {
          "type": "boolean"
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "value"
          ],
          "properties": {
            "value": {
              "description": "The value (literal, reference or `secret://name`).",
              "type": [
                "string",
                "number",
                "boolean"
              ],
              "examples": [
                "phc_xxx"
              ],
              "x-guv-since": "1"
            },
            "public": {
              "type": "boolean",
              "default": false,
              "description": "Explicitly allowed to appear in client bundles. Any other variable whose value appears in built static assets blocks the deploy.",
              "examples": [
                true
              ],
              "x-guv-since": "1"
            },
            "build": {
              "type": "boolean",
              "default": false,
              "description": "Also available to the build, as a BuildKit secret mount (never a build argument).",
              "examples": [
                true
              ],
              "x-guv-since": "1"
            }
          }
        }
      ],
      "examples": [
        "production",
        "${{ databases.main.pooled_url }}",
        "secret://stripe-secret-key",
        {
          "value": "phc_xxx",
          "public": true
        }
      ],
      "x-guv-since": "1"
    },
    "env": {
      "type": "object",
      "description": "Environment variables by name.",
      "propertyNames": {
        "pattern": "^[A-Za-z_][A-Za-z0-9_]*$"
      },
      "additionalProperties": {
        "$ref": "#/$defs/envValue"
      },
      "examples": [
        {
          "NODE_ENV": "production",
          "DATABASE_URL": "${{ databases.main.pooled_url }}"
        }
      ],
      "x-guv-since": "1"
    },
    "resources": {
      "type": "object",
      "additionalProperties": false,
      "description": "Limits per replica. You are billed for actual use, not for limits.",
      "properties": {
        "cpu": {
          "type": "number",
          "minimum": 0.1,
          "maximum": 32,
          "description": "vCPU limit, in steps of 0.1.",
          "examples": [
            1,
            0.5
          ],
          "x-guv-since": "1"
        },
        "memory": {
          "$ref": "#/$defs/size",
          "description": "Memory limit (at least 128MB).",
          "examples": [
            "1GB"
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "cpu": 1,
          "memory": "1GB"
        }
      ],
      "x-guv-since": "1"
    },
    "healthcheck": {
      "description": "How Guv decides a new deployment is healthy before moving traffic. A string is shorthand for an HTTP path.",
      "oneOf": [
        {
          "type": "string",
          "pattern": "^/"
        },
        {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "type": {
              "type": "string",
              "enum": [
                "http",
                "tcp"
              ],
              "default": "http",
              "description": "HTTP GET expecting 2xx/3xx, or a TCP connect.",
              "examples": [
                "http"
              ],
              "x-guv-since": "1"
            },
            "path": {
              "type": "string",
              "pattern": "^/",
              "default": "/",
              "description": "Path for HTTP checks.",
              "examples": [
                "/healthz"
              ],
              "x-guv-since": "1"
            },
            "port": {
              "type": "integer",
              "minimum": 1,
              "maximum": 65535,
              "description": "Port to check; defaults to the service port.",
              "examples": [
                3000
              ],
              "x-guv-since": "1"
            },
            "timeout": {
              "$ref": "#/$defs/duration",
              "description": "Per-check timeout (up to 60s).",
              "examples": [
                "10s"
              ],
              "x-guv-since": "1"
            },
            "interval": {
              "$ref": "#/$defs/duration",
              "description": "Time between checks.",
              "examples": [
                "10s"
              ],
              "x-guv-since": "1"
            },
            "start_timeout": {
              "$ref": "#/$defs/duration",
              "description": "How long a new deployment has to become healthy (up to 15m).",
              "examples": [
                "5m"
              ],
              "x-guv-since": "1"
            }
          }
        }
      ],
      "examples": [
        "/healthz",
        {
          "path": "/healthz",
          "timeout": "10s"
        }
      ],
      "x-guv-since": "1"
    },
    "build": {
      "type": "object",
      "additionalProperties": false,
      "description": "How the service is built. Without this block Guv detects a Dockerfile or uses Railpack.",
      "properties": {
        "builder": {
          "type": "string",
          "enum": [
            "railpack",
            "dockerfile",
            "image"
          ],
          "default": "railpack",
          "description": "`railpack` auto-detects the stack, `dockerfile` builds a Dockerfile, `image` deploys a prebuilt image.",
          "examples": [
            "dockerfile"
          ],
          "x-guv-since": "1"
        },
        "dockerfile": {
          "type": "string",
          "description": "Path to the Dockerfile, relative to the repository root.",
          "examples": [
            "services/api/Dockerfile"
          ],
          "x-guv-since": "1"
        },
        "context": {
          "type": "string",
          "description": "Build context directory, relative to the repository root. Defaults to `root`.",
          "examples": [
            "services/api"
          ],
          "x-guv-since": "1"
        },
        "image": {
          "type": "string",
          "description": "Image to deploy when `builder` is `image` (Docker Hub, GHCR or a private registry).",
          "examples": [
            "ghcr.io/acme/api:1.4.2"
          ],
          "x-guv-since": "1"
        },
        "registry_credentials": {
          "type": "string",
          "pattern": "^secret://[A-Za-z_][A-Za-z0-9_-]{0,127}$",
          "description": "Secret holding credentials for a private registry.",
          "examples": [
            "secret://ghcr-token"
          ],
          "x-guv-since": "1"
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "builder": {
                "const": "image"
              }
            },
            "required": [
              "builder"
            ]
          },
          "then": {
            "required": [
              "image"
            ]
          }
        }
      ],
      "examples": [
        {
          "builder": "dockerfile",
          "dockerfile": "services/api/Dockerfile"
        }
      ],
      "x-guv-since": "1"
    },
    "scaleOn": {
      "type": "object",
      "additionalProperties": false,
      "description": "Targets for horizontal autoscaling between `min` and `max`.",
      "properties": {
        "cpu": {
          "$ref": "#/$defs/percent",
          "description": "Average CPU use, as a percentage of the limit.",
          "examples": [
            "70%"
          ],
          "x-guv-since": "1"
        },
        "memory": {
          "$ref": "#/$defs/percent",
          "description": "Average memory use, as a percentage of the limit.",
          "examples": [
            "80%"
          ],
          "x-guv-since": "1"
        },
        "concurrency": {
          "type": "integer",
          "minimum": 1,
          "description": "In-flight requests per replica, measured at the edge.",
          "examples": [
            80
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "cpu": "70%",
          "concurrency": 80
        }
      ],
      "x-guv-since": "1"
    },
    "replicas": {
      "description": "Number of copies. An integer fixes the count; an object enables autoscaling.",
      "oneOf": [
        {
          "type": "integer",
          "minimum": 0,
          "maximum": 20
        },
        {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "min": {
              "type": "integer",
              "minimum": 0,
              "maximum": 20,
              "default": 1,
              "description": "Fewest replicas. 0 allows scaling to zero.",
              "examples": [
                2
              ],
              "x-guv-since": "1"
            },
            "max": {
              "type": "integer",
              "minimum": 1,
              "maximum": 20,
              "description": "Most replicas.",
              "examples": [
                6
              ],
              "x-guv-since": "1"
            },
            "scale_on": {
              "$ref": "#/$defs/scaleOn",
              "description": "What to scale on.",
              "x-guv-since": "1"
            }
          }
        }
      ],
      "examples": [
        2,
        {
          "min": 2,
          "max": 6,
          "scale_on": {
            "cpu": "70%"
          }
        }
      ],
      "x-guv-since": "1"
    },
    "volumeMount": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "mount"
      ],
      "description": "Mount a volume declared under `volumes`.",
      "properties": {
        "name": {
          "$ref": "#/$defs/name",
          "description": "Volume name.",
          "examples": [
            "rota-cache"
          ],
          "x-guv-since": "1"
        },
        "mount": {
          "type": "string",
          "pattern": "^/",
          "description": "Absolute mount path inside the service.",
          "examples": [
            "/data"
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "name": "rota-cache",
          "mount": "/data"
        }
      ],
      "x-guv-since": "1"
    },
    "domain": {
      "description": "A custom domain for the service, as a hostname or an object.",
      "oneOf": [
        {
          "$ref": "#/$defs/hostname"
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "host"
          ],
          "properties": {
            "host": {
              "$ref": "#/$defs/hostname",
              "description": "Hostname.",
              "examples": [
                "primecarers.co.uk"
              ],
              "x-guv-since": "1"
            },
            "redirect_to": {
              "$ref": "#/$defs/hostname",
              "description": "Redirect every request to this hostname (permanent redirect), instead of serving.",
              "examples": [
                "www.primecarers.co.uk"
              ],
              "x-guv-since": "1"
            }
          }
        }
      ],
      "examples": [
        "www.primecarers.co.uk",
        {
          "host": "primecarers.co.uk",
          "redirect_to": "www.primecarers.co.uk"
        }
      ],
      "x-guv-since": "1"
    },
    "service": {
      "$ref": "#/$defs/serviceBody",
      "unevaluatedProperties": false,
      "description": "A service. Every field is optional; Guv detects what it can. Under `environments.<env>.services` the same fields override the base service for that environment (deep merge).",
      "examples": [
        {
          "root": "apps/web",
          "port": 3000,
          "healthcheck": "/healthz"
        }
      ],
      "x-guv-since": "1"
    },
    "serviceBody": {
      "type": "object",
      "patternProperties": {
        "^x-": {
          "description": "Extension fields, ignored by Guv.",
          "examples": [
            {
              "x-owner": "web-team"
            }
          ],
          "x-guv-since": "1"
        }
      },
      "description": "The fields of a service, shared by `service` and `serviceFile` (which add the closed-object check).",
      "properties": {
        "description": {
          "type": "string",
          "maxLength": 500,
          "description": "What the service does, shown in the dashboard.",
          "examples": [
            "Public website and dashboard"
          ],
          "x-guv-since": "1"
        },
        "root": {
          "type": "string",
          "description": "Directory of the service in the repository.",
          "examples": [
            "apps/web"
          ],
          "x-guv-since": "1"
        },
        "watch": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Globs of paths that trigger a rebuild on push. Defaults to `<root>/**` plus shared lockfiles.",
          "examples": [
            [
              "apps/web/**",
              "packages/ui/**",
              "pnpm-lock.yaml"
            ]
          ],
          "x-guv-since": "1"
        },
        "build": {
          "$ref": "#/$defs/build",
          "description": "Build settings.",
          "x-guv-since": "1"
        },
        "start": {
          "type": "string",
          "description": "Start command. Detected when omitted.",
          "examples": [
            "pnpm --filter web start"
          ],
          "x-guv-since": "1"
        },
        "pre_deploy": {
          "type": "string",
          "description": "Command run once per deployment, in a separate VM with the same variables, before traffic moves (for migrations). Failure stops the rollout.",
          "examples": [
            "pnpm --filter db migrate deploy"
          ],
          "x-guv-since": "1"
        },
        "port": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535,
          "description": "Port the app listens on. Services with a port get a generated domain unless `private`.",
          "examples": [
            3000
          ],
          "x-guv-since": "1"
        },
        "healthcheck": {
          "$ref": "#/$defs/healthcheck",
          "description": "Health check.",
          "x-guv-since": "1"
        },
        "domains": {
          "type": "array",
          "description": "Custom domains. They attach to the production environment unless set under `environments.<env>.services.<svc>.domains`.",
          "items": {
            "$ref": "#/$defs/domain"
          },
          "examples": [
            [
              "www.primecarers.co.uk",
              "primecarers.co.uk"
            ]
          ],
          "x-guv-since": "1"
        },
        "private": {
          "type": "boolean",
          "default": false,
          "description": "No public domain; reachable only on the environment's private network as `<name>.guv.internal`.",
          "examples": [
            true
          ],
          "x-guv-since": "1"
        },
        "replicas": {
          "$ref": "#/$defs/replicas",
          "description": "Replicas and autoscaling.",
          "x-guv-since": "1"
        },
        "resources": {
          "$ref": "#/$defs/resources",
          "description": "CPU and memory limits.",
          "x-guv-since": "1"
        },
        "sleep_when_idle": {
          "type": "boolean",
          "description": "Scale to zero after 10 minutes without requests; the next request wakes it in 1–3 seconds.",
          "examples": [
            false
          ],
          "x-guv-since": "1"
        },
        "env": {
          "$ref": "#/$defs/env",
          "description": "Variables for this service.",
          "x-guv-since": "1"
        },
        "volumes": {
          "type": "array",
          "items": {
            "$ref": "#/$defs/volumeMount"
          },
          "description": "Volumes to mount. A service with a single-server volume runs one replica.",
          "examples": [
            [
              {
                "name": "rota-cache",
                "mount": "/data"
              }
            ]
          ],
          "x-guv-since": "1"
        },
        "cron": {
          "type": "string",
          "pattern": "^(@(hourly|daily|weekly|monthly|yearly)|(\\S+\\s+){4}\\S+)$",
          "description": "Five-field cron schedule. Makes the service a scheduled job; you are billed for run time only.",
          "examples": [
            "0 2 * * *"
          ],
          "x-guv-since": "1"
        },
        "timezone": {
          "type": "string",
          "default": "Europe/London",
          "description": "IANA time zone for `cron`.",
          "examples": [
            "Europe/London"
          ],
          "x-guv-since": "1"
        },
        "timeout": {
          "$ref": "#/$defs/duration",
          "description": "Maximum run time of each cron run.",
          "examples": [
            "30m"
          ],
          "x-guv-since": "1"
        },
        "concurrency_policy": {
          "type": "string",
          "enum": [
            "forbid",
            "allow",
            "replace"
          ],
          "default": "forbid",
          "description": "What to do if a cron run is still going when the next is due.",
          "examples": [
            "forbid"
          ],
          "x-guv-since": "1"
        },
        "restart": {
          "type": "object",
          "additionalProperties": false,
          "description": "Restart behaviour when the process exits.",
          "properties": {
            "policy": {
              "type": "string",
              "enum": [
                "on_failure",
                "always",
                "never"
              ],
              "default": "on_failure",
              "description": "When to restart.",
              "examples": [
                "on_failure"
              ],
              "x-guv-since": "1"
            },
            "max_retries": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "default": 10,
              "description": "Restarts before the service is marked crashed.",
              "examples": [
                10
              ],
              "x-guv-since": "1"
            }
          },
          "examples": [
            {
              "policy": "on_failure",
              "max_retries": 10
            }
          ],
          "x-guv-since": "1"
        },
        "overlap": {
          "$ref": "#/$defs/duration",
          "description": "How long the previous deployment keeps serving after traffic moves.",
          "examples": [
            "20s"
          ],
          "x-guv-since": "1"
        },
        "auto_rollback": {
          "type": "boolean",
          "description": "Roll back automatically if a new deployment crash-loops or fails health checks. Defaults to true in production.",
          "examples": [
            true
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "root": "apps/web",
          "port": 3000,
          "healthcheck": "/healthz"
        }
      ],
      "x-guv-since": "1"
    },
    "serviceFile": {
      "description": "Schema of a per-directory `guv.service.yaml`, pulled in with `include`. The same fields as a service, plus its name.",
      "$ref": "#/$defs/serviceBody",
      "unevaluatedProperties": false,
      "required": [
        "name"
      ],
      "properties": {
        "name": {
          "$ref": "#/$defs/name",
          "description": "Service name.",
          "examples": [
            "web"
          ],
          "x-guv-since": "1"
        },
        "$schema": {
          "type": "string",
          "description": "Schema URL, for editors.",
          "examples": [
            "https://guv.dev/schema/v1.json#/$defs/serviceFile"
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "name": "web",
          "root": "apps/web",
          "port": 3000
        }
      ],
      "x-guv-since": "1"
    },
    "protection": {
      "type": "object",
      "additionalProperties": false,
      "description": "Guard rails for an environment.",
      "properties": {
        "require_approval": {
          "type": "boolean",
          "description": "Deploys and changes by agents (and by roles not in `approver_roles`) need a human to approve with a passkey.",
          "examples": [
            true
          ],
          "x-guv-since": "1"
        },
        "approver_roles": {
          "type": "array",
          "items": {
            "type": "string",
            "enum": [
              "owner",
              "admin",
              "developer"
            ]
          },
          "default": [
            "owner",
            "admin"
          ],
          "description": "Roles that may approve and may change the environment directly.",
          "examples": [
            [
              "owner",
              "admin"
            ]
          ],
          "x-guv-since": "1"
        },
        "allowed_branches": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Only these branches may deploy to the environment.",
          "examples": [
            [
              "main"
            ]
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "require_approval": true
        }
      ],
      "x-guv-since": "1"
    },
    "environment": {
      "type": "object",
      "additionalProperties": false,
      "description": "An environment. Each has its own private network, variables, secrets and databases.",
      "properties": {
        "kind": {
          "type": "string",
          "enum": [
            "production",
            "staging",
            "preview",
            "development"
          ],
          "description": "Defaults from the name (`production`, `staging`, `preview`), otherwise `development`.",
          "examples": [
            "staging"
          ],
          "x-guv-since": "1"
        },
        "branch": {
          "type": "string",
          "description": "Git branch that deploys to this environment on push.",
          "examples": [
            "main"
          ],
          "x-guv-since": "1"
        },
        "pull_requests": {
          "type": "boolean",
          "default": false,
          "description": "Create a short-lived copy of this environment for every pull request.",
          "examples": [
            true
          ],
          "x-guv-since": "1"
        },
        "ttl": {
          "$ref": "#/$defs/duration",
          "description": "How long a preview environment lives after its last push.",
          "examples": [
            "72h"
          ],
          "x-guv-since": "1"
        },
        "spend_cap": {
          "$ref": "#/$defs/money",
          "description": "Monthly cap for this environment (for previews, per preview).",
          "examples": [
            "5GBP"
          ],
          "x-guv-since": "1"
        },
        "databases": {
          "description": "How databases are provided in this environment. `new` creates empty ones, `branch` clones production, `branch-masked` clones and applies masking rules first. A map sets the mode per database.",
          "oneOf": [
            {
              "type": "string",
              "enum": [
                "new",
                "branch",
                "branch-masked"
              ]
            },
            {
              "type": "object",
              "propertyNames": {
                "$ref": "#/$defs/name"
              },
              "additionalProperties": {
                "type": "string",
                "enum": [
                  "new",
                  "branch",
                  "branch-masked"
                ]
              }
            }
          ],
          "examples": [
            "branch-masked",
            {
              "main": "branch-masked",
              "cache": "new"
            }
          ],
          "x-guv-since": "1"
        },
        "protection": {
          "$ref": "#/$defs/protection",
          "description": "Approval rules.",
          "x-guv-since": "1"
        },
        "sleep_when_idle": {
          "type": "boolean",
          "description": "Default `sleep_when_idle` for services in this environment.",
          "examples": [
            true
          ],
          "x-guv-since": "1"
        },
        "variables": {
          "$ref": "#/$defs/env",
          "description": "Shared variables for this environment, overriding top-level `variables`.",
          "x-guv-since": "1"
        },
        "services": {
          "type": "object",
          "description": "Per-service overrides for this environment (deep-merged over the base service).",
          "propertyNames": {
            "$ref": "#/$defs/name"
          },
          "additionalProperties": {
            "$ref": "#/$defs/service"
          },
          "examples": [
            {
              "web": {
                "replicas": 1,
                "resources": {
                  "cpu": 0.5,
                  "memory": "512MB"
                }
              }
            }
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "branch": "develop"
        },
        {
          "pull_requests": true,
          "ttl": "72h",
          "spend_cap": "5GBP",
          "databases": "branch-masked"
        }
      ],
      "x-guv-since": "1"
    },
    "defaults": {
      "type": "object",
      "additionalProperties": false,
      "description": "Defaults for every service.",
      "properties": {
        "sleep_when_idle": {
          "type": "boolean",
          "description": "Default for `sleep_when_idle`.",
          "examples": [
            false
          ],
          "x-guv-since": "1"
        },
        "resources": {
          "$ref": "#/$defs/resources",
          "description": "Default limits.",
          "x-guv-since": "1"
        },
        "replicas": {
          "$ref": "#/$defs/replicas",
          "description": "Default replicas.",
          "x-guv-since": "1"
        },
        "healthcheck": {
          "$ref": "#/$defs/healthcheck",
          "description": "Default health check.",
          "x-guv-since": "1"
        },
        "restart": {
          "type": "object",
          "description": "Default restart behaviour (same fields as `services.<name>.restart`).",
          "additionalProperties": false,
          "properties": {
            "policy": {
              "type": "string",
              "enum": [
                "on_failure",
                "always",
                "never"
              ],
              "description": "When to restart.",
              "examples": [
                "on_failure"
              ],
              "x-guv-since": "1"
            },
            "max_retries": {
              "type": "integer",
              "minimum": 0,
              "maximum": 100,
              "description": "Restarts before the service is marked crashed.",
              "examples": [
                10
              ],
              "x-guv-since": "1"
            }
          },
          "examples": [
            {
              "policy": "on_failure"
            }
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "sleep_when_idle": false,
          "resources": {
            "cpu": 1,
            "memory": "1GB"
          }
        }
      ],
      "x-guv-since": "1"
    },
    "masking": {
      "type": "object",
      "description": "Masking rules applied to database branches before they are attached (mandatory for special-category projects).\nKeys are `table.column` or `schema.table.column`. Values: `fake_name`, `fake_email`, `fake_phone`, `fake_address`,\n`fake_postcode`, `hash`, `hash_email`, `redact`, `keep`, `shift_days(±N)`, `constant(text)`, or null to set NULL.\n",
      "propertyNames": {
        "pattern": "^[a-z_][a-z0-9_]*(\\.[a-z_][a-z0-9_]*){1,2}$"
      },
      "additionalProperties": {
        "oneOf": [
          {
            "type": "null"
          },
          {
            "type": "string",
            "pattern": "^(fake_name|fake_email|fake_phone|fake_address|fake_postcode|hash|hash_email|redact|keep|shift_days\\((±|\\+-|-)?[0-9]{1,4}\\)|constant\\(.{0,200}\\))$"
          }
        ]
      },
      "examples": [
        {
          "clients.full_name": "fake_name",
          "clients.nhs_number": null,
          "clients.date_of_birth": "shift_days(±30)",
          "carers.email": "hash_email"
        }
      ],
      "x-guv-since": "1"
    },
    "database": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "engine"
      ],
      "description": "A managed database. Backed up by default to two off-site locations, with daily restore tests.",
      "properties": {
        "engine": {
          "type": "string",
          "enum": [
            "postgres",
            "valkey",
            "mysql"
          ],
          "description": "Database engine. Valkey is Redis-compatible.",
          "examples": [
            "postgres"
          ],
          "x-guv-since": "1"
        },
        "version": {
          "type": "string",
          "description": "Major version. Postgres defaults to 18 (17 supported).",
          "examples": [
            "18"
          ],
          "x-guv-since": "1"
        },
        "plan": {
          "type": "string",
          "enum": [
            "starter",
            "standard",
            "ha",
            "ha-plus"
          ],
          "description": "`starter` one instance, 7-day PITR; `standard` 14-day PITR and daily restore tests; `ha` adds a synchronous replica with automatic failover and 30-day PITR; `ha-plus` adds read replicas and 35-day PITR.",
          "examples": [
            "ha"
          ],
          "x-guv-since": "1"
        },
        "storage": {
          "$ref": "#/$defs/size",
          "description": "Storage size. Grows online; cannot shrink.",
          "examples": [
            "50GB"
          ],
          "x-guv-since": "1"
        },
        "backups": {
          "type": "object",
          "additionalProperties": false,
          "description": "Backup settings beyond the plan defaults.",
          "properties": {
            "pitr": {
              "$ref": "#/$defs/duration",
              "description": "Point-in-time recovery window (up to 35d). Shortening it needs human approval.",
              "examples": [
                "30d"
              ],
              "x-guv-since": "1"
            }
          },
          "examples": [
            {
              "pitr": "30d"
            }
          ],
          "x-guv-since": "1"
        },
        "extensions": {
          "type": "array",
          "description": "Postgres extensions from the allowlist.",
          "items": {
            "type": "string",
            "enum": [
              "pgvector",
              "postgis",
              "pg_trgm",
              "pg_stat_statements",
              "pgcrypto",
              "uuid-ossp",
              "citext",
              "hstore",
              "pg_partman",
              "timescaledb",
              "pg_cron"
            ]
          },
          "uniqueItems": true,
          "examples": [
            [
              "pgvector",
              "pg_trgm"
            ]
          ],
          "x-guv-since": "1"
        },
        "maintenance_window": {
          "type": "string",
          "pattern": "^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) ([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]$",
          "description": "Weekly window (UK time) for patching and maintenance. Defaults to daily 02:00–05:00.",
          "examples": [
            "Sun 02:00-04:00"
          ],
          "x-guv-since": "1"
        },
        "masking": {
          "$ref": "#/$defs/masking",
          "description": "Masking rules for branches.",
          "x-guv-since": "1"
        },
        "pooling": {
          "type": "boolean",
          "default": true,
          "description": "Put a connection pooler in front (Postgres). `pooled_url` uses it; `url` is direct.",
          "examples": [
            true
          ],
          "x-guv-since": "1"
        },
        "public": {
          "type": "object",
          "additionalProperties": false,
          "description": "Public access through the TCP proxy. Off by default; turning it on needs human approval.",
          "properties": {
            "enabled": {
              "type": "boolean",
              "description": "Expose the database on a public port with TLS.",
              "examples": [
                false
              ],
              "x-guv-since": "1"
            },
            "allow_ips": {
              "type": "array",
              "items": {
                "type": "string"
              },
              "description": "CIDR allowlist (mandatory on Hobby).",
              "examples": [
                [
                  "203.0.113.4/32"
                ]
              ],
              "x-guv-since": "1"
            }
          },
          "examples": [
            {
              "enabled": true,
              "allow_ips": [
                "203.0.113.4/32"
              ]
            }
          ],
          "x-guv-since": "1"
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "engine": {
                "not": {
                  "const": "postgres"
                }
              }
            },
            "required": [
              "engine"
            ]
          },
          "then": {
            "not": {
              "anyOf": [
                {
                  "required": [
                    "extensions"
                  ]
                },
                {
                  "required": [
                    "masking"
                  ]
                }
              ]
            }
          }
        }
      ],
      "examples": [
        {
          "engine": "postgres",
          "version": "18",
          "plan": "ha",
          "storage": "50GB"
        },
        {
          "engine": "valkey",
          "plan": "standard"
        }
      ],
      "x-guv-since": "1"
    },
    "volume": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "size"
      ],
      "description": "A persistent volume on NVMe. Single-server unless `ha` is true.",
      "properties": {
        "size": {
          "$ref": "#/$defs/size",
          "description": "Size (hard cap). Grows online; cannot shrink.",
          "examples": [
            "5GB"
          ],
          "x-guv-since": "1"
        },
        "ha": {
          "type": "boolean",
          "default": false,
          "description": "Replicate synchronously to a second server for automatic failover (£0.20 per GB-month instead of £0.10).",
          "examples": [
            false
          ],
          "x-guv-since": "1"
        },
        "offsite_hourly": {
          "type": "boolean",
          "default": false,
          "description": "Hourly off-site backups (RPO 1 hour) instead of daily.",
          "examples": [
            false
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "size": "5GB",
          "ha": false
        }
      ],
      "x-guv-since": "1"
    },
    "alert": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "when",
        "notify"
      ],
      "description": "An alert rule.",
      "properties": {
        "name": {
          "$ref": "#/$defs/name",
          "description": "Rule name.",
          "examples": [
            "api-errors"
          ],
          "x-guv-since": "1"
        },
        "when": {
          "type": "string",
          "description": "Condition, e.g. `logs(service=api, level=error) > 20/min` or `metric(service=web, name=latency_p95) > 800ms for 5m`.",
          "examples": [
            "logs(service=api, level=error) > 20/min"
          ],
          "x-guv-since": "1"
        },
        "environment": {
          "$ref": "#/$defs/name",
          "description": "Environment the rule applies to. Defaults to production.",
          "examples": [
            "production"
          ],
          "x-guv-since": "1"
        },
        "notify": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "string",
            "pattern": "^(email|webhook|slack|discord):.+$"
          },
          "description": "Where to send notifications.",
          "examples": [
            [
              "email:ops@primecarers.co.uk",
              "webhook:https://hooks.slack.com/services/T000/B000/XXXX"
            ]
          ],
          "x-guv-since": "1"
        }
      },
      "examples": [
        {
          "name": "api-errors",
          "when": "logs(service=api, level=error) > 20/min",
          "notify": [
            "email:ops@primecarers.co.uk"
          ]
        }
      ],
      "x-guv-since": "1"
    },
    "spend": {
      "type": "object",
      "additionalProperties": false,
      "description": "Spend cap and what happens when it is reached.",
      "properties": {
        "monthly_cap": {
          "$ref": "#/$defs/money",
          "description": "Monthly cap. Warnings at 50% and 80%; the `on_cap` action at 100%.",
          "examples": [
            "400GBP"
          ],
          "x-guv-since": "1"
        },
        "on_cap": {
          "type": "string",
          "enum": [
            "sleep-non-production",
            "pause-all",
            "notify-only"
          ],
          "default": "sleep-non-production",
          "description": "What happens at 100%. `notify-only` (never pause production) needs a payment method and `acknowledge_no_pause`.",
          "examples": [
            "sleep-non-production"
          ],
          "x-guv-since": "1"
        },
        "warn_at": {
          "type": "array",
          "items": {
            "type": "integer",
            "minimum": 1,
            "maximum": 99
          },
          "default": [
            50,
            80
          ],
          "description": "Percentages at which to warn by email, banner and webhook.",
          "examples": [
            [
              50,
              80
            ]
          ],
          "x-guv-since": "1"
        },
        "acknowledge_no_pause": {
          "type": "boolean",
          "description": "Required true with `on_cap: notify-only`.",
          "examples": [
            true
          ],
          "x-guv-since": "1"
        }
      },
      "allOf": [
        {
          "if": {
            "properties": {
              "on_cap": {
                "const": "notify-only"
              }
            },
            "required": [
              "on_cap"
            ]
          },
          "then": {
            "required": [
              "acknowledge_no_pause"
            ],
            "properties": {
              "acknowledge_no_pause": {
                "const": true
              }
            }
          }
        }
      ],
      "examples": [
        {
          "monthly_cap": "400GBP",
          "on_cap": "sleep-non-production"
        }
      ],
      "x-guv-since": "1"
    },
    "canvas": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "layout": {
          "type": "string",
          "enum": [
            "auto",
            "manual"
          ],
          "default": "auto",
          "description": "Layout mode. In auto mode the dashboard arranges nodes in direction and pins listed nodes. In manual mode saved positions are used and unpositioned nodes are placed automatically.",
          "examples": [
            "auto"
          ],
          "x-guv-since": "1"
        },
        "direction": {
          "type": "string",
          "enum": [
            "LR",
            "TB"
          ],
          "default": "LR",
          "description": "Automatic arrangement direction. LR is left to right; TB is top to bottom.",
          "examples": [
            "LR"
          ],
          "x-guv-since": "1"
        },
        "nodes": {
          "type": "object",
          "maxProperties": 500,
          "propertyNames": {
            "$ref": "#/$defs/canvasNodeKey"
          },
          "additionalProperties": {
            "$ref": "#/$defs/canvasNode"
          },
          "description": "Pinned node positions, keyed by KIND.NAME.",
          "examples": [
            "services.web: { x: 0, y: 0 }"
          ],
          "x-guv-since": "1"
        },
        "groups": {
          "type": "array",
          "maxItems": 100,
          "items": {
            "$ref": "#/$defs/canvasGroup"
          },
          "description": "Named visual groups of canvas nodes.",
          "examples": [
            [
              {
                "name": "backend",
                "nodes": [
                  "services.api"
                ]
              }
            ]
          ],
          "x-guv-since": "1"
        },
        "viewport": {
          "$ref": "#/$defs/canvasViewport",
          "description": "Saved canvas viewport.",
          "examples": [
            "{ x: 0, y: 0, zoom: 1 }"
          ],
          "x-guv-since": "1"
        }
      },
      "description": "Dashboard canvas layout and presentation preferences.",
      "examples": [
        {
          "layout": "auto",
          "direction": "LR"
        }
      ],
      "x-guv-since": "1"
    },
    "canvasNodeKey": {
      "type": "string",
      "pattern": "^[a-z][a-z_]*\\.[a-z][a-z0-9-]{0,62}$",
      "description": "Canvas resource key in KIND.NAME form. Unknown kinds are accepted for forward compatibility.",
      "examples": [
        "services.web"
      ],
      "x-guv-since": "1"
    },
    "canvasNode": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "x",
        "y"
      ],
      "properties": {
        "x": {
          "type": "number",
          "minimum": -100000,
          "maximum": 100000,
          "description": "Horizontal canvas position.",
          "examples": [
            0
          ],
          "x-guv-since": "1"
        },
        "y": {
          "type": "number",
          "minimum": -100000,
          "maximum": 100000,
          "description": "Vertical canvas position.",
          "examples": [
            0
          ],
          "x-guv-since": "1"
        },
        "collapsed": {
          "type": "boolean",
          "description": "Whether the node is collapsed in the dashboard.",
          "examples": [
            true
          ],
          "x-guv-since": "1"
        }
      },
      "description": "Position and display state for one canvas node.",
      "examples": [
        {
          "x": 0,
          "y": 0
        }
      ],
      "x-guv-since": "1"
    },
    "canvasGroup": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "nodes"
      ],
      "properties": {
        "name": {
          "$ref": "#/$defs/name",
          "description": "Stable group name.",
          "examples": [
            "backend"
          ],
          "x-guv-since": "1"
        },
        "label": {
          "type": "string",
          "maxLength": 100,
          "description": "Display label.",
          "examples": [
            "Back end"
          ],
          "x-guv-since": "1"
        },
        "nodes": {
          "type": "array",
          "maxItems": 500,
          "items": {
            "$ref": "#/$defs/canvasNodeKey"
          },
          "description": "Canvas node keys in this group.",
          "examples": [
            [
              "services.api"
            ]
          ],
          "x-guv-since": "1"
        },
        "colour": {
          "type": "string",
          "pattern": "^(#[0-9a-fA-F]{6}|grey|blue|green|amber|red|purple|teal|pink)$",
          "description": "Group colour.",
          "examples": [
            "blue"
          ],
          "x-guv-since": "1"
        }
      },
      "description": "A named visual group on the canvas.",
      "examples": [
        {
          "name": "backend",
          "nodes": [
            "services.api"
          ]
        }
      ],
      "x-guv-since": "1"
    },
    "canvasViewport": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "x",
        "y",
        "zoom"
      ],
      "properties": {
        "x": {
          "type": "number",
          "description": "Horizontal viewport offset.",
          "examples": [
            0
          ],
          "x-guv-since": "1"
        },
        "y": {
          "type": "number",
          "description": "Vertical viewport offset.",
          "examples": [
            0
          ],
          "x-guv-since": "1"
        },
        "zoom": {
          "type": "number",
          "minimum": 0.1,
          "maximum": 4,
          "description": "Viewport zoom from 0.1 to 4.",
          "examples": [
            1
          ],
          "x-guv-since": "1"
        }
      },
      "description": "Saved canvas viewport coordinates and zoom.",
      "examples": [
        {
          "x": 0,
          "y": 0,
          "zoom": 1
        }
      ],
      "x-guv-since": "1"
    }
  },
  "x-guv-since": "1"
}
