---
id: w1/m21/t001
title: static_site service type — CRD/spec + build → object-store/CDN origin
worker: worker1
status: done
estimate: 90m
depends_on: []
---

## Objective

Introduce a `static_site` service type: the spec recognizes it, and the build pipeline produces a static bundle published to an object-store/CDN origin instead of an image → Deployment. This is the mechanism foundation; serving/edge rules are t002.

## Context

- Compute service types today (web/worker/cron from m15) reconcile to Deployments; static sites must **not** — they build → object storage (the CDN origin).
- Build-from-git already exists in-cluster (w1/m5, BuildKit Jobs); the static build reuses that build trigger but a different output sink.
- **Design decision (milestone README, 2026-07-11):** the origin is a NEW bucket `bex-static` in the existing Wasabi account (`TF_STATE_*` creds from `.env` — never `bex-tfstate`, which holds private state/backups). Wired S3-generic via the backup-vars pattern: `BEX_STATIC_S3_ENDPOINT` / `BEX_STATIC_S3_BUCKET` / `BEX_STATIC_S3_SECRET` (Secret created out-of-band from `.env`, like `etcd-backup-s3`); any unset ⇒ `static_site` unavailable. The endpoint is deployment-time config (CI uses Hetzner Object Storage) — nothing in code may assume Wasabi.
- **Prerequisite (out-of-band, one-time):** create the `bex-static` bucket before this task's code runs against a real cluster.

## Steps

1. Add a `static_site` service type + `spec.publishPath` (and any static-only build fields) to the `App`/service spec in `lego/types/`.
2. Read `BEX_STATIC_S3_ENDPOINT` / `BEX_STATIC_S3_BUCKET` / `BEX_STATIC_S3_SECRET` in the operator (any unset ⇒ reject `static_site` Apps with a clear status message).
3. Wire the operator to run the build for a static site and sync `publishPath` output to `s3://<bucket>/<app-id>/<revision>/` — revision-prefixed keys so cutover/rollback is repointing the prefix.
4. Skip Deployment/Service creation for static sites; record the served origin/URL in status.
5. Document the env vars (CLAUDE.md env table) and the out-of-band bucket + Secret creation (docs), mirroring the `etcd-backup-s3` runbook shape.

## Files

- `lego/types/…` (service type + publishPath)
- `lego/operator/internal/controller/…` (static build → object-store path)
- `CLAUDE.md` env table + docs (new `BEX_STATIC_S3_*` vars, bucket/Secret runbook)

## Acceptance criteria

- [ ] A `static_site` App builds and its `publishPath` output lands in the object store under `<app-id>/<revision>/`; no Deployment is created.
- [ ] With any `BEX_STATIC_S3_*` unset, a `static_site` App is rejected with an explanatory status (no partial reconcile).
- [ ] `make test` green; deepcopy/CRD regenerated.

## Out of scope

- Serving, redirects/rewrites, custom headers (t002); API/UI (t003/t004).
