Skip to content

Architecture

Oxide is composed of focused crates, each with a narrow responsibility. Together they form a cohesive, Laravel-style stack on top of Rust.

Crate graph

Four published crates — one umbrella, one forced-separate proc-macro crate, one optional auth module, and one standalone CLI binary.

┌──────────────────┐
│ oxide-cli │ scaffolder, migrate, vendor:publish
└────────┬─────────┘
│ shells out
┌──────────────────┐
│ your app │ consumer project
└────────┬─────────┘
│ depends on
┌────────────┴────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ oxide-auth │ │ oxide │
│ Sanctum tokens, │ │ core, config, │
│ Hash, abilities, │──────│ http, db all in │
│ middleware gates │ │ one umbrella │
└──────────────────┘ └────────┬─────────┘
│ re-exports
┌──────────────────┐
│ oxide-macros │ #[controller],
│ (proc-macros) │ #[injectable],
│ │ #[request], etc.
└──────────────────┘

Published crates

oxide

The umbrella. Ships four internal modules:

  • oxide::core — TypeId-keyed IoC container, ServiceProvider trait, FromContainer for dependency-injected constructors.
  • oxide::config — TOML loader with shell-style env interpolation (${VAR:-default}), process-wide Config singleton, free helpers like config("app.name"), auto type-coercion.
  • oxide::http — the HTTP stack. Application, routing (hyper 1 + matchit), FromRequest extractors, Middleware trait and global middleware chain, Request / Response, validation envelopes, form requests, CORS, pagination.
  • oxide::db — database layer on SeaORM. Database handle with lazy pool, DatabaseServiceProvider, Blueprint-shape migration DSL, Model facade, global conn() accessor, timestamps_behavior!() macro.

Transparently re-exports every macro from oxide-macros, so consumers only declare oxide in their Cargo.toml.

oxide-auth

Sanctum-style API tokens, kept in its own crate because it pulls bcrypt + crc32fast + subtle + sha2 — projects that don’t need auth shouldn’t compile that. Opt-in: add oxide-auth = { git = ... } to your Cargo.toml and register AuthServiceProvider in bootstrap.rs; see Authentication.

PersonalAccessToken entity published into your project via vendor:publish sanctum. Authenticatable + HasApiTokens traits for any user entity. Auth helper for verification, Hash::make / Hash::check bcrypt wrappers, auth_middleware::<U>(), abilities([...]), ability([...]) middleware gates.

oxide-macros

Proc-macros: #[controller(prefix = "...")], #[api_resource], #[injectable], #[request], #[get / post / put / patch / delete] route markers. Separate crate because Rust requires proc-macros to live in a dedicated crate with proc-macro = true. Re-exported by oxide so consumers never depend on it directly.

oxide-cli

The oxide binary. new scaffolder, make:migration, vendor:publish, forwards serve / migrate / migrate:rollback / migrate:fresh / migrate:status to the consumer project.

Install once, globally, with cargo install oxide-cli — the binary is named oxide and is project-aware.

Layer conventions inside your app

src/
├── main.rs entry point + subcommand dispatch
├── bootstrap.rs which service providers to register + in what order
├── config.rs typed config structs (bind via bind_config::<T>)
├── providers/ each provider wires one concern (config, DB, routes, …)
├── routes/ one file per URL surface
├── app/
│ ├── domain/<name>/ entities + domain services (DDD-style)
│ └── http/
│ ├── controllers/
│ ├── middleware/
│ ├── requests/ FormRequest-shape validators
│ └── resources/ API resource transformers
└── database/migrations/ Migrator with all versioned schema changes
config/ *.toml files (env-interpolated)

Request lifecycle

  1. hyper accepts a TCP connection, builds an http::Request.
  2. server::handle reads the body (capped by APP_BODY_MAX_MB) and wraps it in an oxide::http::Request.
  3. Wraps the whole flow in tokio::time::timeout so handlers can’t pin a worker forever.
  4. Runs the global middleware chain (CORS, anything added via app.use_middleware(...)) — outer → inner.
  5. Terminal dispatcher looks up the route. Missing path → 404; wrong method → 405. Both still go through the global chain on the way out so CORS decorates them.
  6. Route-specific middleware chain runs next (auth, abilities, …).
  7. Handler runs. If it takes a typed FromRequest arg, the extractor validates; failure short-circuits with a 422 envelope.
  8. Response bubbles back up through every wrapping middleware layer so they can decorate headers (Cors, LogRequests, etc.).
  9. hyper serializes and flushes to the socket.

Graceful shutdown

SIGTERM / SIGINT flips a broadcast flag. The accept loop exits, new connections are refused. Each in-flight connection gets told to graceful_shutdown() — it completes its current request, then closes. APP_SHUTDOWN_TIMEOUT_SECS caps how long stubborn connections get before the process exits anyway.