Choosing capabilities
The framework is a set of capabilities, not a required stack. A first app can be
just iet-core plus your own code. Add a capability when a concrete requirement
forces it, and skip it while a trivial substitute works.
Ask three questions before adding anything:
- What requirement forces this? "Users must log in" is a requirement. "Real apps have auth" is not.
- Can something simpler satisfy it now? A
Veccan back a store. A constant can back configuration. - What does it cost? Each capability adds a dependency edge and a lifecycle (a pool, a background task, a secret to load). Add it when that cost is less than the requirement it removes.
The six primary choices
| The app must... | Add | Minimal form |
|---|---|---|
| Remember data past a restart, or share it across processes | iet-db + a provider | iet-db-timescale |
| Vary values by environment, or keep secrets out of source | iet-config | iet-config + iet-config-env |
| Serve a browser or HTTP client | iet-http-axum and/or iet-http-htmx-axum | the host feature for a listener |
| Know who is calling | iet-auth + a mechanism | a session feature + an axum integration |
| Distinguish "logged in" from "allowed" | iet-authz | a deny-by-default authorizer |
| Avoid repeating an expensive read | iet-cache | iet-cache-memory |
Persistence
- Add when data must survive a restart, two processes must see the same rows, or you need real transactions.
- Skip while an in-memory store is enough. The contract port hides the
storage code, so the service and routes do not change — but a durable adapter
also brings configuration, a session type, and, if a write spans more than one
statement,
CallScopeand transactions. Budget for those, not just a new struct. crates/core/iet-dbdefines lifecycle and migration contracts with no driver SQL types;crates/features/iet-db-timescaleimplements the session and transaction traits and owns migrations. The provider also owns the real, non-fakeable work: pool limits, health checks, and the non-retryable commit failure.
Configuration
- Add when a value changes between environments or must not live in source.
- Skip while constants are genuinely constant.
crates/core/iet-confighas no implicit environment access and no global state, andSecret<T>redactsDebugand has noDisplay. That does not make logging impossible —.expose()returns the value — and it does not zero memory. Treat redaction as a guardrail, not a guarantee. Load configuration once at composition and inject the validated settings, rather than reading environment variables deep in business code.
HTTP transport
- Add when a browser or HTTP client calls you.
- Skip while your entry point is a CLI or a background worker.
crates/features/iet-http-axumprovides REST conventions and, behind itshostfeature, the listener.crates/features/iet-http-htmx-axumprovides fragments, forms, request detection, and assets. Keep transport at the edge: in the starter, the adapter implements the routes and the composition root also uses the host, but the contract and service never importaxum.
Authentication
- Add when not every caller is trusted — a browser session, a password, a bearer token.
- Skip while a single trusted operator runs the process. You can still mark
operations
Access::Authenticated; they will simply fail until you supply a realPrincipal. crates/core/iet-authis only ports (Authenticate,TokenIssuer,Csrf,ProfileStore) plus one explicit default,NoProfiles. Mechanisms, storage adapters, and transport adapters are chosen separately. CSRF protects cookie-authenticated browser requests; an API authenticated by a bearer header does not need it.
Authorization
- Add when "logged in" is not the same as "allowed" — roles, per-resource access, ownership.
- Skip while every authenticated user may do everything.
crates/core/iet-authzoffersAuthorizer,RoleStore,AccessStore, andAll/Anycombinators. Use a deny-by-default authorizer even before you have a policy: if an operation is ever markedAccess::Permission(...), it fails closed instead of silently succeeding.
Caching
- Add when a read is expensive and repeated, and slightly stale answers are acceptable.
- Skip while the database is comfortably fast. For a derived read cache,
caching is a performance layer, not a correctness layer — but the framework
also uses cache backends as authoritative stores (
iet-auth-session-cache), where the cache is the source of truth. Decide which kind you are adding. crates/core/iet-cacheis one typed contract;crates/features/iet-cache-memoryis a bounded, process-local backend. The contract documents safety rules: errors are not misses, zero TTL bypasses and removes, and a transaction-active read bypasses the shared cache. Keys are expected to include every result-changing argument and the visibility scope — the cache does not enforce that, so leaving the owner out of a key is a caller bug. The starter demonstrates the discipline with a revision counter folded into each key.
Extras, add later
These are worth knowing about but rarely belong in a first app.
- Telemetry (
iet-telemetry,iet-telemetry-otel): add when operational questions go beyond stdout. The core is a compile-time no-op unless therecordingfeature is on, so calling it is cheap. - Rate limiting (
iet-rate-limit-core,iet-rate-limit): add when an endpoint is exposed to the public or to noisy clients. Enforcement scope equals backend scope, so a process-local limiter limits per process, not per cluster. - Background work (
iet-app,iet-runtime-tokio): add when work must outlive a request or needs supervised shutdown. A single HTTP listener does not need it — the starter drives its host directly. - Object storage (
iet-storage): add when bytes are too large or too binary for rows. Authorization and tenant scoping remain caller policy. - UI assets (
iet-ui,iet-ui-build): add when you server-render escaped HTML. Skip for JSON APIs. - Stacks (
iet-stack-web-axum,iet-stack-auth-htmx-axum): presets that register groups of already-built adapters. Add when a preset matches your routes; they do not generate an app, connect a database, or start a listener.
How adding works
The dependency direction is fixed: core ← features ← integrations ← stacks.
Applications sit above it and are the only layer that picks concrete stacks.
Because your contract defines the trait, adding a capability is usually: implement one more port, add one dependency edge in the implementation or composition crate, and change nothing in the code that calls it. If a capability needs a database or transport type inside your contract crate, it belongs behind a port instead.
The starter's Cargo.toml is the "everything on" end state; every entry maps to
a concrete need. You are not expected to match it. See
application-model.md for the four roles and
calls-and-transactions.md for the call model.