Architecture
Pagifier is a configuration-driven deployment platform for Kubernetes.
Developers write exactly one file — pagifier.yaml — and interact with
exactly one surface — the REST API. Everything else (images, manifests,
ingress, scaling, security) is generated by the platform.
System overview
Components
| Component | Binary | Responsibility |
|---|---|---|
| API server | cmd/pagifier-api | Auth, upload, config resolution, Release creation, history, rollback, logs, events, console |
| Operator | cmd/pagifier-operator | Release build orchestration; rendering and reconciling workload resources; progressive delivery |
| Builder | cmd/pagifier-builder | Build-job entrypoint: verify artifact, run build commands, generate Dockerfile, drive BuildKit, sign and attest |
The two CRDs
Application (applications.pagifier.io) is long-lived: one per project
per environment. It carries almost nothing — tenant, environment, and a
pointer (spec.activeRelease) to the Release that should be live.
Release (releases.pagifier.io) is immutable: one per upload. It
snapshots the artifact reference (bucket/key/SHA256) and the fully
resolved configuration after all template merging.
Because the Application only points at a Release, the delivery strategies fall out naturally:
- Rollback — point
activeReleaseat an older Release. - Canary — a parallel track serves staged traffic weights, gated on metrics.
- Blue/green — two color tracks; the Service selector flip is the cutover.
- Preview — an ephemeral Application per branch, TTL-bound.
Configuration resolution
Merging happens once, at upload time, in the API server. The result is embedded in the Release. The operator never re-reads templates for an existing Release, so publishing a new template version never changes already-deployed applications — it applies to the next upload.
Precedence, lowest to highest:
platform/defaults.yaml (S3)
runtime-images/<runtime>.yaml (S3)
templates/<name>/<version>.yaml (S3)
defaults/<environment>.yaml (S3)
project pagifier.yaml (user)
Merge semantics: objects merge key-by-key recursively; scalars and arrays
replace wholesale; an explicit null deletes the key. Unknown fields are
rejected everywhere — a typo is an error, never a silent no-op.
Build pipeline
The operator renders a Kubernetes Job per Release:
- An init container copies the static
pagifier-builderbinary,buildctl, andcosigninto a shared volume. - The build container — the runtime's toolchain image (e.g.
node:22-alpine), selected by platform config, never by inspection — downloadsapp.zip, verifies its SHA256 against the value recorded at upload, extracts it (rejecting traversal and symlinks), runs the configured build commands verbatim, and assembles a build context containing only the artifact directory. - BuildKit (a rootless sidecar, or the shared pool with its persistent layer cache) builds and pushes the image. No privileged containers, no Docker socket.
- When signing is configured, the image is signed and a SLSA provenance attestation is attached — hard failures both.
Images are content-addressed: the tag derives from a fingerprint of the artifact digest plus everything that reaches the image (build commands, env, base images). Identical inputs skip the build entirely and reuse the existing image, so revert commits deploy in seconds. Server configuration (nginx.conf) is not baked into the image; it mounts from a ConfigMap, so a headers or cache change redeploys without rebuilding.
Multi-tenancy and security
- Namespace per tenant per environment (
pgf-<tenant>-<env>), labeled for Pod Security Admissionrestricted. - API tokens map to tenants; all reads and writes are tenant-scoped.
- Rendered pods: non-root, read-only rootfs, no capabilities, no service-account token, seccomp RuntimeDefault.
- Default-deny NetworkPolicy admitting only the ingress controller namespace and same-namespace peers — selecting the application label so it covers canary and blue/green tracks too.
- Artifact SHA256 recorded at upload and re-verified in the build job; optional per-tenant cosign signature enforcement.
- Every state-changing API call lands in the audit table.
State ownership
| State | Owner | Why |
|---|---|---|
| Desired state (what should run) | Kubernetes CRs | Declarative, watchable, RBAC-scoped |
| Deployment history & audit | PostgreSQL | Survives CR garbage collection; queryable |
| Artifacts | S3 | Durable, versioned by key, lifecycle-manageable |
| Platform behavior (templates, profiles, gates) | S3 | Changeable without redeploying Pagifier |
Observability
- Prometheus metrics from the API (request counts/latency) and the operator (controller-runtime reconcile metrics).
- OpenTelemetry traces spanning upload → operator → build pod, propagated through a Release annotation.
- Structured JSON logs everywhere; Kubernetes Events on every transition; build logs captured onto failed Releases.