REST API CLI Tool
A command-line interface for managing very_simple_rest API deployments.
Overview
This CLI tool simplifies the setup and management of very_simple_rest API applications, with a focus on secure user management and configuration.
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
Scaffold a starter project from the bundled template:
The starter template now uses local Turso by default and wires in the shared runtime security helpers for request limits, CORS, trusted proxies, auth rate limits, and response headers.
Setup
Initialize a new API deployment with interactive prompts:
This command will:
- Check your database connection
- Create necessary tables if they don't exist
- Help you create an admin user
- Generate a
.envtemplate file
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.
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.
Server Generation
Generate a runnable Rust server project from a bare .eon service:
This emits:
Cargo.tomlsrc/main.rs- the copied
.eonfile .env.exampleopenapi.jsonmigrations/0000_auth.sqlwith built-in auth enabled by defaultmigrations/0001_service.sql
You can also build a server binary directly:
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 current directory and names the binary after the.eonfile stem
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.
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.
The generated server fails fast if built-in auth is enabled and JWT_SECRET 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: "TURSO_ENCRYPTION_KEY"
}
}
You can still override it explicitly:
database: {
engine: {
kind: TursoLocal
path: "var/data/app.db"
encryption_key_env: "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_env: reads a hex key from the named environment variable and uses Turso local encryption with the current default cipher (aegis256) during bootstrap
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"
}
replication: {
mode: ReadReplica
read_routing: Explicit
read_url_env: "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 such as JWT_SECRET remain environment-driven. 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 connection string | 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 |
Secret key for JWT tokens | Required for built-in auth at runtime |
Examples
Complete Setup Example
# Set database URL
# Initialize the application
# Check database status
.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 environment variables or a secure secret management system
- 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