Skip to main content

Templates and environment profiles

Templates are how the platform team defines the golden path: with a good template, a production deployment is four meaningful lines of pagifier.yaml. This page is the complete reference for authoring them.

What a template is

A template is a pagifier.yaml fragment stored in the config bucket at templates/<name>/<version>.yaml. There is no separate template language: anything a project file can say, a template can pre-say — and the project can override. Templates typically set:

ConcernKeys
Runtime selectionruntime
Build recipebuild.enabled, build.workDir, build.commands
What shipsartifact.directory
How it startsruntimeConfig.command, runtimeConfig.port
Caching policycache.static, cache.html
ProbeshealthCheck

They can also set resources, headers, env, autoscaling — anything — but environment concerns belong in environment profiles (below), and identity (name, environment, ingress.host) always comes from the project.

A complete template, annotated

# templates/react-static/2.yaml
runtime: static # selects runtime-images/static.yaml too

build:
enabled: true
workDir: .
commands: # run verbatim in the toolchain image
- npm ci
- npm run build

artifact:
directory: dist # ONLY this directory enters the image

cache:
static: {maxAge: 365d} # hashed assets are immutable
html: {maxAge: 5m}
# templates/go-api/1.yaml — server runtime with binary-only shipping
runtime: go

build:
enabled: true
commands:
- mkdir -p bin
- CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o bin/server .

artifact:
directory: bin # sources never enter the image

runtimeConfig:
command: /app/server # never inferred from go.mod
port: 8080

healthCheck: {path: /healthz}

Versioning and the publishing workflow

Version files are immutable once published; latest.yaml is a floating pointer:

  • template: react-static resolves templates/react-static/latest.yaml
  • template: react-static@2 pins templates/react-static/2.yaml

The safe rollout sequence:

# 1. Publish the new version alongside the old ones
aws s3 cp react-static-2.yaml s3://pagifier-platform/templates/react-static/2.yaml

# 2. Verify with a pinned project (template: react-static@2) or a dry run:
curl -H "Authorization: Bearer $TOKEN" --data-binary @test-project.yaml \
https://pagifier.company.com/v1/validate

# 3. Promote it as the default
aws s3 cp react-static-2.yaml s3://pagifier-platform/templates/react-static/latest.yaml

Nothing running changes at any of these steps: resolution happens at upload, so version 2 applies to each project's next deploy, and the Release records "template:react-static": "2" in templateVersions. Rolling a bad template back is re-copying the previous file over latest.yaml. Teams that need stability during a migration pin (@1) and unpin on their own schedule.

How the merge actually behaves

Five layers, lowest to highest precedence — later layers win per key:

platform/defaults.yaml → runtime-images/<runtime>.yaml
→ templates/<name>/<version>.yaml → defaults/<environment>.yaml
→ the project's pagifier.yaml

Three rules govern every collision:

  1. Maps merge recursively. A project overriding resources.memory keeps the template's resources.cpu.
  2. Scalars and lists replace wholesale. A project's build.commands replaces the template's list — commands never concatenate.
  3. Explicit null deletes. compression: null in a project removes whatever any lower layer set.

Worked example — these five inputs:

# platform/defaults.yaml # runtime-images/static.yaml
port: 80 runtime: static
security: {profile: recommended}
images:
# templates/react-static/2.yaml runtime: nginx:1.27-alpine
build: {enabled: true, build: node:22-alpine
commands: [npm ci, npm run build]}
artifact: {directory: dist}
cache: {static: {maxAge: 365d}}

# defaults/prod.yaml # project pagifier.yaml
ha: true version: 1
replicas: 3 name: frontend
resources: {cpu: 250m, template: react-static@2
memory: 256Mi} environment: prod
deploy: canary ingress: app.company.com
resources: {memory: 512Mi}

resolve to this snapshot (abbreviated):

name: frontend # project
runtime: static # template
port: 80 # platform defaults
build: {enabled: true, commands: [npm ci, npm run build]} # template
artifact: {directory: dist} # template
images: {runtime: nginx:1.27-alpine, build: node:22-alpine} # runtime def
ha: true # env profile
replicas: 3 # env profile
resources: {cpu: 250m, memory: 512Mi} # cpu from profile, memory from project
deploy: {strategy: canary} # env profile
ingress: {host: app.company.com, tls: true} # host from project, tls from defaults
security: {profile: recommended, headers: {}} # defaults, expanded from security/

Anyone can inspect this for a real release — GET …/releases/{id}/config returns the exact snapshot.

Environment profiles

defaults/<environment>.yaml is a pagifier.yaml fragment plus three profile-only keys (logging, podDisruptionBudget, rollingUpdate). Profiles are where operational policy lives:

# defaults/prod.yaml
ha: true
replicas: 3
autoRollback: true
resources: {cpu: 250m, memory: 256Mi}
podDisruptionBudget: {enabled: true, minAvailable: "50%"}
rollingUpdate: {maxSurge: "25%", maxUnavailable: "0"}
deploy:
strategy: canary
canary: {steps: [10, 50], stepInterval: 5m, analysis: [error-rate, latency-p95]}
# defaults/dev.yaml — fast and cheap
ha: false
replicas: 1
resources: {cpu: 50m, memory: 64Mi}
logging: {level: debug}

Developers get metric-gated canaries in prod and instant rolling updates in dev without writing a line of strategy configuration — and can still override any of it per project when justified.

Guardrails on template authors

The project file always wins, with two deliberate exceptions that protect the platform's guarantees:

  • Auth middleware annotations (middleware/authentication-*.yaml) override project ingress annotations — apps cannot annotate around authentication.
  • Validation applies to the merged result — a template cannot make an invalid configuration deployable (ascending canary steps, port ranges, mandatory runtime commands are all enforced post-merge).

And a failure guarantee for authors: a template with a typo'd key is rejected at upload with a did-you-mean error, never silently dropped — strict decoding applies to every layer, not just user files.