Repository navigation
v0.9.0
The second release under the plan of plans: typed SQL with migrations, repositories, three
databases and TLS; typed routes with OpenAPI; authentication and authorization; test containers;
and the Ledger reference app that uses all of it in CI. Upgrading from 0.8: see the
0.9 migration guide.
Added
- New crates (ADR 0010 Amendment 5, tier
platformsince the Ledger reference app uses them):
id_effect_auth,id_effect_authz,id_effect_test,id_effect_redis. examples/reference/ledger: the Ledger reference app (CI jobreference-ledger). Double entry
over typed routes,SERIALIZABLEpostings, PostgreSQL sessions, JWT, OIDC + PKCE, TOTP, RBAC, a
Redis cache, a read replica and Vault, built only throughid_effect_kit. New on the way:
Route::handler_with_request_caps(id_effect_axum), kit featureaudit.id_effect_redis: pooledRedisClientlayer; typedRedisKv/RedisHash/RedisJson
stores;RedisCacheBackend; pub/sub as aStream;RedisSessionStore(featuresession);
RedisRateLimitStore(featurerate-limit). Coreid_effect: additiveCacheBackendtrait and
Cache::with_backend(sameCacheAPI, in-process single-flight kept, backend failures counted
byCache::backend_errors).id_effect_config: per-environment profiles (Profiles,ID_EFFECT_PROFILE/--profile)
layeringconfig/base.toml→config/<profile>.toml→APP_*env (env > profile > base); an
unknown profile isConfigError::UnknownProfile;Profiles::overlaytakes a secret provider.id_effect_axum::route: typed routes —
Route::get("/accounts/{id}").path::<P>().query::<Q>().headers::<H>().body::<B>().response::<R>(status).handler(f)
withHasSchemaparts, every decode issue reported at once (422);route::Routerkeeps a
RouteRegistryofRouteInfo(method, path, schema per part, JSON Schema 2020-12) for OpenAPI
and route listings. The existingrouting::get/post/…are unchanged.id_effect_axum: RFC 9457Problem(application/problem+json) witherrors[]of
{in, pointer, message}; typed routes answer invalid input with it (422, or 400 for malformed
JSON) and list every issue. Standalone extractorsValidPath,ValidQuery,ValidHeaders,
ValidJson,ValidForm(urlencoded and multipart text fields) share the same decoder.
Route::form::<T>(),RouteInfo::body_kind,json::decode_json. Absent array / nullable
query, form and header fields decode to[]/null.id_effect_axum::middleware:CompressionLayer(gzip / brotli / deflate),RequestIdLayer
(x-request-idpropagation,RequestIdextractor, onehttp.requesteffect span per request),
TimeoutLayer(503 Problem; interrupts the handler's fiber),BodyLimitLayer(413 Problem) and
RateLimitLayer(429 Problem +Retry-After; token buckets behind a pluggable
RateLimitStore). New dependencyid_effect_resilience.RateLimitKey::tenantkeys limits by
the request's tenant (read fromRequestCapsor a request extension).id_effect_axum:server::serve_dirstatic files with strongETag/Last-Modified/ 304,
If-Match/If-Range, precompressed.br/.gz, hashed-assetCache-Control, traversal
and hidden-file protection;middleware::HttpCacheLayer+CacheControladdCache-Control
and a body-hashETagto GET responses and answerIf-None-Match/If-Modified-Sincewith
304. Book: part6/ch29-03-middleware.md; example090_middleware.id_effect_axumfeatureopenapi:OpenApi::from_routergenerates an OpenAPI 3.1 document from
typed routes. Component schemas are exactly the decoding schemas' JSON Schema; path / query /
header parameters, request bodies per media type, documented and Problem
(application/problem+json) responses, security schemes (bearer JWT, session cookie, OpenID
Connect) with per-operation requirements from the newRouteInfo::policies.
Endpoint::from_parts/Endpoint::layer;id_effect_authzguarded endpoints convert into an
Endpointand record their policies. Opt-inopenapi_router(&doc, OpenApiUi::scalar())/
doc.routes()serve/openapi.json(ETag, 304) and a Scalar/docspage loaded from a CDN or
mirror (cdn_url,integrity) under its own strict CSP that works behindSecurityLayer;
route::introspect::routes_routerserves a/_routestable (method, path, handler, schemas,
policies) as JSON or text;RouteInfo::handlerrecords the handler's type name. Book:
part6/ch29-02-openapi.md; example085_openapi.id_effect_authz:Principaland theCurrentPrincipalcapability;Policy<Res>evaluated as an
effect returningDecision, combined withand/or/negate;Rbac(roles → permissions,
inheritance, wildcards); the named-policy registryPolicieswithauthorize/check;
ownership, tenant and ACL resource policies; claim (ABAC) policies;AuthzError::http_status
(401 / 403 / 500). Featureaxum: route guards (Guardlayer;.authorize(guard)on an axum
MethodRouterand on a typedEndpoint) answering 401 / 403 / 500 asapplication/problem+json.
Every decision can be recorded as a serdeDecisionEventto theAuthzEventssink capability.
Featuretesting:AuthzTestwithassert_allows/assert_denies/assert_denied_because.id_effect_auth: argon2idPasswordHasher(OWASP defaults, rehash-on-verify, dummy verification
for unknown users, hashing on the blocking pool) and server-sideSessionsover aSessionStore
(MemorySessionStore; idle and absolute timeouts, id rotation on login, rotate / logout /
revoke-all,__Host-Secure HttpOnly SameSite=Lax cookie); featureaxumaddsSessionLayer.
Credential middlewareAuthenticate(session → bearer JWT → API key → Basic, configurable
order) installs aCurrentPrincipal(AuthPrincipal) for the authz guards; a bad credential is a
401 Problem withWWW-Authenticate. JWT issue / verify (featurejwt: HS / RS / PS / ES / EdDSA,
iss / aud / exp / nbf / leeway, required claims, claim → roles / permissions mapping);
JwksClientwith kid-rotation refetch, rate limiting and stale-cache fallback (featurejwks);
hashed API keys with constant-time compare; Basic auth viaUserLookup+verify_or_dummy.
Featurepg:PgSessionStorekeeps sessions in PostgreSQL (SHA-256 of the session id as the
key, jsonb attributes, timestamptz expiries, upsertsave),pg_session_store_layer(),
ensure_schema()and the migrationmigrations/0001_id_effect_sessions.{up,down}.sql.
Featureoidc: OpenID Connect login.OidcClient(discovery, authorization code + PKCE S256,
state / nonce, ID tokens verified over the provider's JWKS withiss/aud/exp/nonce
/azp,ClaimMapping, RP-initiated logout; implementsid_effect_platform'sOAuthClient)
andOidcRoutes(/auth/oidc/{login,callback,logout}, a separate flow session, login through
Sessions::login). Tested against a wiremock IdP and a Keycloak container.
Featuretotp: TOTP second factor (totp-rs). Enrolment with anotpauth://URI, ±1-step
verification on the executor clock with per-user replay protection, single-use recovery codes
stored as argon2id hashes, and a session step-up withamrclaims for therequire_mfaauthz
policy.id_effect_config: HashiCorp Vault KV v2 secret provider behind the newvaultfeature
(VaultSettings,VaultProvider::layer,VaultAuth::token). Secrets load once in a layer, come
back asSecret<T>, and merge above the profile files and below the environment;
refresh_everyre-fetches and swaps atomically; failures areConfigError::Vault.
Secret<T>now implementsDeserialize.id_effect_sql: typed SQL.SqlValue(bool, i16/i32/i64, f32/f64, text, bytes, uuid, jiff
Timestamp/DateTime/Date/Time,rust_decimal::Decimal, JSON),ToSqlValue/
FromSqlValue,params!, typedSqlRow::get::<T>(name | index)with errors naming the column,
SqlColumnmetadata,Json<T>. PostgreSQL binds NULL untyped and decodes by column type;
unsupported column types are a clear error instead of a silent"".testing::arb_sql_value
(featureproptest). Book: part6/ch28-01-typed-sql.md.id_effect_sql:FromRowand#[derive(FromRow)](rename,default,flatten,#[schema]
renames,#[from_row(schema)]validation into the newSqlError::InvalidRow), tupleFromRow,
query_as/query_one_as/query_opt_as,SqlError::RowNotFound.id_effect_sql: nestedsavepointscopes; isolation / read-only / deferrable transactions
(transaction_with,with_transaction_options,SqlClient::begin_with);
retry_serialization_failuresover aSchedule; streaming queries (query_stream,
query_stream_as) with bounded buffering and cancel-on-interrupt on PostgreSQL.TestSqlClient
supports all three.id_effect_sqlfeaturemigrate:Migrator(from_dir,from_files,from_sqlxfor
sqlx::migrate!embedding) withrun/status/revert(n)/checkas effects and a startup
Layer; sqlx-cli file naming and checksums, tracking table_id_effect_migrations, one
transaction per migration under a PostgreSQL advisory lock with a timeout, typedMigrateError.id_effect_sqlfeaturesqlite:SqliteSqlClientover sqlx SQLite (in-memory and file
databases, savepoints, streaming,Serializable→BEGIN IMMEDIATE, busy → retryable 40001).
RoutingSqlClientsends reads to replicas (round-robin or custom) and writes / transactions to
the primary. One conformance suite runs on PostgreSQL and SQLite.id_effect_sqlfeaturemysql:MySqlSqlClient/MySqlPoolConfigon sqlx; the shared
conformance suite passes on MySQL 8. Isolation levels,READ ONLYtransactions, savepoints,
deadlocks and lock wait timeouts retryable as SQLSTATE 40001, bounded streaming with
KILL QUERYon drop.id_effect_sqlfeaturequery: build statements with sea-query 1.0, render them per dialect
(BuildSql::build_sql(Dialect::{Postgres, MySql, Sqlite})→ SQL +Vec<SqlValue>, our own value
bridge, no sea-query-binder) and run them withexecute_query/fetch_all_as/
fetch_one_as/fetch_optional_as.id_effect_sqlfeaturerepository:#[derive(Entity)]andRepository<T>. CRUD, paging,
optimistic locking via#[entity(version)](SqlError::StaleVersion), soft-delete / tenant /
custom global filters on every generated statement withwith_deleted()/unscoped()escape
hatches (a tenant-scoped entity with no tenant fails closed); runs in transactions via
repo.on(tx). Book: part6/ch28-03-repositories.md; example040_repository.- Book part6/ch30-07-audit-tenancy.md: the row-level tenant pattern end to end (authenticated
principal →CapOverrideLayersettingCurrentTenantandAuditContext→ tenant-filtered
Repository→audit::recordin the request's transaction →verify_chain), its fail-closed
behaviour and the alternatives it does not cover (row-level security, schema or database per
tenant); exampleid_effect_sql/050_tenancy_audit. id_effect_sqlfeatureaudit: hash-chained audit log.AuditLoglayer,append/record
inside the caller's transaction, one chain per tenant withsha256(prev_hash ‖ record)(the
chain head is row-locked, so concurrent appends never fork it),verify_chainreports the first
edited, removed or relinked record; bundled migrationmigrations/audit/.id_effect_test: effect-native test containers (testcontainers). Postgres (with a ready
SqlClientlayer under featuresql), MySQL, Redis, Keycloak, Vault and a generic
ContainerSpecas scoped layers, removed when their scope closes (success, failure, defect or
interrupt);Sharedone-per-binary containers with the Ryuk reaper; readiness waits with typed
timeouts (ContainerError::NotReady);require_docker()prints SKIP, or fails under
ID_EFFECT_REQUIRE_DOCKER=1. Book: part4/ch15-07-containers.md.
Per-test database isolation:isolated_db()(a rolled-back transaction over anySqlClient,
with savepoints per statement and per nested transaction) andTemplateDb(a migrated template
cloned per test withCREATE DATABASE … TEMPLATE, dropped on scope close);Fixture/
Factorywith seeded Schema-arbitrary defaults andFromRowrows (featurefixtures); an
in-processTestClientover any towerService(featurehttp). Book:
part4/ch15-08-integration-testing.md; example020_isolated_db. Featurecontract:Contract/
TestResponse::assert_conformscheck responses against the response schema the OpenAPI document
declares for their operation and status, naming each violation by JSON pointer.id_effect_kit: featuresopenapi,sql,sql-sqlite,sql-mysql,migrate,repository,
auth(+auth-oidc,auth-totp),authz,redis,config-vaultandtest(for
dev-dependencies; not infull);fullupdated; the prelude gains typed routes,Problem,
Valid*, middleware,OpenApi,FromRow/Entity/Repository,Migrator, the auth and
authz types andProfiles. Derives used through the kit alone need
#[from_row(crate = id_effect_kit::sql)].id_effect_toolservicetemplate: appliesmigrations/withid_effect_sql::migrate::runon
start (no moreinclude_str!+ execute on every start), serves a typed Problem+JSON route
behind request-id / compression / timeout / body-limit middleware, and with--db pgits CI
runsid-effect migrate run+migrate check.id_effect_tool:id-effect migrate new|run|status|revert|check(sqlx-cli file layout;
sequential or timestamp numbering following the directory;DATABASE_URL/.env;--json;
exit codes 0 / 1 / 2 / 3).checkexits 1 on pending, drifted or missing migrations, for CI.
Book: part6/ch28-02-migrations.md; exampleid_effect_sql/examples/030_migrate.rs(in memory by
default, live on PostgreSQL withDATABASE_URL/--live).
Changed
- Breaking
id_effect_sql: statements take&[SqlValue]; rows are typed;SqlErroris
#[non_exhaustive]. See MIGRATION-0.9.md. - Breaking
id_effect_tool:cli::CommandgainsMigrateand is#[non_exhaustive]. id_effect_sql: the backends enable sqlx'stimefeature (sqlx 0.8.6 or later), so sqlx's
query!macros type date-time columns astimetypes even whenchronois also enabled. See
MIGRATION-0.9.md.
Deprecated
id_effect_sql::SqlParam,SqlRow::new,SqlRow::text_cells(removed in 0.10).id_effect_axum::json::decode_json_schema,JsonSchemaError,SchemaJsonErrorBody(removed in
0.10; see MIGRATION-0.9.md).
Security
id_effect_sql: TLS for PostgreSQL and MySQL (rustls on aws-lc-rs, no OpenSSL); 0.8 connected in
plaintext.PgPoolConfig/MySqlPoolConfigtakewith_tls_mode(PgTlsMode / MySqlTlsMode),
with_root_cert_file/with_root_cert_pemandeffective_tls_mode(), overriding the URL's
sslmode/ssl-mode. The default staysPrefer/Preferred(unverified, may fall back to
plaintext); useVerifyFull/VerifyIdentityin production. With sqlx 0.8,VerifyCaalso
checks the host name (stricter than its name).id_effect_config:ProfiledConfigandFigmentConfigProviderno longer print merged values in
Debug, and errors fromConfig::secret()loaders no longer contain the raw value.id_effect_node: the handshake docs no longer call the QUIC transport mutual TLS (no client
certificate; the server certificate is verified only with pinned roots).
Scorecard
Measured by blind re-score (docs/scorecard/REPORT-0.9.0.md, METHOD 2.1):
| Index | 0.8.0 | 0.9.0 | Target |
|---|---|---|---|
| Enterprise (E) | 55.2 (#5) | 67.2 (#3) | 64.6 (#3) |
| Runtime (RT) | 64.4 (#2) | 70.8 (#2) | 68.9 (#2) |
| Rows < 40 | 31 | 21 | ≤ 21 |
| Rows where id_effect leads | 7 | 19 | — |
24 rows up, none down. Data 39.7 → 79.6 (ahead of every competitor), Security 48.2 → 87.8. Every
0.9-targeted row met or beat its target except dx.scaffold (75; the generators moved to 0.10).
DX KPIs: K3 (lines to add the invoice resource to Ledger) 508, K1 67.6 s, K4 17/20, K5 94.7 %.
Release artifacts
Each crate ships with its .crate file and a CycloneDX SBOM (.cdx.json).
Full Changelog: v0.8.0...v0.9.0