13 KiB
CLAUDE.md
Guidance for Claude Code (claude.ai/code) in this repository.
⚠️ CRITICAL — LIFE OR DEATH: zero code comments
This is the most important rule in this file. Violating it is treated as a failed task. Never write a comment that narrates, restates, or explains what the code already says — no "fetch the user", no "call the service", no explaining your change to the reviewer. Make the code self-explanatory instead: better names, smaller functions.
// BAD — restates the code. Never do this:
// fetch the user's repositories
const repos = await this.repositoryService.list(userId);
// GOOD — no comment; the code already says it:
const repos = await this.repositoryService.list(userId);
The only permitted comment captures something the code cannot express — a why, a constraint, a gotcha a reader would otherwise miss. These are rare. When in doubt, do not comment. Before finishing any edit, re-read your diff and delete every comment that fails this bar.
This rule is about code. Infra config (tf/, ansible/, kubernetes/) has no equivalent
remedy: the keys belong to Ceph, Terraform and Ansible, so there is no better name to pick and
no smaller function to extract. The bar is the same and restating a value is still banned, but
a comment recording why a value deviates from its default, or what breaks when it changes, is
usually the only place that knowledge can live.
What this is
Yucca is a multi-tenant backup service: OIDC-authenticated users get S3-backed restic repositories. Two things at once:
- Application (
packages/) — NestJS/Go/Svelte services. - Infrastructure (
tf/,ansible/,kubernetes/,charts/) — Ceph object storage, Talos K8s, Flux GitOps, networking. SeeREADME.mdand per-directoryREADME.mdfiles.
Tooling
- Everything runs through mise tasks (
.mise/tasks/scripts +.mise/config.tomlaggregates). mise pins every binary (node, pnpm, go, kubectl, helm, tilt, opentofu, ansible…) — do not assume a tool is on PATH outside of mise. - pnpm workspaces with a catalog (
pnpm-workspace.yaml): add/bump shared deps in the catalog, referenced as"catalog:"in eachpackage.json— not in individual packages. - Secrets come from 1Password via
op run;.envfiles containop://refs, never literal secrets.OP_ACCOUNTis set in.env(copy.env.example).
Common commands
mise dev # compose-based dev: deps, docker infra (postgres/minio/mock-oidc/victoria-*), all *:dev
mise <pkg>:dev # one service, e.g. mise web:dev, mise yucca-api:dev
mise docs:dev # docs site (packages/docs) on :36034
mise check # lint + format check + svelte-check + unit tests (= the `checks` CI job)
mise fix # autofix lint/format + lingui extract
mise build # build all packages
mise test # all unit tests (jest per NestJS pkg, vitest for web)
mise test:integration # integration tests (--jobs 1; needs infra up)
mise test:integration:k3d # CI split: the database-backed suites; :s3 = the Ceph-backed ones
mise test:e2e # e2e (needs the stack running); mise test:e2e:web = Playwright
mise <pkg>:test # one package; args after -- go to jest: mise yucca-api:test -- -t "name"
mise yucca-api:migrations <args> # DB migrations (@immich/sql-tools; yucca-api owns the schema)
k3d + Tilt (prod-shaped dev)
mise k3d:up → mise tilt:up (build images, render charts from kubernetes/apps/dev/local,
port-forward, live_update); tilt:down / k3d:down to tear down. Tilt's source of truth is the
Flux tree under kubernetes/ — see the extensively commented Tiltfile.
CI deploys resource subsets rather than the whole stack (mise tilt:ci still does everything):
tilt:ci-infra (integration — postgres, mock-oidc, victoria; no Ceph), then for e2e
tilt:ci-ceph (Rook only; builds no images, so it converges while the workspace installs)
followed by tilt:ci-e2e (the apps the e2e suites touch). Ceph converging outweighed every test
in the integration job, so that job dropped it and its S3-backed suites moved to the e2e one.
Ports: 5173 web · 3020 yucca-api · 3030 yucca-admin-api · 3010 michael ·
8092 mock-oidc · 8025 mailpit · 8093 mock-postmark · 9000 ceph rgw · 8428 victoria-metrics · 9428 victoria-logs.
Infrastructure commands
mise tf:plan / tf:init / tf:fmt # terragrunt (TF_STACK_DIR=tf/deployment/<partition>/<region>/<stack>)
mise k8s:validate # helm template + kubeconform + flux-local of the k8s tree (no cluster)
mise mgmt:render-inventory / mgmt:ansible
CI owns terraform applies (.github/workflows/infra.yml on merge to main). Locally you may
tf:plan but never tf:apply. Gotcha: unset the shell's stray AWS_CA_BUNDLE before planning —
it breaks the OVH S3 state backend. We own the futo-org/netbird provider
(../terraform-provider-netbird, pin ≥ 1.0.2); renaming a NetBird setup key forces replacement
(the API can't rename keys), regenerating its value.
Application architecture
Backend services are NestJS 11 + TypeScript: controllers → services → repositories,
Zod-validated env.ts, JWT auth guards via @AuthRoute(), OTel from @common/server
(imported at bootstrap; pino logs, OTLP to victoria-*).
| Service | Lang | Role |
|---|---|---|
yucca-api |
NestJS | User-facing API. Owns auth (OIDC code + device flow, ES256 JWTs), repositories, DB schema + migrations. |
yucca-admin-api |
NestJS | Admin API (user/session/repository management). Same DB + JWT validation. |
michael |
Go | Production restic REST backend — S3 proxy with JWT verification, WORM enforcement, backend pooling. |
columbo |
Go | Ticket investigation agent: LLM loop (OpenRouter) over per-user-scoped o11y queries, answers only into staff threads. See docs/columbo.md. |
monk |
Go | Ceph scrub-backlog exporter: polls pg ls, serves measured per-pool scrub-age metrics on :9284. Deployed onto mon hosts by ansible, not K8s. |
yucca-metrics-worker |
NestJS | 5-min cron: RadosGW usage → meter tables → per-connection rollup (connectionMetrics, billing floor), OTel gauges. |
redis (valkey) |
Shared platform cache (ephemeral; keys yucca:<service>:<purpose>:*). Primary-region only. |
|
mock-oidc-provider |
Node | Dev/test OIDC IdP (code + device flow). |
mock-postmark-provider |
Node | Dev/test Postmark API mock; delivers into the Mailpit inbox. |
common (@common/server) |
TS lib | OTel init, pino repository, feature-flag registry + connection-type registry, Postmark EmailRepository (@common/server/email). |
emails (@common/emails) |
Svelte lib | Transactional email templates (better-svelte-email, web theme), prebuilt to JS for the NestJS apps. See docs/email.md. |
Frontend (packages/web): SvelteKit 5 + Tailwind 4, @immich/ui, lingui i18n
(mise web:lingui:*; compiled locales are generated, not edited), generated API client.
Docs (packages/docs, https://docs.futo.cloud): the end-user documentation site (beta setup
guides for Immich and the standalone container). SvelteKit + adapter-static; every page is a
src/routes/<section>/<slug>/+page.md compiled by @immich/svelte-markdown-preprocess (front matter
title/description/order; sections in src/lib/index.ts). Deployed to Cloudflare Pages
(tf/pages/docs + .github/workflows/docs.yml; every PR gets a preview). Internal/developer docs stay
in docs/ and the per-directory READMEs — do not move them onto the site.
packages/yucca-sdk/ (orchestration-api + orchestration-ui) is separately versioned and
added explicitly in pnpm-workspace.yaml.
API client generation: yucca-api DTOs → mise yucca-api:sync-openapi →
mise yucca-api-client:build (oazapfts) → packages/yucca-api-client/src/fetch-client.ts.
Generated — regenerate on contract changes, never hand-edit.
Database: PostgreSQL via Kysely. Schema in packages/yucca-api/src/schema/
(tables/, migrations/); yucca-api is the schema owner, other services read the same DB.
Go services: michael and yuctl are Go 1.25, internal/<pkg>, aws-sdk-go-v2, zerolog.
yuctl (cobra) is the ops CLI: reads the Terraform discovery contract from S3 state to
resolve topology and drive day-2 ops. See packages/yuctl/README.md.
Connections, feature flags, token revocation
Full detail: docs/connections.md. The essentials:
- Connections: a user has N connection instances of type
immich/restic; every repository has a NOT NULLconnectionId. Device flow binds via?connection_type=&connection_name=; instance attribution is client-driven (POST /connections/:id/adopt), never guessed server-side. The/connectionssurface is open to every authenticated user. - Feature flags: registry in code (
@common/serverFeatureFlags) + strict-boolean per-user overrides; resolutionoverride ?? registry default. Flags gate non-default connection types (connection-restic); admin-provisioned connections bypass. Boundary rule: env/cluster-settings = deployment config (ops-owned); feature flags = per-user product gating (admin-owned, runtime). - Billing:
ConnectionTypeInfosdeclares metering tiers andminObjectSizeBytesfloor; metrics-worker computesbillableBytes = max(size, objects * floor)per connection. - Revocation (revocable types only, default
restic): postgres is truth; michael checks L1 per-process cache → valkey verdict cache (yucca:michael:verdict:<jti>) → yucca-api introspection (GET /internal/restic-tokens/:jti). Revoke flips the DB row + best-effort DELs the valkey key; introspection outage honors the grace window then fails closed. Long-lived tokens are revocable-only. yuctl:tokens list/revoke,repos url --ttl.
Infrastructure architecture
Fleet model: partition → region → { one K8s cluster, one-or-more Ceph clusters }.
Partitions prod/staging/dev; regions e.g. htz-fsn1, austin, local, plus global.
Slug = <partition>-<region>.
tf/— OpenTofu + Terragrunt (deployment/<partition>/<region>/<stack>/, shared logic inshared/modules/). Every stack emits a non-sensitivediscoveryoutput consumed by yuctl/k8s/ansible; secrets in it areop://refs.ansible/—ceph/(cephadm bare-metal),talos/(Talos VMs on the Ceph hypervisors — VM provisioning only; talosctl is owned by the TF siderolabs provider),mgmt/(management hosts + NetBird). Roles gated by*_enabledflags.kubernetes/— Flux GitOps.clusters/<partition>/<region>/entry points;apps/{base,<overlays>,dev/local}/HelmReleases;components/Kustomize Components per role. Config layers merged via postBuild:cluster-settings.generated.yaml(TF),cluster-settings.yaml(human),image-versions(CI).charts/— in-repo Helm charts;lib/yucca-commonlibrary chart +apps/<svc>charts. Service names pinned viafullnameOverrideso DNS is identical under Tilt and Flux.
Deploy flow: merge to main → CI builds 0.0.<run_number> images → Flux auto-promotes to
staging → prod is promoted by merging the release-please PR (stamps both prod pins under
kubernetes/clusters/prod/htz-fsn1/).
Conventions
- Conventional commits enforced on PRs (
feat(scope):,fix(scope):,chore:); scopes likeceph,netbird,michael,yucca-api,ansible,bgp. Releases via release-please; the monorepo is single-versioned. - PR descriptions and commit messages: one line at most. Always. The diff carries the detail; neither the PR body nor the commit message restates it.
- ESLint is strict on promises (
no-floating-promises,no-misused-promises,require-await,await-thenableare errors). Prettier: single quotes, trailing commas, width 120. - Generated files are eslint-ignored:
**/fetch-client.ts,packages/web/src/locales,dist,build,.svelte-kit. - Match the package you are in. Read the surrounding code first; write code that looks like what is already there, not your own conventions. (And remember the life-or-death rule at the top: zero comments.)
Naming
- Clusters are themed by workload: Kubernetes → Star Wars (
luke= staging,father= soon-to-be prod); Ceph → Dune (sietch,spice, …). - Node hostnames:
<product>-<provider>-<region>-<clustername>-<role>-<nodename>, e.g.yucca-int-aus-luke-k8s-<word>. Provider/region are the 3-letter codes from the region'sregion.hcl(austin =int/aus, htz-fsn1 =htz/fsn); role isk8sorceph;<nodename>auto-picks fromtf/shared/modules/node-names/wordlist.txt(deterministic per cluster; pass an explicitnameto override).