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:

  1. What requirement forces this? "Users must log in" is a requirement. "Real apps have auth" is not.
  2. Can something simpler satisfy it now? A Vec can back a store. A constant can back configuration.
  3. 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...AddMinimal form
Remember data past a restart, or share it across processesiet-db + a provideriet-db-timescale
Vary values by environment, or keep secrets out of sourceiet-configiet-config + iet-config-env
Serve a browser or HTTP clientiet-http-axum and/or iet-http-htmx-axumthe host feature for a listener
Know who is callingiet-auth + a mechanisma session feature + an axum integration
Distinguish "logged in" from "allowed"iet-authza deny-by-default authorizer
Avoid repeating an expensive readiet-cacheiet-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, CallScope and transactions. Budget for those, not just a new struct.
  • crates/core/iet-db defines lifecycle and migration contracts with no driver SQL types; crates/features/iet-db-timescale implements 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-config has no implicit environment access and no global state, and Secret<T> redacts Debug and has no Display. 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-axum provides REST conventions and, behind its host feature, the listener. crates/features/iet-http-htmx-axum provides 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 import axum.

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 real Principal.
  • crates/core/iet-auth is 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-authz offers Authorizer, RoleStore, AccessStore, and All/Any combinators. Use a deny-by-default authorizer even before you have a policy: if an operation is ever marked Access::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-cache is one typed contract; crates/features/iet-cache-memory is 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 the recording feature 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.

IET Framework documentation — built with the IET Framework.