modo
modo (Latin: "way, method") — a Rust web framework for small monolithic apps.
One crate. Zero proc macros. Everything you need to ship a real app — sessions, auth, background jobs, email, storage — without stitching together 15 crates and writing the glue yourself.
Built on axum 0.8, so you keep full access to the axum/tower ecosystem. Handlers are plain async fn. Routes use axum's Router. Database queries use libsql directly. No magic, no code generation, no framework lock-in.
Why modo
You need 15+ crates for a real Rust web app. Sessions, auth, background jobs, config, email, flash messages, rate limiting, CORS, CSRF — each one is a separate crate with its own patterns, its own wiring, and its own test setup. modo gives you all of it in one import.
Proc macros slow you down. They increase compile times, hide control flow, and make errors cryptic. modo uses zero proc macros. Handlers are plain functions. Routes are axum routes. What you see is what runs.
Wiring everything together is the real work. Config loading, service injection, middleware ordering, graceful shutdown — the framework should handle this, not you. With modo, it's one Registry, one run! macro, and you're done.
Quick look
use ;
use ;
use Task;
async
async
What's included
Config that just works
YAML files with ${ENV_VAR} and ${ENV_VAR:default} substitution, loaded per APP_ENV. No builder, no manual env parsing, no .env ceremony.
# config/production.yaml
server:
port: ${PORT:8080}
database:
url: ${DATABASE_URL}
let config: Config = load?;
Database without an ORM
SQLite via libsql. A single Database handle wraps an Arc-ed connection — clone-friendly, no pool complexity.
let pool = connect.await?;
migrate.await?;
Sessions with zero glue code
Database-backed, signed cookies, sliding expiry, multi-device, fingerprinting. The middleware handles the full request/response lifecycle — you just call methods.
async
async
Auth without a framework
Password hashing (Argon2id), TOTP (Google Authenticator compatible), one-time codes, backup codes, JWT with middleware, OAuth2 (GitHub, Google) — all plain functions and types, no annotations.
let hash = hash.await?;
let valid = verify.await?;
let totp = from_base32?;
let ok = totp.verify;
Background jobs as plain functions
SQLite-backed queue with retries, exponential backoff, timeouts, scheduled execution, and idempotent enqueue. Handlers use the same extraction pattern as HTTP routes.
async
let worker = builder
.register
.start.await;
new.enqueue.await?;
Graceful shutdown in one line
The run! macro waits for SIGTERM/SIGINT, then shuts down each component in declaration order. No cancellation tokens, no orchestration code.
run!.await
Dependency injection without macros
Registry is a typed map. .add() at startup, Service<T> in handlers. No #[inject], no container config, no runtime reflection.
let mut registry = new;
registry.add;
registry.add;
// In any handler:
async
Request extraction with auto-sanitization
JsonRequest<T>, FormRequest<T>, and Query<T> call your Sanitize impl before the handler runs. Define it once, applied everywhere.
Middleware you'd write anyway
Rate limiting, CORS, CSRF, compression, security headers, request tracing, panic catching, error handler — all included with sensible defaults. All standard Tower layers, not a custom system.
And the rest
| Module | What it does |
|---|---|
template |
MiniJinja with i18n, HTMX detection, flash message integration |
sse |
Server-Sent Events with named broadcast channels |
email |
Markdown-to-HTML email rendering with SMTP |
storage |
S3-compatible object storage with ACL and upload-from-URL |
webhook |
Outbound webhook delivery with Standard Webhooks signing |
dns |
TXT/CNAME verification for custom domain validation |
geolocation |
MaxMind GeoIP2 location lookup with middleware |
rbac |
Role-based access control with guard middleware |
tenant |
Multi-tenancy via subdomain, header, path, or custom resolver |
flash |
Signed, read-once cookie flash messages |
cron |
Cron scheduling (5/6-field expressions) |
health |
/_live and /_ready health check endpoints |
cache |
In-memory LRU cache |
testing |
TestDb, TestApp, TestSession — in-process, no server needed |
Feature flags
Everything above the table is always available. The table modules are opt-in:
[]
= { = "0.1", = ["auth", "templates"] }
| Feature | Modules |
|---|---|
auth |
password, otp, totp, backup codes, jwt, oauth |
templates |
MiniJinja engine with i18n and static file serving |
sse |
Server-Sent Events broadcasting |
email |
Markdown email rendering + SMTP |
storage |
S3-compatible object storage |
webhooks |
Outbound webhook delivery |
dns |
DNS domain verification |
geolocation |
MaxMind GeoIP2 lookup |
sentry |
Sentry error tracking |
test-helpers |
TestDb, TestApp, TestSession |
full |
All of the above |
Re-exports
modo re-exports axum, serde, serde_json, and tokio so you don't need to version-match them yourself.
Claude Code Plugin
The modo-dev plugin gives Claude Code full knowledge of modo's APIs and conventions.
/plugin marketplace add dmitrymomot/modo
/plugin install modo@modo-dev
/reload-plugins
Once installed, it activates automatically when you build with modo. Or invoke it with /modo-dev.
Development
License
Apache-2.0