Valtr docs
Overview.
Self-hosted secrets for your apps. Part of the rec.farm family.
Keep each project's secrets per environment, encrypted at rest. Hand a CI job or a server an API
key that can read (or write) one project and nothing else, pull everything as a .env file or
JSON, and see who touched what in the audit log. One small Rust binary and one SQLite file.
- Encrypted at rest. Every value is sealed with AES-256-GCM under your
VALTR_MASTER_KEY. Listings never include values. - Environments. New projects get
development,stagingandproduction. Add as many as you like. - Secret history. Every write is a new version. Old values stay encrypted, can be read (audited) and restored.
- Shared projects. Invite people with a one-time link as
readorwritemembers. The owner decides who's in. - Scoped API keys.
vltr_…keys belong to one project, read-only or read-write, and can expire and be rotated. They never do more than the person holding them. Only their hash is stored. - A CLI for CI and servers.
valtr run production -- ./serverstarts a process with its secrets;valtr pull production > .envwrites them out. - Exports.
GET …/secrets/.envand…/secrets/json. - Audit log. Creates, updates, reads, exports and deletions, with the client IP.
- Sign in with Handstamp. People sign in with Handstamp (or any OpenID Connect provider), which also decides who may. Signing out there signs you out here (back-channel logout). Machines use API keys. No passwords.
A web app covers all of it: projects and the people in them, environments, a secrets editor with history, API keys and the audit log, in Dark and Paper. Everything it does goes through the same API, so scripts and CI can do it too.
Run it
Needs Rust 1.92, Node 24 and pnpm. The dev server runs through
portless, like the rest of the family. Install it
globally once with npm install -g portless. Don't add it to the project.
pnpm install
cp .env.example .env # then set VALTR_MASTER_KEY (openssl rand -hex 32)
pnpm dev # http://valtr.localhost:1355Open it and use Dev login: in development you sign in as a local developer, without
Handstamp. To try the real sign-in, add the OIDC_* settings from .env.example for a Handstamp
you run (../handstamp) with Valtr registered as an app.
pnpm dev runs Vite with hot reload in front of the Rust server, which it builds and restarts with
the script. pnpm dev:direct skips portless and serves on http://localhost:3000 (PORT to
change). Both read .env and keep the database in ./data/valtr.db. In Herdr, start them through herdr-run.
Or with Docker, once .env has the Handstamp settings: docker compose up -d --build. Outside
development Valtr doesn't start without them.
From a terminal, the same as the web app (the dev login here; in production, a session from the web app or an API key):
VALTR=http://valtr.localhost:1355
curl -c jar -X POST $VALTR/api/auth/dev-login
curl -b jar -H 'content-type: application/json' $VALTR/api/projects -d '{"name":"My App"}'
curl -b jar -H 'content-type: application/json' \
$VALTR/api/projects/my-app/environments/production/secrets \
-d '{"key":"DATABASE_URL","value":"postgres://…"}'
curl -b jar -H 'content-type: application/json' $VALTR/api/api-keys \
-d '{"name":"deploy","projectSlug":"my-app"}' # → {"key":"vltr_…", …} shown onceIn CI and on servers
The same valtr binary is the client. Give it the server and an API key:
export VALTR_URL=https://valtr.example.com VALTR_API_KEY=vltr_…
valtr run production -- ./server # the secrets are in its environment
valtr pull production > .env # or a .env file (--json for JSON)An API key sees only its project, so there's nothing else to say; --project <slug> picks one
when it's ambiguous. A secret wins over an environment variable of the same name, and
VALTR_API_KEY isn't passed on to the command. Secrets that can make a program load other code
(PATH, LD_*, DYLD_*, NODE_OPTIONS, PYTHONPATH, BASH_ENV and the like) are skipped with
a warning; set those yourself. That only blocks the obvious ways: whoever can write a project's
secrets configures what run starts, so give write access only to people you'd trust with the
machines that run it.
run replaces itself with the command, so signals
and the exit status are the command's own. The key only travels over https:// (or to
localhost).
Without the binary, it's one request:
curl -H "Authorization: Bearer $VALTR_API_KEY" \
$VALTR_URL/api/projects/my-app/environments/production/secrets/.env > .envDevelop
pnpm test # unit tests + HTTP API + OIDC sign-in against a mock provider
pnpm check # prettier, rustfmt, clippy -D warnings, tests, release build
pnpm format
pnpm valtr <command> # the binary against .env: serve, migrate, accounts, link, pull, runThe server is backend/ (Rust, axum). The web app is apps/web (Vite, React, Tailwind, on the
R.E.C. design system); the server serves its build. Migrations run on start; pnpm db:migrate runs
them alone.
| Path | What |
|---|---|
backend/src/routes.rs |
Projects, people, environments, secrets and their history, exports, API keys, audit log |
backend/src/auth.rs |
Sessions, accounts, the dev login, API keys and their scoping |
backend/src/oidc.rs |
Sign in with Handstamp (OpenID Connect relying party) |
backend/src/crypto.rs |
AES-256-GCM, API keys, session tokens |
backend/src/client.rs |
valtr pull and valtr run |
backend/src/db.rs |
SQLite schema and migrations |
backend/src/web.rs |
Serving the web app, its CSP and security headers |
apps/web/src/pages |
The web app's pages |
Coming from the TypeScript version
Valtr was a Bun + Hono + Drizzle server. The Rust version speaks the same HTTP API and opens the
same database: point VALTR_DB_PATH at the old valtr.db and keep the same VALTR_MASTER_KEY.
API keys and encrypted values carry over. The differences:
Handstamp instead of passwords. People sign in with Handstamp; the register and login endpoints are gone. Old accounts keep their projects but have no Handstamp identity yet, and Valtr never matches one by email. Have each person sign in once to get their Handstamp
sub(or read it in Handstamp's admin), then link it:valtr link me@example.com <sub>.valtr accountsshows who is linked.API keys are enforced. A key now only reaches its own project,
readkeys can't change anything, and keys can't create or delete projects or manage keys. Before, any key acted as its owner..envandjsonexports work. The/:keyroute used to catch them..envvalues are quoted whenever they hold anything beyond plain characters, so a value can't inject extra lines.Projects and environments with history can be deleted. The audit log keeps plain ids now instead of foreign keys that blocked the delete.
Sessions survive restarts. They live in the database, not in process memory. Everyone signs in once after the upgrade.
More is audited: reads, exports, environment and API-key changes, and the client IP.
Stricter names. Environment names and secret keys use letters, digits,
_,-and.. Existing data isn't touched.No way back. The upgrade drops the old password hashes (
users.password_hash), so the TypeScript server can't open the database afterwards. Back it up first.