Vetrix Docs

Plans Volume — Operator Setup

The per-repo Plans subsystem stores render-only markdown documents on disk under a single configured root. This doc describes the operator-side setup that the API expects.

Layout

{VETRIX_PLANS_DIR}/
├── alice/
│   ├── v1.0/
│   │   └── release-plan.md
│   └── Architecture.md
├── my-org/
│   └── docs/
│       └── handbook.md
└── ...

Each top-level directory is a single repo owner — either a user handle or a canonical org slug. Inside an owner directory, folders and files are arbitrary as long as their components match the validator allow-list ([a-zA-Z0-9._-], length ≤ 200, no . / .. / leading dot, no separators). Files written via the API additionally MUST end in .md — Plans are render-only markdown.

Mount + permissions

  • The volume root MUST be on a filesystem that supports symlinks (the API uses O_NOFOLLOW to refuse opening through them).
  • Owner of the root: the same UID/GID Vetrix runs under (typically vetrix:vetrix).
  • Mode: 0750 on the root (drwxr-x---). Group-readable so backup tooling under the same group can list files; other-not-readable so tenants on a shared host don't accidentally inventory each other.
  • Owner subdirectories created at runtime use 0750 (DirMode in internal/plans/path.go).
  • Files created at runtime use 0640 (FileMode).
sudo mkdir -p /var/lib/vetrix/plans
sudo chown vetrix:vetrix /var/lib/vetrix/plans
sudo chmod 0750 /var/lib/vetrix/plans

Then point the application at it:

VETRIX_PLANS_DIR=/var/lib/vetrix/plans

Default deployments

The default compose + k8s artefacts shipped with Vetrix wire this for you so a fresh docker compose up (or kubectl apply -k deployments/kubernetes) produces a working Plans surface without any extra operator steps:

  • docker-compose.yml (the dev/CI base) sets VETRIX_PLANS_DIR=/data/plans on the api service. The path is on the same vetrix-data named volume that backs /data/repositories + /data/downloads, so the bytes share lifecycle with the rest of the persistent state. deployments/docker/Dockerfile pre-creates /data/plans at image-build time with mode 0750.
  • deployments/kubernetes/configmap.yaml sets VETRIX_PLANS_DIR=/data/plans and deployments/kubernetes/deployment.yaml extends the existing init-data-dirs initContainer to also mkdir -p /data/plans && chmod 0750 /data/plans against the bound PVC before the server boots. This is required because a fresh PVC starts empty and the application never elevates the root's mode after the fact.

The VETRIX_PLANS_DIR env var only needs customisation for bespoke storage layouts — for example, a separate fast-tier volume mounted at a non-default path, or a multi-tenant deployment where the plans tree is on a network filesystem mounted at /srv/customers/<tenant>/plans.

Disabling Plans explicitly

The binary fail-stops at boot: it exits with a non-zero status when VETRIX_PLANS_DIR is unset AND VETRIX_PLANS_DISABLED is not set to 1. Refusing to start surfaces the misconfiguration at boot rather than masking it as a data-loss-shaped symptom in production.

The exact contract:

VETRIX_PLANS_DIR VETRIX_PLANS_DISABLED Result
set (any) Plans enabled; routes mounted.
unset 1 Plans explicitly disabled; routes return 404; single WARN at boot.
unset unset (or any non-1) Boot fails with a level=ERROR line; process exits 1.

To intentionally run a deployment without the Plans feature (for example a slim mirror-only Vetrix host), set:

VETRIX_PLANS_DISABLED=1

The boot WARN line names the disabled feature so operators reading error.log can distinguish "Plans off by design" from "Plans off because the wiring regressed":

vetrix: Plans feature explicitly disabled by VETRIX_PLANS_DISABLED=1
(GET /api/v1/repos/{owner}/{repo}/plans and POST /plans/{folders,upload}
plus PATCH/DELETE-by-id mutation endpoints will return 404).

The error emitted on the misconfigured-and-not-opted-out path is:

vetrix: VETRIX_PLANS_DIR is unset and VETRIX_PLANS_DISABLED is not set;
refusing to start. Set VETRIX_PLANS_DIR to a writable path to enable the
Plans feature, or set VETRIX_PLANS_DISABLED=1 to explicitly opt out.

The routes affected when Plans is disabled (whether explicitly or implicitly via the default deployments above) are the public read group plus the mutation set:

  • GET /api/v1/repos/{owner}/{repo}/plans
  • GET /api/v1/repos/{owner}/{repo}/plans/id/{id}
  • GET /api/v1/repos/{owner}/{repo}/plans/id/{id}/children
  • GET /api/v1/repos/{owner}/{repo}/plans/id/{id}/raw
  • POST /api/v1/repos/{owner}/{repo}/plans/folders
  • POST /api/v1/repos/{owner}/{repo}/plans/upload
  • PATCH /api/v1/repos/{owner}/{repo}/plans/id/{id}
  • DELETE /api/v1/repos/{owner}/{repo}/plans/id/{id}

Allow-list reference

Owner slugs (SanitizeOwnerSlug):

  • ^[a-z0-9._-]{1,64}$
  • Reject .., /, NUL, leading ., non-ASCII.

Path components — folders (SanitizeComponent):

  • ^[a-zA-Z0-9._-]{1,200}$
  • Reject .., /, NUL, ., leading ., non-ASCII, whitespace.

Filenames written by the API (SanitizeFilename):

  • All SanitizeComponent rules, plus
  • MUST end in .md (case-insensitive on the extension; non-empty stem).

ResolvePath runs the owner + component validators and additionally asserts the canonicalised absolute path is rooted under the owner volume — defence-in-depth.

Backup considerations

  • The plans tree is included in the standard Vetrix backup target (see the backup guide in the admin-docs repo).
  • Owner directory modes are preserved; restore as the same UID/GID.

Migration notes

This is a greenfield directory tree; existing deployments do not have a legacy layout to migrate.