vsr CLI
A command-line interface for managing very_simple_rest API deployments.
Overview
vsr is the main entry point for .eon-driven VSR services. It can scaffold a project, serve a
service directly at runtime, emit a Rust server project, build a standalone binary, generate
OpenAPI and .eon reference docs, and manage setup/auth flows.
The published crate is vsra; the installed binary is vsr.
Quick Start
Installation
From Source
# Clone the repository
# Build the CLI tool
# Run the CLI
The workspace default member is the CLI package, so cargo build and cargo run from the
repository root target vsr. Use cargo build --workspace when you want the full workspace, or
cargo build -p very_simple_rest for the library package.
Cargo Install
For an unpublished local checkout:
Commands
Init
Generate a local starter project:
By default, vsr init prompts in a terminal and falls back to the recommended comment-rich
starter in non-interactive use. The generated project is local-only and does not copy app code
from examples/ or fetch from GitHub.
The generated project includes:
api.eon.env.exampleREADME.md.gitignoremigrations/var/data/
The default commented starter uses local Turso/SQLite defaults and includes commented examples for
current .eon features such as typed Object / List / JSON fields, API projections and
contexts, enums, indexes, many-to-many join resources, transforms, and declarative actions.
Setup
Initialize a new API deployment with interactive prompts:
This command will:
- Generate or load
.envbefore any database work when a.eonservice is in use - Generate local self-signed TLS certs when the service enables
tlsand dev cert files are missing - Check your database connection and apply setup migrations
- Help you create or verify an admin user
When setup generates or refreshes .env, it writes the file next to the selected .eon
service, loads it into the current setup process immediately, and prints a summary with the exact
paths it touched. If it refreshes an existing file, the previous contents are backed up to
.env.backup.
vsr setup --production switches to production-safe behavior:
- it does not write live secrets into
.env - it does not generate self-signed dev TLS certs
- it refuses to continue when required production secrets are unresolved
For non-interactive setup (e.g., in CI/CD pipelines):
If you run vsr from a directory containing exactly one .eon service, the CLI auto-discovers
it for setup, create-admin, check-db, and gen-env. You can still pass --config
explicitly when you want a non-default file:
When --config points to a .eon service and --database-url / DATABASE_URL are both absent,
the CLI uses the service’s compiled default database URL. For SQLite services, that now defaults
to encrypted database.engine = TursoLocal, which resolves to the matching SQLite-compatible file
URL and the default TURSO_ENCRYPTION_KEY env var.
In non-interactive mode, setup will bootstrap .env automatically when it is missing or when a
required local Turso encryption key is absent. If the service also enables tls: {} with the
default dev cert paths, setup --non-interactive generates those PEM files before the database
connection step so HTTPS can work immediately.
Env Generation
Generate a .env file directly:
When --config points to a .eon service, the generated file mirrors the compiled default
database URL plus the required/default Turso and security env vars such as
TURSO_ENCRYPTION_KEY, CORS_ORIGINS, TRUSTED_PROXIES, and the configured logging filter env
var when those are referenced by the service. For database.engine = TursoLocal, vsr gen-env
now writes a real 64-hex-character local encryption key instead of a placeholder. By default, the
generated .env is written next to the .eon file when --config is used.
vsr gen-env --production keeps the same shape but comments out live secret values instead of
writing them. Use that mode for deployment templates and prefer mounted *_FILE bindings or a
secret manager for the real values.
Infisical Scaffolding
Generate Infisical Agent/runtime scaffolding directly from a .eon service:
The scaffold includes:
infisical-agent.yamlruntime.envexpected-secrets.envtemplates/*.tplauth/README.md
This is designed around the existing VSR *_FILE support. Infisical Agent renders secret files,
and the generated runtime.env points VSR at those files. See
../../docs/infisical.md.
vsr doctor secrets validates:
- currently resolved
*_FILE/ inline bindings for the service - service-required secret inputs such as the configured JWT signing key, mail credentials, and Turso keys
- optional Infisical scaffold completeness when
--infisical-diris provided
Strict Service Checks
Run compiler-facing schema diagnostics:
The first strict slice reports high-confidence warnings for:
- TLS certificate/key paths that do not exist yet
- authorization contracts that do not affect generated runtime behavior
- declared authorization scopes that are not referenced anywhere else
- row-policy, nested-route,
exists, or hybrid scope lookup fields that rely on inferred indexes without an explicit.eonindex declaration - empty declared build-artifact env overrides, binary/bundle path collisions, and cache/output
path overlaps that can make
vsr buildorvsr cleanunsafe
--strict turns those warnings into a failing exit status so the command can be used in CI.
Serve, Emit, and Build
Serve a bare .eon service directly through the native runtime:
This is the fastest local development loop. The runtime serves the compiled API surface directly
from the .eon file, including /openapi.json, /docs, static mounts, built-in auth, runtime
authorization management routes, compiled database engine settings, and TLS when configured.
Built-in auth is enabled by default. If the .eon service already defines a user table, re-run
with --without-auth because the built-in auth migration owns that table name:
If you want a runnable Rust project you can inspect or modify, emit one instead:
If you want to inspect the full macro-expanded Rust module directly without creating a Cargo project first, expand it to a single source file:
When --output is omitted, vsr server expand writes <input-stem>.expanded.rs next to the
.eon file. --output-dir generated-api is also accepted as a compatibility alias and writes
generated-api/<input-stem>.expanded.rs.
This emits:
Cargo.tomlsrc/main.rs- the copied
.eonfile .env.exampleopenapi.jsonmigrations/0000_auth.sqlwith built-in auth enabled by defaultmigrations/0001_service.sql
If you want a standalone binary directly from the same contract, build it:
Useful options:
--without-authexcludes the built-in auth/account routes and omitsmigrations/0000_auth.sql--package-nameoverrides the generated Cargo package name--build-dirkeeps the temporary Cargo project in a known location--keep-build-dirpreserves the generated build project after compilation--output distwrites the binary into an existing directory; otherwise it defaults to the.eonservice directory and names the binary after the.eonfile stem
vsr build also exports the generated runtime artifacts next to the binary in
<binary>.bundle/, including .env.example, openapi.json, the copied .eon file, README.md,
migrations/, and relative TLS certificate files when they exist at build time. When
runtime.compression.static_precompressed = true, vsr build also generates .br and .gz
companion files for copied static assets inside that bundle.
.eon services can declare build artifact locations under build.artifacts. Each artifact path
resolves with this precedence: CLI override, then the declared env var override, then the literal
.eon path, then the service-relative default. No build-path env vars are read unless the .eon
file explicitly names them.
vsr clean --input api.eon removes the same resolved build cache that vsr build api.eon uses.
Without --input or --build-dir, vsr clean keeps the legacy ./.vsr-build current-directory
fallback.
The generated server fails fast if built-in auth is enabled and the configured JWT signing
material is missing. vsr gen-env and emitted .env.example files still help by generating or
surfacing the required env vars, but runtime auth is no longer allowed to fall back to a random
secret.
Generated server projects serve the OpenAPI document at /openapi.json and Swagger UI at /docs.
When a .eon service defines static mounts, vsr server emit also copies those directories into
the generated project and wires the generated server to serve them. When a .eon service defines
security, the emitted server also applies the compiled JSON body limit, CORS policy,
trusted-proxy handling, auth rate limits, security headers, and built-in auth token settings
automatically. When a .eon service defines database.engine, the emitted server also carries
that runtime engine config into the project, including encrypted local Turso bootstrap by default
for bare SQLite .eon services. When a .eon service defines logging, the emitted server also
uses the compiled log env var, default filter, and timestamp precision instead of hard-coded
logger defaults. When a .eon service defines tls, the emitted server binds HTTPS with Rustls
and HTTP/2, defaults BIND_ADDR to 127.0.0.1:8443, and can use vsr tls self-signed to
generate local certificate PEM files.
TLS Certificate Generation
Generate a self-signed certificate and private key for local development:
Behavior:
- with
--config api.eon, the command uses the configured.eontls.cert_pathandtls.key_path - with no config and no explicit output paths, it defaults to
certs/dev-cert.pemandcerts/dev-key.pemin the current directory - default SANs are
localhost,127.0.0.1, and::1 - private keys are written with restrictive permissions on Unix
OpenAPI Generation
Render an OpenAPI document from either a .eon service or derive-based Rust resources:
Useful options:
--titleoverrides the document title--versionoverrides the OpenAPI version string ininfo.version--server-urlchanges the generated server URL, which defaults to/api- built-in
/auth/register,/auth/login, and/auth/meroutes are included by default, with/auth/megrouped underAccountin Swagger --without-authremoves those built-in auth/account routes from the document--exclude-tableremoves specific tables from the document
.eon Reference Docs
Generate a Markdown reference for the currently supported .eon feature set:
The checked-in reference document lives at docs/eon-reference.md.
Authorization Explain
Inspect how a .eon service compiles into the current internal authorization model:
When --input is omitted, vsr falls back to the same autodiscovered or explicit --config
path used by the other .eon-aware commands.
You can also simulate a single authorization decision:
Repeated --claim, --row, and --proposed arguments use key=value syntax. Values are
inferred as null, bool, i64, or String. Repeated --related-row arguments use
Resource:key=value,other=value syntax so the simulator can evaluate relation-aware exists
predicates against explicit related rows. --scope uses ScopeName=value, and repeated
--scoped-assignment arguments use permission:Name@Scope=value or
template:Name@Scope=value. --load-runtime-assignments loads stored assignments for
--user-id from the configured database, using the table created by vsr migrate authz.
These runtime scoped assignments are validated and resolved by the simulator. --hybrid-source
adds a second, generated-handler view for item, collection_filter, nested_parent, or
create_payload scope derivation when the resource declares
authorization.hybrid_enforcement. Stored assignments include created_at,
created_by_user_id, and optional expires_at; expired assignments are ignored by runtime
simulation and runtime access checks.
You can also manage those persisted runtime assignments directly from the CLI:
authz runtime create validates the requested permission/template and scope against the static
.eon authorization contract before inserting the row. authz runtime evaluate loads stored
assignments for the user and evaluates only the runtime grant layer; it does not apply the
static CRUD role and row-policy checks. authz runtime revoke deactivates an assignment by
setting its expiration to the current time without deleting its record, and authz runtime renew
sets a new future expiration. authz runtime history reads the append-only assignment event log,
which now records created, revoked, renewed, and deleted events with actor and optional
reason data.
Static .eon row policies also support all_of, any_of, not, and a first bounded
relation-aware exists form. vsr authz simulate can fully evaluate exists predicates when
you supply matching related rows with repeated --related-row arguments; otherwise the trace
stays incomplete and reports the missing related resource data.
The .eon format also supports an optional static authorization block for declaring scopes,
permissions, templates, and an opt-in runtime management mount. The scope/permission/template
portion is still contract-only by itself for generated CRUD enforcement: validated and surfaced by
the diagnostic commands. Request-time behavior changes only when you opt into runtime management
or hybrid enforcement.
Generated item-scoped CRUD handlers can now also opt into the first hybrid-enforcement slice:
authorization: {
scopes: {
Family: {}
}
permissions: {
FamilyManage: {
actions: ["Create", "Read", "Update", "Delete"]
resources: ["ScopedDoc"]
scopes: ["Family"]
}
}
hybrid_enforcement: {
resources: {
ScopedDoc: {
scope: "Family"
scope_field: "family_id"
scope_sources: {
item: true
collection_filter: true
nested_parent: true
create_payload: true
}
actions: ["Create", "Read", "Update", "Delete"]
}
}
}
}
This is additive only. Generated GET /resource/{id}, PUT /resource/{id}, and
DELETE /resource/{id} still require the static role check to pass first. When the static row
policy denies the row, the handler can derive a runtime scope from the stored row and consult the
persisted runtime assignment layer. Top-level GET /resource can also use runtime Read grants,
but only when the request includes an exact filter_<scope_field>=... value so the handler can
derive one concrete scope from the query and scope_sources.collection_filter = true. Nested
collection routes can also use runtime Read grants when scope_sources.nested_parent = true
and the nested parent filter is the configured scope_field. Generated POST /resource can also
opt into a narrow hybrid create fallback, and when the created row is only runtime-readable the
generated 201 response can still return the created item through the same hybrid read fallback.
The create path remains narrow:
the handler only opens the configured scope field when it is already assigned from a claim in
policies.create; in that case the create DTO exposes that one field as an optional fallback and
the handler uses it only when the claim is missing and a matching runtime Create grant exists
for the supplied scope.
Static Files In .eon
Bare .eon services can define service-level static mounts:
static: {
mounts: [
{
mount: "/assets"
dir: "public/assets"
mode: Directory
cache: Immutable
}
{
mount: "/"
dir: "public"
mode: Spa
index_file: "index.html"
fallback_file: "index.html"
cache: NoStore
}
]
}
Supported options:
mount: URL prefix such as/assetsor/dir: directory relative to the.eonfilemode:DirectoryorSpaindex_file: optional directory index filefallback_file: SPA fallback filecache:NoStore,Revalidate, orImmutable
The loader rejects mounts that escape the .eon root or conflict with reserved routes such as
/api, /auth, /docs, and /openapi.json.
Database Engine In .eon
Bare .eon services can also define a service-level database engine. For SQLite services, the
default when this block is omitted is:
database: {
engine: {
kind: TursoLocal
path: "var/data/<module>.db"
encryption_key: { env_or_file: "TURSO_ENCRYPTION_KEY" }
}
}
You can still override it explicitly:
database: {
engine: {
kind: TursoLocal
path: "var/data/app.db"
encryption_key: { env_or_file: "TURSO_ENCRYPTION_KEY" }
}
}
Current support:
Sqlx: the legacy runtime path; use this explicitly if you want plain SQLx SQLite for a SQLite.eonserviceTursoLocal: bootstraps a local Turso database file and uses the project runtime database adapter with SQLite-compatible SQLTursoLocal.encryption_key: typed secret ref for the local Turso hex key; the preferred form is{ env_or_file: "TURSO_ENCRYPTION_KEY" }TursoLocal.encryption_key_env: legacy shorthand still accepted for backward compatibility
Current limitation:
- This is still a project-local runtime adapter, not a true upstream SQLx
Anydriver.
Backup And Replication Planning
.eon services can now declare an optional database.resilience contract for backup and
replication intent. This is a planning surface first, not a job scheduler.
database: {
engine: {
kind: Sqlx
}
resilience: {
profile: Pitr
backup: {
mode: Pitr
target: S3
verify_restore: true
max_age: "24h"
encryption_key: { env_or_file: "BACKUP_ENCRYPTION_KEY" }
}
replication: {
mode: ReadReplica
read_routing: Explicit
read_url: { env_or_file: "DATABASE_READ_URL" }
max_lag: "30s"
}
}
}
Use:
Current scope:
- backend-aware backup and replication guidance
- doctor commands for obvious env and topology validation
- live Postgres/MySQL role-state checks in
vsr replication doctor - snapshot artifact creation and restore verification for SQLite/TursoLocal services
- Postgres/MySQL logical dump artifact creation with native tools or Docker client fallbacks
- Postgres/MySQL logical dump restore verification in disposable local Docker databases
- S3-compatible artifact push/pull around the local snapshot format
- checked
.eonresilience vocabulary - no scheduling, failover orchestration, or automatic read routing yet
For MinIO or another S3-compatible endpoint:
Security In .eon
Bare .eon services can also define service-level security defaults:
security: {
requests: { json_max_bytes: 1048576 }
cors: {
origins: ["http://localhost:3000"]
origins_env: "CORS_ORIGINS"
allow_credentials: true
allow_methods: ["GET", "POST", "OPTIONS"]
allow_headers: ["authorization", "content-type"]
}
trusted_proxies: {
proxies: ["127.0.0.1", "::1"]
proxies_env: "TRUSTED_PROXIES"
}
rate_limits: {
login: { requests: 10, window_seconds: 60 }
register: { requests: 5, window_seconds: 300 }
}
headers: {
frame_options: Deny
content_type_options: true
referrer_policy: StrictOriginWhenCrossOrigin
}
auth: {
issuer: "very_simple_rest"
audience: "public-api"
access_token_ttl_seconds: 3600
}
}
This config currently controls:
- JSON body size limits for generated resource and built-in auth routes
- CORS origins, headers, methods, credentials, and preflight caching on the emitted server
- trusted-proxy IP handling for forwarded client addresses
- in-memory built-in auth login and registration rate limits
- security response headers on the emitted server
- built-in auth JWT
iss,aud, and token TTL defaults
Secrets are still configured via environment names today, but production deployments should prefer
mounted *_FILE bindings or a secret manager over inline .env values. The Phase 1 roadmap is in
../../docs/production-secrets-roadmap.md. The current
auth rate limiter is process-local rather than distributed.
Runtime In .eon
Bare .eon services can also define runtime defaults:
runtime: {
compression: {
enabled: true
static_precompressed: true
}
}
Generated modules expose this through module::runtime(). The parsed runtime options are:
compression.enabled: emitted servers now apply dynamic HTTP response compression from this flagcompression.static_precompressed: generated static mounts now serve.brand.gzcompanion files when present and addVary: Accept-Encoding
vsr build now generates those companion files into <binary>.bundle/ when this flag is enabled.
vsr server emit still copies the source static directories as-is.
Generated .eon modules also expose the compiled authorization model through
module::authorization(). That includes the optional static authorization contract and the
resource/action policy view used by vsr authz explain, which makes it suitable for custom
policy-management and diagnostics endpoints in emitted or manual servers. They also expose
module::authorization_runtime(db), and module::configure(...) now registers that runtime
service as Actix app data for the configured scope. For a basic opt-in runtime assignment API,
generated modules also expose module::configure_authorization_management(cfg, db), which mounts
POST /authz/runtime/evaluate plus admin-oriented
GET /authz/runtime/assignment-events, GET/POST/DELETE /authz/runtime/assignments..., and
POST /authz/runtime/assignments/{id}/revoke|renew endpoints backed by the shared authorization
runtime. The evaluate endpoint resolves persisted scoped permissions for an explicit
resource/action/scope request, but it does not apply static CRUD policy checks.
Custom handlers can also enforce persisted runtime grants directly through
AuthorizationRuntime::enforce_runtime_access(...) after injecting the shared runtime as Actix
app data.
Create Admin
Create a new admin user:
# Interactive mode with prompts
# Non-interactive mode with parameters
If the built-in auth user table has auth claim columns, the CLI detects them automatically.
That includes legacy implicit numeric claim columns such as tenant_id, org_id, or
claim_workspace_id, plus explicit security.auth.claims mappings from your .eon service.
Interactive admin creation prompts for those values, and non-interactive flows accept environment
variables named ADMIN_<COLUMN_NAME>, for example ADMIN_TENANT_ID=1 or ADMIN_IS_STAFF=true.
For .eon services with explicit security.auth.claims, vsr setup also generates those mapped
user columns automatically as part of the built-in auth migration flow. You do not need to ship
manual SQL just to add auth claim columns.
Use explicit auth claims for stable user/session attributes. For permissions, delegated access,
and scoped grants, prefer the runtime authorization tables and the .eon authorization
contract instead of storing permission state on the built-in auth user row.
Check Database
Verify database connection and schema:
This will:
- Test the database connection
- Check if required tables exist
- Count existing users and admins
- Provide recommendations based on findings
Generate .env Template
Create a template .env file with common configuration options:
Environment Variables
The CLI tool respects the following environment variables:
| Variable | Description | Default |
|---|---|---|
DATABASE_URL / DATABASE_URL_FILE |
Database connection string or mounted file containing it | Derived from --config or a single local .eon; otherwise sqlite:var/data/app.db?mode=rwc |
ADMIN_EMAIL |
Default admin email address | None |
ADMIN_PASSWORD |
Default admin password | None |
ADMIN_<COLUMN_NAME> |
Optional built-in auth claim column value, for example ADMIN_TENANT_ID or ADMIN_IS_STAFF |
None |
JWT_SECRET / JWT_SECRET_FILE |
Legacy shared-secret JWT signing key or mounted file containing it | Required only when built-in auth uses the legacy security.auth.jwt_secret path |
Examples
Starter Contract Example
The repository starter example now lives at examples/template and is contract-first rather than a
Rust app skeleton:
CMS Example
For a larger .eon-driven example with a Material studio client and local S3-compatible storage:
Complete Setup Example
# Set database URL
# Initialize the application
# Check database status
For production, prefer:
Or, for asymmetric JWT signing:
.eon-Driven Local Turso Example
# Use the compiled database settings from a bare .eon service explicitly
Creating Admin in CI/CD Pipeline
# Set required variables
# Create admin non-interactively
Security Best Practices
- Never store admin credentials in version control
- Use mounted secret files or a secure secret management system for JWT keys and other high-value secrets
- Change default admin passwords immediately in production
- Use strong, unique passwords
- Consider setting up a dedicated admin user for each team member
Troubleshooting
Common Issues
Database Connection Errors
- Verify the database URL format
- Ensure the database server is running
- Check file permissions for SQLite databases
Admin Creation Fails
- Ensure both email and password are provided
- Verify the database is accessible and writable
- Check if an admin with the same email already exists