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_NOFOLLOWto refuse opening through them). - Owner of the root: the same UID/GID Vetrix runs under (typically
vetrix:vetrix). - Mode:
0750on 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 ininternal/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) setsVETRIX_PLANS_DIR=/data/planson theapiservice. The path is on the samevetrix-datanamed volume that backs/data/repositories+/data/downloads, so the bytes share lifecycle with the rest of the persistent state.deployments/docker/Dockerfilepre-creates/data/plansat image-build time with mode0750.deployments/kubernetes/configmap.yamlsetsVETRIX_PLANS_DIR=/data/plansanddeployments/kubernetes/deployment.yamlextends the existinginit-data-dirsinitContainer to alsomkdir -p /data/plans && chmod 0750 /data/plansagainst 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}/plansGET /api/v1/repos/{owner}/{repo}/plans/id/{id}GET /api/v1/repos/{owner}/{repo}/plans/id/{id}/childrenGET /api/v1/repos/{owner}/{repo}/plans/id/{id}/rawPOST /api/v1/repos/{owner}/{repo}/plans/foldersPOST /api/v1/repos/{owner}/{repo}/plans/uploadPATCH /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
SanitizeComponentrules, 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.