arcly-http 0.1.0

Enterprise-grade NestJS-inspired web framework on axum: zero-lock DI, declarative controllers, multi-tenant data routing, transactional outbox, ABAC, and a self-documenting OpenAPI surface
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
<div align="center">

# ⚡ arcly-http

### NestJS ergonomics meets Rust safety & speed

*A batteries-included, enterprise-grade HTTP framework built on axum — declarative controllers, zero-lock DI, a complete auth pipeline, realtime gateways, and first-class observability.*

[![Rust](https://img.shields.io/badge/rust-1.75%2B-orange.svg?logo=rust)](https://www.rust-lang.org)
[![axum](https://img.shields.io/badge/built%20on-axum%200.7-blue.svg)](https://github.com/tokio-rs/axum)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](#-license)
[![OpenAPI](https://img.shields.io/badge/OpenAPI-Swagger%20UI-85EA2D.svg?logo=swagger&logoColor=black)](#)

[Quick Start](#-quick-start) •
[Features](#-feature-matrix) •
[Architecture](#-architecture) •
[Auth Pipeline](#-authentication--authorization) •
[Examples](#-examples) •
[Configuration](#-configuration)

</div>

---

## 👀 At a Glance

```rust
use arcly_http::prelude::*;

pub struct UserController;

#[Controller("/users", tags("users"))]
impl UserController {
    /// Fetch a single user by ID — JWT-protected, traced, documented.
    #[Get("/:id", summary("Get user"), security("bearer"))]
    #[UseInterceptors(TraceInterceptor)]
    async fn get_user(
        svc: Inject<UserService>,          // ← zero-lock DI, O(1) resolution
        ctx: RequestContext,               // ← claims & session pre-decoded
        #[Param("id")] id: u64,            // ← typed path extraction
    ) -> Result<Json<User>, HttpException> {
        JWT_AUTH.check(&ctx)?;             // ← zero-overhead guard
        svc.find(id).map(Json).ok_or_else(|| NotFound::new("user not found").into())
    }
}

#[Module(controllers(UserController), providers(UserService))]
pub struct AppModule;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    App::launch::<AppModule>("0.0.0.0:3000").await   // /docs, /healthz, /metrics — free
}
```

One annotation gives you: routing, OpenAPI docs, JWT decoding, distributed tracing, and dependency injection — with **no locks on the request hot path**.

---

## 📦 Feature Matrix

| | Capability | Details |
|---|---|---|
| 🧩 | **Declarative routing** | `#[Controller]`, `#[Get]`/`#[Post]`/`#[Put]`/`#[Patch]`/`#[Delete]`, typed `#[Param]`/`#[Query]`/`#[Body]` extraction |
| 🔌 | **Zero-lock DI** | `#[Injectable]` + `#[Module]` DAG → frozen `&'static` container; `Inject<T>` resolves in O(1) with **no locks, no allocation** |
| 🔐 | **JWT auth** | `JwtService` (HS/RS/ES families), access + refresh pairs, automatic Bearer decoding at the boundary, `JWT_AUTH` / `RoleGuard` |
| 🍪 | **Signed cookies** | `CookieService` — HMAC-SHA256, tamper-proof; boundary falls back to the JWT cookie when no Bearer header (browser-friendly, no localStorage) |
| 🗂️ | **Server-side sessions** | `SessionManager` + pluggable `SessionStore` trait; `ctx.session()` on every request; `SESSION_AUTH` guard |
| 🌐 | **OAuth 2.0 + PKCE** | `OAuth2Provider` trait + registry — Authorization Code flow with atomic single-use CSRF state; Google & GitHub providers in the example |
| 🛡️ | **Fine-grained permissions** | `resource:action` perms embedded in JWT (`issue_access_with_perms`) → zero-I/O permission checks with wildcard support (`users:*`) |
| 🏢 | **Multi-tenancy** | `TenantRegistry` (header / subdomain strategies, frozen map) resolved once per request; `ctx.tenant()` + `TENANT` guard cross-checks the JWT `tenant` claim against the resolved tenant |
| 🌍 | **Distributed rate limiting** | `DistributedRateLimit` over a pluggable `RateLimitBackend` — cluster-wide sliding window (atomic Redis Lua), `sub`-then-IP principal keying, explicit `FailOpen`/`FailClosed` policy |
| 🔄 | **Secrets rotation** | `SecretSource` (Vault / AWS SM / env) + `Rotating<T>` over `ArcSwap` — hot-swap `JWT_SECRET`/cookie secrets with zero restarts; previous key verifies through the grace window |
| 🗄️ | **Datasource routing** | `DataSource` trait + frozen `DataSourceRegistry`: tenant-scoped pools, read/write splitting, `ReadAfterWritePin` defeats replica lag |
| 📋 | **Audit trail** | `#[AuditLog(action, resource)]` — one record per mutation (who/what/when/where/outcome), lock-free `try_send` hot path, hash-chained (SHA-256) batches into an app-provided append-only `AuditSink` |
| 📤 | **Transactional outbox** | `OutboxTx::enqueue` inside the business transaction + background `OutboxRelay` (poll → publish → ack, at-least-once with idempotency keys) — kills the dual-write problem |
| 🏷️ | **API versioning** | `#[Version("v1")]` mounts the controller under `/v1/...`; `#[Deprecated(sunset = "…")]` adds RFC 8594 `Deprecation`/`Sunset` headers so clients learn deadlines on the wire |
| 🧬 | **Unified DB layer** | `ArclyDbPool` — one injectable handle over **SQLx / SeaORM / Diesel** (Cargo features `db-sqlx-*` / `db-seaorm-*` / `db-diesel-*`); tenant routing + RW splitting apply via the existing registry; sync Diesel fully isolated behind `spawn_blocking` |
| 💾 | **`#[Transactional]`** | Begin on the tenant pool → commit on `Ok`, rollback on `Err` / `#[Timeout]` cancellation; task-local tx (no locks); `with_current_tx` reaches it from services — outbox rows ride the **same transaction** |
| 🔏 | **Distributed lock** | `DistributedLock` + `DLockBackend` — fencing tokens (cluster-monotonic), compare-and-delete release, auto-renew heartbeat; `OutboxRelay` leader election ships on it |
| 🗃️ | **DB migrations** | `MigrationRunner` — versioned `arcly_migrations` ledger per datasource, checksum **drift = boot failure**, fleet DDL serialized on the cluster lock, multi-tenant `run_all` |
| 🔁 | **Idempotency keys** | `#[Idempotent(ttl = "24h")]` — Stripe-style `Idempotency-Key`: atomic claim, stored-response replay (`Idempotency-Replayed: true`), 409 for concurrent duplicates |
| ⚖️ | **ABAC policy engine** | `#[RequirePolicies("orders.refund")]` — default-deny attribute rules (subject × resource × env) over `ArcSwap`; hot-reload a new `PolicySet` and the next request obeys it — no OPA call on the hot path, no restart |
| 📨 | **Event consumer mesh** | `#[EventConsumer]` + `#[EventPattern("topic")]` — link-time handler registry (frozen dispatch), consume-side dedupe, bounded retries → dead-letter with reason; transport (Kafka/AMQP/NATS) is one app-side trait |
| 🧵 | **Cross-channel tracing** | `traceparent` rides outbox rows and message envelopes — consumers continue the producing request's trace (`TraceContext::from_traceparent`) instead of starting orphan roots |
| 🕶️ | **PII masking** | `#[MaskFields("card:last4")]` + hot-reloadable `Masker` (`ArcSwap`) — Redact/Hash/Last4/Drop applied **before durability** at every sink: responses, the idempotency replay cache, outbox rows, and dead-lettered payloads; fields surface as `x-arcly-masked-fields` |
| 🏬 | **Dynamic tenants** | `TenantRegistry` over `ArcSwap` snapshots — `upsert`/`suspend`/`resume` live on the next request (no redeploy); suspended tenants get 401 with no silent fallback |
| ⏱️ | **Deadlines & bulkheads** | `#[Timeout("2s")]``504` + future cancellation (worker freed); `Bulkhead` (const semaphore) sheds with `503` instead of queueing when a slow dependency saturates |
|| **Validation** | `validator` integration on `#[Body]`/`#[Query]` — violations return RFC-7807 `422` with per-field errors, automatically |
|| **Resilience** | `#[circuit_breaker(threshold = 3, cooldown = "10s")]` (lock-free state machine) + `RateLimit` fixed-window limiter (single CAS) |
| 📡 | **Realtime** | WebSocket gateways with rooms & broadcast, JWT auth at handshake; SSE streams |
| 🔭 | **Observability** | W3C `traceparent` in/out, Prometheus `/metrics`, `/healthz` + `/readyz` with pluggable checks, OTLP traces, structured JSON logs |
| 📜 | **OpenAPI** | Swagger UI at `/docs` — and the spec mirrors the whole hardening stack: `Idempotency-Key` param + 409 + replay header, ABAC-aware 403s, 504 with the route deadline, RFC 8594 `Sunset` headers, `x-arcly-*` vendor extensions for gateways/SDK generators |
| 🧱 | **Plugin system** | `ArclyPlugin` lifecycle (init → start → graceful drain with timeout); plugin routes share the *exact same* request pipeline as macro routes |
| 🚀 | **Response caching** | `#[CacheTTL(30)]` + `CacheInterceptor` — keyed on path + query, hit/miss stats, one-call flush |

---

## 🏛 Architecture

```mermaid
flowchart LR
    subgraph entry ["Request Entry Points"]
        A["HTTP<br/>macro routes"]
        B["Plugin<br/>routes"]
        C["WebSocket<br/>handshake"]
    end

    subgraph pipeline ["One Shared Pipeline"]
        D["auth::extract<br/>Bearer → cookie → session"]
        E["observability::propagation<br/>W3C traceparent"]
        F["RequestContext"]
    end

    subgraph runtime ["Runtime"]
        G["Guards<br/>JWT · Role · Session · Perms"]
        H["Handler"]
        I["core: frozen DI<br/>(HTTP-agnostic)"]
    end

    A --> D
    B --> D
    C --> D
    D --> F
    E --> F
    F --> G --> H
    H -. "Inject&lt;T&gt; · O(1), no locks" .-> I
```

### Workspace layout

```
arcly-http/                  ── the framework crate
└── src/
    ├── auth/                identity & access — one place, one pipeline
    │   ├── jwt.rs           JwtService: sign / decode / validate (rotating keys)
    │   ├── cookie.rs        HMAC-SHA256 signed cookies (rotating secrets)
    │   ├── session.rs       SessionManager + SessionStore trait
    │   ├── oauth.rs         OAuth2Provider trait + registry
    │   ├── secrets.rs       SecretSource · Rotating<T> (ArcSwap) · watcher
    │   ├── policy.rs        ABAC engine: default-deny PolicySet · hot reload
    │   ├── guards.rs        JWT_AUTH · RoleGuard · SESSION_AUTH
    │   └── extract.rs       ★ the single credential-extraction pipeline
    ├── core/                DI engine + plugin lifecycle (HTTP-agnostic)
    ├── data/                DataSource · tenant registry · RW splitting · outbox
    │   ├── db.rs / tx.rs    unified ArclyDbPool + #[Transactional] runtime
    │   ├── drivers/         sqlx · seaorm · diesel (feature-gated)
    │   └── migrate.rs       versioned migrations · checksum drift guard
    ├── web/                 boundary, RequestContext, tenants (dynamic), versioning, idempotency
    ├── compliance/          PII masking: #[MaskFields] · sink-level redaction
    ├── messaging/           event consumer mesh: #[EventPattern] · retries · DLQ
    ├── realtime/            WebSocket gateways + SSE
    ├── observability/       telemetry, metrics, health, propagation, audit trail
    └── resilience/          circuit breaker · rate limiting · timeout · bulkhead · dlock

arcly-http-macros/           ── proc-macros (#[Controller], #[Module], …)
examples/
├── enterprise_app/          ── full e-commerce backend, every feature exercised
│   └── src/
│       ├── api/             one file per resource + shared guards
│       ├── auth/            AuthService, OAuth providers, permissions
│       ├── infra/           bootstrap, redis, sessions, datasources, secrets
│       └── domain.rs · services.rs
└── realtime_enterprise_app/ ── realtime-focused walkthrough
```

### Design invariants

> These are enforced by structure, not convention — each has exactly one home in the tree.

| Invariant | Why it matters |
|---|---|
| **One request pipeline** — every entry point authenticates through `auth/extract.rs` | A security fix lands everywhere at once; entry points can never drift |
| **`core` is HTTP-agnostic** — the DI engine never imports axum | Engine internals evolve without touching routing code |
| **Handlers never see axum**`RequestContext` is the only request surface | Swap or upgrade the HTTP layer without breaking user code |
| **Macros target one stable module** — codegen references `__macro_support` only | Internal files move freely; the macro crate never needs to follow |
| **No locks on the request path** — frozen maps, atomics, and `ArcSwap` only | Tenancy, rate limiting, and secret rotation scale without contention |

---

## 🚀 Quick Start

```bash
git clone git@gitlab.com:arcly/arcly-http/arcly-http.git
cd arcly-http

# Run the full-featured example — works without Redis (in-memory fallback)
cargo run -p enterprise_app
```

| Endpoint | URL |
|---|---|
| 📜 Swagger UI | `http://localhost:3000/docs` |
| ❤️ Health | `http://localhost:3000/healthz` |
| 📊 Metrics | `http://localhost:3000/metrics` |
| 📡 Live SSE feed | `curl -N http://localhost:3000/streams/inventory` |

---

## 🔐 Authentication & Authorization

The enterprise example seeds two demo users at startup:

| Email | Password | Role | Permissions |
|---|---|---|---|
| `admin@example.com` | `admin123` | admin | `users:*` `products:*` `orders:*` `admin:*` |
| `user@example.com` | `user123` | customer | `products:read` `orders:read` `orders:create` |

### Three ways in, one pipeline

```bash
# ── 1. Login: returns a JWT pair AND sets signed cookies ──────────────────────
curl -s -X POST http://localhost:3000/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"admin123"}' -c cookies.txt | jq

# ── 2a. Bearer auth (API clients) ──────────────────────────────────────────────
TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"admin123"}' | jq -r .access_token)
curl -s http://localhost:3000/auth/me -H "Authorization: Bearer $TOKEN" | jq

# ── 2b. Cookie auth (browsers) — no header, the signed cookie carries the JWT ──
curl -s http://localhost:3000/auth/me -b cookies.txt | jq

# ── 3. Tampered cookies are rejected by HMAC verification ─────────────────────
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/auth/me \
  -H "Cookie: arcly_auth=forged.signature"        # → 401
```

### Token lifecycle guarantees

```mermaid
sequenceDiagram
    participant C as Client
    participant S as Server
    participant R as Redis

    C->>S: POST /auth/login
    S->>R: SET refresh::{jti} (TTL 7d)
    S->>R: SET access2refresh::{access_jti} → refresh_jti
    S-->>C: token pair + Set-Cookie ×2

    C->>S: POST /auth/refresh
    S->>R: GETDEL refresh::{jti}  (atomic — single use)
    Note over S,R: concurrent replay of the same<br/>refresh token loses the race → 401
    S-->>C: new token pair

    C->>S: POST /auth/logout
    S->>R: GETDEL access2refresh::{access_jti}
    S->>R: DEL refresh::{refresh_jti}
    S-->>C: 204 + Max-Age=0 cookies (auth + session cleared)
```

- **Refresh tokens are single-use** — rotation uses atomic `GETDEL`, so a stolen-then-replayed token always loses the race.
- **Logout actually revokes** — the `access→refresh` JTI mapping lets a Bearer-only logout kill the paired refresh token, delete the server-side session, and expire both cookies.
- **Permissions ride the token**`PermissionGuard::require("users:read")` checks the JWT `perms` claim first (zero I/O), with a role-map fallback. Wildcards (`users:*`) are honored.
- **Login is brute-force-protected cluster-wide**`/auth/login` sits behind a `DistributedRateLimit` (10/min per principal, **fail-closed**): the sliding window counts in Redis via one atomic Lua call, so the limit holds across every replica.
- **Secrets rotate without restarts** — a `SecretSource` watcher hot-swaps JWT/cookie keys through `ArcSwap`; the previous key keeps verifying until live credentials expire naturally.

### Declarative route hardening

Every cross-cutting policy is one attribute on the handler — the macro weaves
it into the same compiled thunk, so there is no runtime middleware lookup:

```rust
#[Post("/", status(201), security("bearer"))]
#[Idempotent(ttl = "24h")]                                // safe client retries
#[MaskFields("payment.card:last4")]                       // PII never persists raw
#[RequirePolicies("orders.create")]                       // ABAC gate (hot-reloadable)
#[AuditLog(action = "order.create", resource = "order")]  // compliance record
#[Timeout("5s")]                                          // 504 + cancel on expiry
#[UseInterceptors(TraceInterceptor)]
async fn create_order(ctx: RequestContext, #[Body] dto: CreateOrderDto) -> … {
    JWT_AUTH.check(&ctx)?;
    let _permit = ORDER_BULKHEAD.try_enter()?;            // 503 when saturated
    …                                                     // outbox enqueue is
}                                                         // atomic with the write
```

```bash
# Versioned + deprecated endpoints announce their lifecycle on the wire:
curl -si http://localhost:3000/v1/meta/info | grep -iE "deprecation|sunset"
# deprecation: true
# sunset: 2027-01-01
```

And the stack is **self-documenting** — every attribute above surfaces in the
generated OpenAPI spec, no extra annotations:

```jsonc
// curl -s localhost:3000/openapi.json | jq '.paths["/orders/"].post'
{
  "parameters": [ { "name": "Idempotency-Key", "in": "header", … } ],
  "responses": {
    "409": { "description": "Conflict — a request with this Idempotency-Key is already in flight" },
    "504": { "description": "Gateway Timeout — handler exceeded its 5000ms deadline (work cancelled)" },
    "201": { "headers": { "Idempotency-Replayed": { … } } }
  },
  "x-arcly-idempotent-ttl-secs": 86400,
  "x-arcly-audit": { "action": "order.create", "resource": "order" },
  "x-arcly-timeout-ms": 5000
}
// ABAC routes get policy-aware 403s + x-arcly-policies; sunset routes get
// deprecated:true, x-sunset, and documented RFC 8594 response headers.
```

### Multi-tenancy & datasource routing

```bash
# Tenant resolved once per request (X-Tenant-Id header → frozen TenantRegistry);
# TENANT guard rejects a forged header that contradicts the JWT tenant claim.
curl -s http://localhost:3000/admin/tenant-info \
  -H "Authorization: Bearer $TOKEN" -H "X-Tenant-Id: acme" | jq
```
```json
{
  "tenant": "acme",
  "datasource": "acme",
  "read_before_write": "acme-replica-0",   // reads → replica (round-robin)
  "write":             "acme-primary",     // writes → primary (trips the pin)
  "read_after_write":  "acme-primary"      // pinned — replica-lag protection
}
```

### OAuth 2.0 (Google / GitHub)

Providers self-register only when credentials are present — no config, no route:

```bash
export GOOGLE_CLIENT_ID=…  GOOGLE_CLIENT_SECRET=…
export GITHUB_CLIENT_ID=…  GITHUB_CLIENT_SECRET=…
export OAUTH_REDIRECT_BASE=https://your-domain.example
```

```
GET /oauth/{provider}/authorize   → { "url": "https://accounts.google.com/…" }
GET /oauth/{provider}/callback    → validates CSRF state (atomic, single-use),
                                    exchanges code with PKCE, upserts the user,
                                    mints tokens through the same path as
                                    password login, sets both cookies
```

---

Tenants are **dynamically provisionable**: `POST /admin/tenants` upserts a
tenant live (effective on the next request — no redeploy), and
`/admin/tenants/:id/suspend` hard-cuts its traffic to `401` with no silent
fallback to the shared pool. The hot path stays one `ArcSwap` pointer load.

### Unified database layer & `#[Transactional]`

Pick your ecosystem with Cargo features — the injected handle and the
transaction semantics stay identical:

```toml
arcly-http = { version = "0.1", features = ["db-sqlx-postgres"] }
# or: db-seaorm-postgres · db-diesel-postgres · db-sqlx-sqlite · …
# Features are composable — enable two drivers during a migration.
```

```rust
// boot (plugin on_init): build pools, register per tenant, freeze
let primary = sqlx_driver(&db_url, 16).await?;
ctx.provide(DataSourceRegistry::new(ArclyDbPool::new("default", primary)));

// handler: one attribute = begin → commit on Ok / rollback on Err or timeout
#[Post("/", status(201), security("bearer"))]
#[Transactional]
#[AuditLog(action = "note.create", resource = "note")]
async fn create_note(ctx: RequestContext, #[Body] dto: CreateNoteDto) -> … {
    with_current_tx(|mut tx| async move {
        let r = async {
            tx.execute_bind("INSERT INTO notes (body) VALUES (?)", &[&dto.body]).await?;
            tx.execute_bind("INSERT INTO arcly_outbox (…) VALUES (…)", &[…]).await?;
            Ok(())                       // ← outbox rides the SAME transaction
        }.await;
        (tx, r)
    }).await?;
}
```

Guarantees, verified by the example's `/notes` demo:

| Path | Behaviour |
|---|---|
| Handler returns `Ok` | transaction commits — note **and** outbox row land together |
| Handler returns `Err` *after* both inserts | rollback — neither row survives |
| `#[Timeout]` expiry / client disconnect | future dropped → uncommitted tx → driver rollback |
| Sync Diesel | `#[Transactional]` rejects with guidance — use `DieselBlockingPool::transaction(…)` (whole tx in one `spawn_blocking` closure; workers never block) |
| Schema changes | `MigrationRunner` — ledgered, checksummed (drift fails the boot), serialized across the fleet on the cluster lock |

---

### Event mesh — produce transactionally, consume declaratively

```rust
// produce (inside #[Transactional] — same DB transaction as the data):
tx.execute_bind("INSERT INTO arcly_outbox (topic, payload, idempotency_key, traceparent) …").await?;

// consume (NestJS-style — registered at link time, dispatched lock-free):
#[EventConsumer]
impl NoteEventsConsumer {
    #[EventPattern("note.created")]
    async fn on_note_created(ctx: EventContext) -> Result<(), String> {
        let evt: NoteCreated = ctx.payload()?;          // typed payload
        // ctx.trace continues the producing request's trace
        ctx.inject::<SearchIndexer>().index(evt.note_id).await
    }
}
```

| Guarantee | Mechanism |
|---|---|
| No dual writes | outbox row commits with the business data |
| Fleet-safe relay | leader election on the distributed lock |
| Effectively-once consume | claim → complete-on-success / release-on-failure via the idempotency store |
| Poison isolation | bounded nack-retries, then dead-letter with the failure reason |
| Trace continuity | `traceparent` column → `EventContext.trace` (same trace ID end-to-end) |

---

### PII masking — redact before durability

The compliance machinery is durable *on purpose* (hash-chained audit,
committed outbox rows, replayable idempotency cache, DLQ) — so raw PII
reaching any of them is permanent. The `Masker` closes that at the sink:

```rust
ctx.provide(Masker::new(MaskingPolicy::new(1)
    .field("email")                      // Redact: j***@e***.com
    .field("payment.card.number:last4")  // ************4242
    .field("ssn:drop")
    .field("items.*.patient_name:hash"), // joinable, unreadable
));
```

One `ArcSwap` load + a pure tree walk per sink write — rules hot-reload at
runtime (`POST /admin/masking` in the example), and `#[MaskFields]` sits
*inside* `#[Idempotent]` so replay caches only ever hold masked bodies.

---

## 🧰 Examples

<details>
<summary><b>📦 enterprise_app</b> — full e-commerce backend (click to expand)</summary>

| Feature | Where |
|---|---|
| JWT + refresh rotation (Redis) | `auth.rs` |
| Cookie + session auth | `auth.rs`, `session_store.rs` |
| OAuth 2.0 Google/GitHub + PKCE | `oauth.rs`, `oauth_providers.rs` |
| `resource:action` permissions | `permissions.rs` |
| Module DAG (5 modules) | `main.rs` |
| Circuit-breaker on payments | `services.rs` |
| Response caching with TTL | `controllers.rs` (products) |
| WebSocket order gateway | `gateways.rs` |
| SSE inventory stream | `controllers.rs` |
| Health checks + plugin lifecycle | `plugin.rs` |
| Redis with in-memory fallback | `redis.rs` |

</details>

<details>
<summary><b>📡 realtime_enterprise_app</b> — realtime-focused walkthrough</summary>

WebSocket gateways, rooms, broadcast patterns, and SSE under one roof.

</details>

---

## ⚙️ Configuration

All settings are environment variables read at startup by the example's `AppInitPlugin`:

| Variable | Default | Purpose |
|---|---|---|
| `REDIS_URL` | `redis://127.0.0.1:6379` | Token store + sessions — **falls back to in-memory** when unreachable |
| `JWT_SECRET` | dev default | JWT HMAC signing secret |
| `COOKIE_SECRET` | dev default | Auth-cookie HMAC secret |
| `SESSION_SECRET` | dev default | Session-cookie HMAC secret |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | unset | Enables the Google OAuth provider |
| `GITHUB_CLIENT_ID` / `GITHUB_CLIENT_SECRET` | unset | Enables the GitHub OAuth provider |
| `OAUTH_REDIRECT_BASE` | `http://localhost:3000` | Base URL for OAuth callbacks |
| `ARCLY_DB_URL` | `sqlite:///tmp/arcly_demo.db?mode=rwc` | Unified DB pool for the `/notes` `#[Transactional]` demo |

Secrets (`JWT_SECRET`, `COOKIE_SECRET`, `SESSION_SECRET`) are watched at runtime: change the value at the source and the corresponding service hot-rotates its keys within 60 s — no restart, no mass logout. Tenants are selected per request via the `X-Tenant-Id` header (`acme` / `globex`, fallback `public`).

> ⚠️ **Production:** set real secrets, enable `secure: true` on cookies (TLS), and point `REDIS_URL` at a persistent instance — the in-memory fallback loses sessions and token state on restart.

---

## 🛠 Development

```bash
cargo build --workspace                   # build everything
cargo test  -p arcly-http                 # unit + doc tests
cargo test  -p arcly-http --test smoke    # integration smoke suite
cargo run   -p enterprise_app             # full example server
```

---

## 📄 License

MIT