Ferra and the Rust ecosystem
Ferra is a thin layer over a small set of upstream Rust crates that already solve specific concerns well. This page explains what is Ferra-defined and what is re-exported, so that you know where to look for documentation and which surface to import when you build an application.
If you are looking for “five-minute zero-to-API,” see
Getting started. This page is about the long-term
shape of the dependency graph you inherit when you depend on ferra.
The principle: cohabitation, not encapsulation
Ferra does not wrap the upstream crates it integrates. Instead, it does one of two things:
- Direct re-export — when an upstream crate already offers a stable,
namespaced surface, Ferra simply re-exports it under
ferra::*. You importferra::sea_orm::DatabaseConnection,ferra::biscuit_quote::check, and you call them with the API the upstream crate documents. There is no Ferra-renamed equivalent. - Default-locking + opt-in feature — when an upstream crate is correct for some applications but not all, Ferra locks the upstream-default configuration explicitly behind a Cargo feature, so the dependency weight only lands when you opt in.
Ferra introduces a wrapper or a Ferra-defined trait only when one of three things is true:
- There is concrete behaviour to inject at the call site (logging, validation, span propagation).
- A typed Ferra newtype prevents misuse that the upstream type does not
catch (
Email,UserId). - A Ferra trait abstracts over multiple backends with build-time or
runtime swap-out (
FerraAuthProvider,RateLimitStore).
If none of those apply, the upstream surface is exposed verbatim and the upstream documentation is the canonical reference.
What lives under ferra::*
Ferra-defined surface
The ferra:: namespace defines a small, stable user-facing API:
| Namespace | What it is |
|---|---|
ferra::Foundry | The router builder — Foundry::new(conn).mount::<M>().build(). |
ferra::Id | The framework’s identifier type used by #[id] fields. |
ferra::Router | The composed Axum router returned by Foundry::build(). |
ferra::FerraError | The framework-wide error enum surfaced in HTTP responses. |
ferra::FerraLayer | The Tower layer composition entry point. |
#[derive(FerraModel)] | The proc-macro that turns a struct into a Ferra resource. |
#[ferra(...)] | The attribute namespace for Ferra-specific concerns. |
#[id], #[field(...)] | Field-level attributes for primary keys and projection rules. |
#[authorize(...)] | Operation-level authorization rules. |
The Ingot, ferra-forge, ferra-anvil thematic names belong to internal
documentation. You will not see them in your application code.
Re-exported upstream surface
The rest of ferra::* is re-exports. The biggest namespaces:
| Namespace | Re-exports | Documentation |
|---|---|---|
ferra::sea_orm | All of sea_orm | sea_orm docs.rs |
ferra::biscuit_quote | All of biscuit-quote | biscuit-quote docs.rs |
ferra::tracing | All of tracing | tracing docs.rs |
When you write:
#![allow(unused)]
fn main() {
use ferra::sea_orm::Database;
let conn = Database::connect("postgres://...").await?;
}
…you are calling sea_orm::Database::connect. The upstream documentation
applies verbatim. There is no Ferra-side wrapping.
Crate graph: modules pub(crate), items glob-exported at root
Every framework implementation crate (ferra-core, ferra-db,
ferra-forge, ferra-http, ferra-openapi) keeps its modules
pub(crate) and re-exports its public items at its crate root. The
facade then writes pub use ferra_<crate>::*; once per implementation
crate. A consequence is that two implementation crates can share a
module name (emit, routes, schema, …) without collisions: only
the items at each crate’s root reach the facade, and the items
themselves are uniquely named across the workspace.
This rule is enforced mechanically. The cargo clippy --workspace -- -D ambiguous_glob_reexports lint fires the moment two glob
re-exports in the facade name the same item from different sources;
CI gates the lint as a release-blocker (FR-024). A future regression
that exposes a sub-module from any implementation crate as pub
would be caught the next time the facade picks up a sibling-crate
item with the same name.
If you are reading the source code: prefer the absence of pub mod
on every implementation crate’s lib.rs. The single exception is
ferra::prelude on the facade itself — the prelude is intended to be
opened directly by consumers, so its module visibility is pub (FR-025).
Cargo feature surface
Ferra’s default dependency graph is intentionally lean. Heavy or audience-partial features are opt-in.
| Feature flag | What it enables | When you want it |
|---|---|---|
| (default) | Core framework + tracing + structured stderr JSON logging | Always. |
otel | OpenTelemetry SDK + OTLP/HTTP exporter (locked to upstream-default transport) | You run an OTel collector and want OTLP exports. |
otel-grpc | otel + gRPC transport (instead of HTTP/protobuf) | Your collector requires gRPC and your ingress preserves HTTP/2. |
otel-fips | otel + runtime-installed FIPS-compliant rustls CryptoProvider | FIPS-compliant deployments. |
Activating a feature
[dependencies]
ferra = { package = "ferra-rs", version = "0.x", features = ["otel"] }
The
package = "ferra-rs"alias is required because theferraname on crates.io is owned by an unrelated project; the framework’s facade is published asferra-rs. See Getting Started for full details.
Adding the feature pulls the locked OTel SDK pin into your build; without the feature, none of those crates compile. There is no runtime check — the choice is at compile time.
Where to look for documentation
| Question | Source |
|---|---|
| How do I declare a model? | This user guide → ferra-forge.md |
What does Foundry::new(...) do? | This user guide → ferra-http.md |
| How do I run a database query? | Upstream — sea_orm docs.rs (Ferra re-exports verbatim) |
What are valid Datalog rules in #[authorize]? | Upstream — biscuit documentation + the rule grammar reference |
| How do I configure tracing subscribers? | Upstream — tracing-subscriber docs.rs (Ferra re-exports verbatim) |
When you ask an AI coding assistant about a Ferra application, the assistant should reach for upstream documentation for re-exported surfaces and for this user guide for Ferra-defined surface. The distinction matters because upstream training corpora are large and Ferra-specific corpora are small — the cohabitation pattern is what makes a Ferra application legible to AI agents trained on the broader Rust ecosystem.
A worked example
A complete Ferra application demonstrating both halves of the principle:
// Cargo.toml:
// ferra = { package = "ferra-rs", version = "0.x", features = ["otel"] }
// Direct re-export (Half A): upstream `sea_orm` reached without renaming.
use ferra::sea_orm::Database;
// Direct re-export (Half A): upstream `biscuit-quote::check` for compile-time
// Datalog. A typo here is a `cargo build` error, not a runtime panic.
use ferra::biscuit_quote::check;
// Ferra-defined surface: the model derive, the router builder.
use ferra::{Foundry, FerraModel, Id};
#[derive(FerraModel)]
struct Film {
#[id]
id: Id,
title: String,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Upstream API (sea_orm): no Ferra-side wrapper.
let conn = Database::connect("postgres://localhost/films").await?;
// Ferra-defined: the router composition surface.
let app = Foundry::new(conn).mount::<Film>().build();
// Upstream API (axum / hyper): Ferra returns a standard Axum Router.
axum::serve(tokio::net::TcpListener::bind("0.0.0.0:3000").await?, app).await?;
Ok(())
}
The same application without the otel feature compiles to a smaller
binary; the OTel SDK is not in the dependency graph at all.
When you need a Ferra wrapper that does not yet exist
If you find yourself wanting a Ferra-side wrapper around an upstream type — a Ferra newtype, a Ferra trait abstracting multiple backends, or a Ferra macro that does something the upstream macro does not — the wrapper is a feature request worth filing. The maintainers will ask which of the three justifications applies (concrete behaviour to inject, type-narrowing, swap-out insurance) and ship the wrapper at that point. Until then, the upstream surface is the documented path.