rustango 0.60.7

Batteries-included web framework for Rust: ORM + migrations + auto-admin + multi-tenancy + audit log + auth (sessions, JWT, OAuth2/OIDC, HMAC) + APIs (ViewSet, OpenAPI auto-derive, JSON:API) + jobs (in-mem + Postgres) + email + media (S3 / R2 / B2 / MinIO + presigned uploads + collections + tags) + production middleware (CSRF, CSP, rate-limiting, compression, idempotency, etc.).
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
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
[package]
name = "rustango"
description = "Batteries-included web framework for Rust: ORM + migrations + auto-admin + multi-tenancy + audit log + auth (sessions, JWT, OAuth2/OIDC, HMAC) + APIs (ViewSet, OpenAPI auto-derive, JSON:API) + jobs (in-mem + Postgres) + email + media (S3 / R2 / B2 / MinIO + presigned uploads + collections + tags) + production middleware (CSRF, CSP, rate-limiting, compression, idempotency, etc.)."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
keywords.workspace = true
categories.workspace = true
# Drop runnable demos + integration-test files from the published
# tarball. `src/admin/templates/` and `src/tenancy/{templates,static}/`
# stay — they're baked into the binary via include_str! / include_bytes!.
exclude = ["examples/", "tests/"]

# docs.rs builds with **default features only** unless told otherwise, and
# several headline modules are not in `default`: `tenancy`, `sso`, `mcp`,
# `passkey`, `admin-sso`, `cache-redis`. Without this section every one of them
# 404s on docs.rs — so multi-tenancy, the framework's flagship differentiator,
# had no published API documentation at all, and a reader arriving from the
# README would reasonably conclude the feature does not exist.
#
# `all-features` rather than an explicit list because the three backends
# coexist by design (the tri-dialect work makes `postgres,mysql,sqlite`
# together a supported, CI-tested combination), so there is nothing to exclude.
#
# `--cfg docsrs` enables `#[cfg_attr(docsrs, doc(cfg(...)))]` badges, so each
# gated item shows which feature unlocks it instead of appearing unconditional.
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[lints]
workspace = true

[features]
# Out of the box: ORM + auto-admin + Postgres + layered config +
# public forms framework. Opt in to multi-tenancy with
# `features = ["tenancy"]`. Drop `default-features` for the bare
# ORM (core + query + sql + migrate).
default = ["postgres", "batteries"]

# Everything `default` gives you **except** the database backend (#1211).
#
# A downstream project cannot use `default` and still choose its backend:
# `#[derive(Model)]` gates its `FromRow` / `LoadRelated` emissions on the
# *consumer's* features (a cfg inside a derive resolves against the destination
# crate), so if the dependency pins `rustango/postgres` while the project's own
# `postgres` feature is off, rustango demands impls the derive never emitted.
# Naming `batteries` instead lets the project forward the backend through its
# own feature:
#
#     rustango = { version = "…", default-features = false, features = ["batteries"] }
#
#     [features]
#     default  = ["postgres"]
#     postgres = ["rustango/postgres"]
#     sqlite   = ["rustango/sqlite"]
#
# `cargo rustango new` emits exactly that. Keeping the list here rather than in
# the generated manifest means it can't rot out of sync with `default`.
batteries = ["manage", "admin", "config", "forms", "serializer", "cache", "signals", "email", "storage", "scheduler", "secrets", "totp", "webhook", "webhook-delivery", "api_keys", "passwords", "signed_url", "notifications", "casts", "jobs", "jobs-postgres", "auth_flows", "sse", "websocket", "oauth2", "http-client", "compression", "openapi", "csp-nonce", "sessions", "hmac-auth", "jwt", "uploads", "storage-s3", "storage-azure", "media", "runserver", "template_views"]
# `tls-rustls` (→ rustls-ring + webpki roots, no openssl — matching the
# reqwest / lettre stance) is folded in because a NETWORK database backend
# is useless without it: every managed Postgres (Supabase, Neon, RDS,
# Cloud SQL, …) mandates TLS, and sqlx built with no TLS backend cannot
# honour `sslmode=require` — the handshake just fails. SQLite deliberately
# stays TLS-free (local file, no socket).
postgres = ["sqlx/postgres", "sqlx/tls-rustls"]

# Unified `cargo run` / `cargo run -- <verb>` dispatcher. Pulls the
# minimum axum + tokio surface so bare-API projects (without admin)
# can still use `rustango::manage::Cli::new().api(...).run().await`.
# v0.16+. Default-on so `cargo rustango new` projects work with no
# extra opt-ins.
manage = ["_axum", "_http_layers", "runtime"]

# Test-only constructors for downstream live tests. Currently
# unlocks `Tenant::for_test(org, conn)` so tenancy-aware crates
# (rustango-cms, etc.) can build a Tenant without booting a full
# axum server with TenantContext. v0.30.26+.
test_utils = []

# Dev-only test-support helpers (`crate::testkit`): build framework
# tables from `Model::SCHEMA` (no hand-written DDL) + model factories
# for struct-update construction. Integration tests are external
# crates, so the module must be public API behind this feature; it
# pulls in nothing and costs a normal build nothing.
testkit = []

# MySQL 8.0+ backend. Fully tri-dialect since v0.38: every framework
# surface (ORM, migrations, admin, tenancy database-mode, jobs queue,
# audit, media, contenttypes, permissions, inspectdb, ViewSet,
# Serializer) works against MySQL through the same `&Pool` API as
# Postgres. Schema-mode tenancy remains Postgres-only by language
# semantics (`SET search_path` has no MySQL equivalent).
mysql = ["sqlx/mysql", "sqlx/tls-rustls"]

# SQLite 3.35+ backend. Fully tri-dialect since v0.38: every framework
# surface works against a file-backed or in-memory SQLite DB through
# the same `&Pool` API as Postgres. v0.40 turns on `foreign_keys`,
# `journal_mode = WAL` (file-backed), and `busy_timeout = 5000` on
# every connection via `sql::pool::sqlite_connect_options` — apps
# don't have to remember the SQLite-default-off FK pragma.
sqlite = ["sqlx/sqlite"]

# ---------------------------------------------------------------------
# Internal capability features (#1208). Not for downstream use — enable a
# product feature instead; these carry a leading underscore to say so.
#
# The problem they solve: several modules are declared `pub mod` with no
# `cfg` at all, but their bodies use an *optional* dependency. That only
# builds when some unrelated product feature happens to have pulled the
# dep in, so `--no-default-features --features postgres,manage` (and most
# other minimal sets) failed with dozens of "unlinked crate" errors deep
# inside the framework. `cfg(feature = "...")` cannot ask "is dep:rand
# enabled?", so each optional dep that a module needs gets a capability
# feature, every product feature enables the capability rather than the
# dep, and the module gates on the capability.
#
# Adding an optional dep to a module? Give it a capability here, route the
# product features through it, and gate the module — do not leave the
# module unconditional.
_rand         = ["dep:rand"]
_tera         = ["dep:tera"]
_tower        = ["dep:tower"]
_axum         = ["dep:axum"]
_async_trait  = ["dep:async-trait"]
_base64       = ["dep:base64"]
# The three that always travel together for cookie/token signing.
_signing      = ["_base64", "dep:hmac", "dep:sha2"]
# The request middleware (`cors`, `body_limit`, `security_headers`, …): `manage`
# mounts them from `[server]`/`[security]` settings without `admin` (#1739),
# and the request span, request id and access log with them (#1514), and
# `static_files` for `Cli::with_static` (#2042), streamed off the runtime (#1531).
_http_layers  = [
    "_axum", "_tower", "_base64", "_rand", "dep:http-body", "dep:http-body-util",
    "dep:tokio-util", "tokio/fs", "tokio/io-util",
]

# Layered TOML config (`rustango::config`). Loads `config/default.toml`
# → `config/{env}.toml` → env-var overrides. Slice 8.3.
# `signals`: `test_settings` fires `setting_changed` (#1739).
config = ["dep:toml", "dep:serde_path_to_error", "signals"]

# Public forms framework (`rustango::forms`) — value parsing for HTML
# form payloads, shared between admin and user route handlers. Slice
# 8.4A ships the parsers; slice 8.4B added `#[derive(Form)]`. Slice
# 8.4C lights up CSRF protection behind the `csrf` sub-feature so
# parser-only users don't pay the cookie/rand cost.
forms = []

# Serializer layer (`rustango::serializer`) — `#[derive(Serializer)]` +
# `ModelSerializer` trait. Produces typed JSON output from model instances
# with field control (read_only, write_only, source, skip) and cross-field
# validation. Implies `forms` so serializer errors share `FormErrors`.
serializer = ["forms"]

# Caching layer (`rustango::cache`) — pluggable `Cache` trait with
# `NullCache` (no-op), `InMemoryCache` (tokio RwLock + TTL) and
# `FileCache` (on disk; `fs4`/`libc` for its lock files). Redis
# backend is behind the separate `cache-redis` feature.
cache = ["_async_trait", "dep:sha2", "dep:fs4", "dep:libc"]

# Per-view caching tower layer (`rustango::cache_page`) — caches a
# whole successful response, plus `Cache-Control` / `Vary` builders.
# Implies `cache` (cache backend) + `admin` (axum types) + base64 for
# body round-trip serialization.
cache-page = ["cache", "admin", "dep:base64"]

# Signals layer (`rustango::signals`) — pre_save / post_save
# / pre_delete / post_delete async dispatch. Receivers register globally
# per model type; senders fire signals after write paths.
#
# Empty list, not dead config: the feature still gates `rustango::signals` and
# the emission sites in `migrate::runner` / `sql::m2m`. It carried `dep:tokio`
# until tokio became unconditional.
signals = []

# Email backends (`rustango::email`) — Mailer trait + ConsoleMailer +
# InMemoryMailer + NullMailer. SMTP and external transports plug in via
# the trait.
email = ["_async_trait"]

# Production SMTP backend (`rustango::email::SmtpMailer`, issue #48).
# Implies `email`. Pulls `lettre` with rustls TLS (no openssl).
# Default builds don't include this — opt in when you ship to prod and
# need a real relay (Postmark, SendGrid, AWS SES via SMTP, etc.).
email-smtp = ["email", "dep:lettre"]

# Multi-channel notifications layer (`rustango::notifications`) — Laravel-shape
# fan-out across mail / database / log / broadcast. Implies email + cache.
notifications = ["email", "cache"]

# Background job queue (`rustango::jobs`) — Job trait, in-memory queue,
# worker pool, retry with exponential backoff, dead-letter callback.
jobs = ["_async_trait"]

# Database-backed job queue (`rustango::jobs::pg::PgJobQueue`).
# v0.38 — tri-dialect: PostgreSQL + MySQL 8.0+ use `FOR UPDATE SKIP
# LOCKED` for safe multi-process pickup; SQLite uses a transaction
# around `UPDATE … RETURNING` (SQLite serializes writers globally so
# the pickup is implicitly mutually-exclusive). The feature name is
# `jobs-postgres` for back-compat with v0.37 — the queue itself is
# no longer PG-only. The struct is still `PgJobQueue` for the same
# back-compat reason; new code can use `PgJobQueue::new_pool(pool)`.
jobs-postgres = ["jobs"]

# Pre-built auth flows (`rustango::auth_flows`) — password reset, email
# verification, magic-link login. Composes signed_url + email helpers.
auth_flows = ["signed_url"]

# Broadcast event bus (`rustango::sse`) — fan-out for SSE / WebSocket /
# signal-driven push. tokio::sync::broadcast under the hood.
#
# Empty list, not dead config — still gates `rustango::sse`. Carried
# `dep:tokio` until tokio became unconditional.
sse = []

# WebSocket handler scaffold (`rustango::ws`) — fan-out via the SSE
# EventBus, axum upgrade handler with auto JSON encode/decode + ping/pong
# keep-alive. Implies `admin` (axum) + `sse` (broadcast bus). Adds
# tokio-tungstenite via the `axum/ws` feature.
websocket = ["admin", "sse", "axum/ws"]

# Model Context Protocol server (`rustango::mcp`) — expose tools / prompts
# / resources to external ML agents over JSON-RPC 2.0 + Streamable HTTP.
# Tenant-scoped agent identities + scoped-JWT auth (Slice 2+). `tenancy`
# brings the agent/skill models + per-request pool; `sse` the server→client
# notification bus; `serializer`/`openapi` the tool inputSchema; `jwt`
# exposes `crate::signing` (agent-bound pagination cursors) and reflects that
# MCP agent tokens are JWTs. Epic #1013.
mcp = ["tenancy", "sse", "serializer", "openapi", "jwt", "_axum", "dep:async-stream"]

# OAuth2 / OIDC swiss-knife (`rustango::oauth2`) — works for pure OAuth2
# providers (GitHub, Discord) AND OIDC providers (Google, Microsoft,
# Apple, Keycloak) via the `/userinfo` endpoint as identity source.
# Per-tenant via `OAuth2Registry`. Pulls reqwest (rustls — no openssl).
oauth2 = ["_signing", "dep:reqwest", "dep:urlencoding", "_rand", "_async_trait"]

# Opinionated HTTP client (`rustango::http_client`) — reqwest wrapper
# with sane timeouts, exponential-backoff retry on idempotent verbs +
# 5xx/timeout/connect failures, and a default User-Agent. Standalone
# wrapper that any rustango app can use directly.
http-client = ["dep:reqwest"]

# Response compression middleware (`rustango::compression`) — gzip +
# deflate via flate2 (miniz_oxide by default, no C deps). Honors the
# client's `Accept-Encoding`, skips already-compressed and binary
# content-types. Implies `admin` (axum-only).
compression = ["admin", "dep:flate2"]

# Per-request CSP nonce middleware (`rustango::csp_nonce`) — generates
# a fresh CSPRNG nonce per request, stuffs it into request extensions
# for handlers to consume, and substitutes it into the
# Content-Security-Policy header so inline `<script nonce="…">` blocks
# pass strict CSP. Implies `admin` (axum-only) + rand for the nonce.
csp-nonce = ["admin", "_rand"]

# Server-side sessions (`rustango::sessions`) — opaque cookie ID
# backed by any `Cache` implementation. Implies `cache` + rand for
# the ID + base64 for encoding it.
sessions = ["cache", "_rand", "dep:base64"]

# HMAC-signed request authentication (`rustango::hmac_auth`) for
# service-to-service traffic. AWS-style canonical request signed with
# SHA-256, replay-bounded by an X-Date tolerance window. Implies
# `admin` (axum) + the existing webhook crypto deps.
hmac-auth = ["_signing", "admin"]

# Standalone HS256 JWT (`rustango::jwt`) — sign / verify / decode for
# magic links, microservice tokens, third-party SSO. Standalone
# alternative to `tenancy::jwt_lifecycle` (which adds refresh +
# blacklist + sliding rotation but is gated on tenancy; since v0.48
# its blacklist is also pluggable via `Arc<dyn rustango::jti_store::JtiStore>`
# for multi-instance deployments).
jwt = ["_signing"]

# Multipart file upload helper (`rustango::uploads`) — wraps
# axum's multipart extractor + the `storage::Storage` trait so file
# uploads are a one-call API with size + extension guards. Pulls
# `multer` transitively via `axum/multipart`.
uploads = ["admin", "storage", "axum/multipart"]

# First-class media model (`rustango::media`) — a Media row that
# references a file in the Storage layer. Handles direct browser
# upload (via S3 presigning), CDN-aware URLs, soft delete + orphan
# purge sweep. v0.38 — tri-dialect: every query dispatches per
# backend (PG / MySQL 8+ / SQLite), so `media` now only requires
# `storage` + at least one database backend feature
# (postgres / mysql / sqlite).
# `MediaAuthorizer`, the policy hook the media router requires since
# 0.57.7, needs `_async_trait` — which arrives via `storage`, so it is
# not repeated here. The router itself is behind `admin`.
media = ["storage"]

# S3-compatible storage backend (`rustango::storage::s3::S3Storage`).
# Hand-rolled SigV4 over reqwest — works for AWS S3, Cloudflare R2,
# Backblaze B2, MinIO, and any other S3-compatible API. No
# `aws-sdk-s3` dependency.
storage-s3 = ["storage", "dep:reqwest", "dep:hmac", "dep:sha2"]

# Azure Blob Storage backend (`rustango::storage::azure::AzureBlobStorage`).
# Shared Key for server calls, service SAS for presigned URLs; hand-rolled
# over reqwest like `storage-s3`. No Azure SDK dependency.
storage-azure = ["storage", "dep:reqwest", "dep:hmac", "dep:sha2", "dep:base64"]

# OpenAPI 3.1 spec builder + Swagger UI router (`rustango::openapi`).
# Pure-Rust types you assemble into a spec, serialized to JSON via serde.
# Optional axum mount at `/openapi.json` + `/docs` (Swagger UI from a
# CDN). Forwards through to the proc-macro so `#[derive(Serializer)]`
# also emits an `OpenApiSchema` impl.
openapi = ["rustango-macros/openapi"]

# File storage backends (`rustango::storage`) — Storage trait + LocalStorage
# (filesystem) + InMemoryStorage (tests). S3/GCS/Azure plug in via the trait.
storage = ["_async_trait"]

# In-process scheduled task runner (`rustango::scheduler`) — fire async jobs
# at fixed intervals with panic isolation per task.
#
# Empty list, not dead config — still gates `rustango::scheduler`. Carried
# `dep:tokio` until tokio became unconditional.
scheduler = []

# Secrets manager (`rustango::secrets`) — Secrets trait + EnvSecrets +
# InMemorySecrets. Vault / AWS / GCP plug in via the trait.
secrets = ["_async_trait"]
# Attribute casts (#819) — `Cast<C>` field wrapper + `CastValue` trait;
# the `EncryptedString` built-in pulls XChaCha20-Poly1305 + base64 + a
# SHA-256 key derivation (`rand` for the per-write nonce).
casts = ["dep:chacha20poly1305", "dep:base64", "_rand", "dep:sha2"]

# TOTP / 2FA (`rustango::totp`) — RFC 6238 time-based one-time passwords
# for second-factor authentication. SHA-1 (Google Authenticator default).
# `sha2` because it enables `crypto`, which imports it (#1678).
totp = ["dep:hmac", "dep:sha1", "dep:sha2", "_rand"]
# Passkey / WebAuthn (#392). Foundation slice = credential storage only
# (no extra deps). The ceremony layer adds pure-Rust CBOR (`ciborium`) +
# ES256 (`p256`) when it lands — keeping the all-RustCrypto, no-C-dep
# stack (vs `webauthn-rs` → openssl).
passkey = ["_rand", "cache", "dep:base64", "dep:hmac", "dep:sha2", "dep:ciborium", "dep:p256"]

# Webhook signature verification (`rustango::webhook`) — HMAC-SHA256
# constant-time verification for inbound webhooks (Stripe, GitHub, etc.).
webhook = ["_signing"]

# Outbound webhook delivery (`rustango::webhook_delivery`) — POSTs an
# HMAC-signed JSON payload to a subscriber URL via the background job
# queue (so retries with exponential backoff are free). Implies
# `webhook` (signing format) + `jobs` (retry machinery) + reqwest.
webhook-delivery = ["webhook", "jobs", "dep:reqwest"]

# API keys (`rustango::api_keys`) — standalone generate/hash/verify
# helpers for `{prefix}.{secret}` keys; hashing is `passwords`' (#1729).
api_keys = ["passwords", "_rand"]

# Generic password helpers (`rustango::passwords`) — argon2id hash/verify
# + minimal strength check. Standalone alternative to tenancy::password.
# `getrandom` for the salt's `OsRng`; other features only got it through `rand` (#1739).
passwords = ["dep:argon2", "dep:password-hash", "password-hash/getrandom"]

# Signed URL helpers (`rustango::signed_url`) — HMAC-SHA256 signed URLs
# with optional expiry. For magic-link login, password resets, etc.
signed_url = ["_signing"]

# Redis cache backend (`rustango::cache::RedisCache`). Adds the `redis`
# async connection-manager client. Implies `cache`.
cache-redis = ["cache", "dep:redis"]

# CSRF protection (`rustango::forms::csrf`) — double-submit-cookie
# axum middleware. Lives behind its own flag so `forms`-only users
# (parsers without CSRF) don't pull cookie + rand + tower
# transitively.
csrf = ["forms", "_axum", "dep:base64", "dep:cookie", "dep:hmac", "dep:sha2", "_rand", "_tower"]

# Generic class-based views for HTML templates (`rustango::template_views`)
# — `ListView` / `DetailView` / `CreateView` / `UpdateView` /
# `DeleteView` over `#[derive(Model)]` types, rendered through Tera.
# The JSON-API counterpart is `rustango::viewset` (always on). Every POST
# route checks the CSRF token, so `csrf` comes with it.
template_views = ["forms", "csrf", "_axum", "_tera", "dep:serde_urlencoded"]

# Auto-admin HTTP layer (`rustango::admin`). Pulls in axum + Tera +
# the supporting middleware deps. Implies `forms` + `csrf` so the
# admin's CRUD handlers reuse the shared parsers and ship CSRF on
# every mutation by default.
#
# v0.37 — admin is **fully tri-dialect**: every fetch goes through
# `select_*_as_json_pool`, every audit + permissions path is on
# `_pool` companions, and SQL is rendered via the dialect's
# `quote_ident` + `placeholder` emitters. The admin feature no
# longer implies `postgres`; pick whichever of `postgres` / `mysql`
# / `sqlite` your project needs and the admin works against it.
admin = [
    "forms",
    "csrf",
    # #253 — admin session auth uses argon2 via crate::passwords for
    # the AdminUser password hash. `passwords` was already a
    # transitive dep through forms/tenancy in practice; this makes
    # the relationship explicit so `--features sqlite,admin` builds
    # cleanly without `tenancy` or `passwords` named.
    "passwords",
    # #414 — login / logout handlers fire the auth lifecycle signals
    # (`user_logged_in` / `user_logged_out` / `user_login_failed`).
    # Tokio was already in the admin closure; signals adds only the
    # in-process registry helpers.
    "signals",
    # #1609 — every login's account lock lives in `account_lockout`,
    # so no admin/tenancy build can compile a login without it.
    "cache",
    "_axum",
    "_http_layers",
    "_tera",
    "_signing",
    # `manage create-admin` (slice B) prompts for a password
    # interactively when `--password` isn't supplied. `rpassword` was
    # tenancy-only before #253; admin now picks it up too.
    "dep:rpassword",
    "dep:http",
    "dep:http-body",
    "dep:http-body-util",
    "dep:serde_urlencoded",
]

# SSO for the admin login (`rustango::admin::sso`) — sign in with an
# external IdP (OpenID Connect, Google, Microsoft/Azure AD, GitHub, …)
# instead of a local password. Composes the existing `oauth2` client
# (the OIDC/OAuth2 handshake) with `admin` (the session + user model).
# Access is *link-to-existing* — the verified IdP email must match an
# admin user; SSO never auto-provisions. Works for the bare admin
# (global `[sso]` config) and, with `tenancy`, per-Org.
#
# Composes the admin-independent `sso` core (below) with `admin` (the
# session + user model). `admin-sso` = `admin` + `sso`; `sso` transitively
# brings `oauth2` + `casts`.
admin-sso = ["admin", "sso"]

# SSO core (`rustango::sso`) — the admin-INDEPENDENT OpenID Connect /
# social-OAuth handshake plumbing (`ResolvedSso`, `build_provider`,
# `verified_email`, …) plus the DB-backed `SsoProvider` model
# (`rustango_sso_providers`). No `admin` dependency, so member / end-user
# SSO (`tenancy::member_auth`) can build without pulling in the auto-admin.
# Composes the existing `oauth2` client with `casts` (encrypted-at-rest
# client secret). `admin-sso` layers the admin login wiring on top.
sso = ["oauth2", "casts"]

# Multi-tenancy module (`rustango::tenancy`). Implies `admin` (the
# tenant admin builder wraps `rustango::admin`). Pulls in Argon2 +
# HMAC-SHA256 + cookie + tower for the per-tenant + operator
# authentication paths. Implies `storage` + `uploads` so the operator
# console can persist per-tenant brand assets (logo / favicon) on
# disk and serve them via the tenancy router.
tenancy = [
    # v0.38 — `tenancy` no longer implies `postgres`. Database-per-tenant
    # mode (`DatabasePools<DB>`, since v0.33) works on Postgres, MySQL,
    # and SQLite alike. Schema-mode tenancy is still Postgres-only by
    # language semantics (uses `SET search_path`); the framework's
    # tenancy/migrate + tenancy/pools code paths keep their
    # `#[cfg(feature = "postgres")]` gates around schema-mode-specific
    # logic, and the runtime returns a clear error when a non-PG
    # registry is asked to serve a schema-mode tenant.
    #
    # Picking a backend: add `--features sqlite,tenancy,admin`,
    # `--features mysql,tenancy`, or `--features postgres,tenancy,admin`
    # depending on the target deployment. Apps that *also* want
    # schema-mode tenants get `postgres` automatically by including it.
    "runtime",
    # #414 — tenant admin + operator console login_submit / logout
    # fire the auth lifecycle signals.
    "signals",
    "storage",
    "uploads",
    "_async_trait",
    # #1322 — the operator console streams a provisioning run as SSE, and
    # the handler needs a `Stream` to hand axum. `async-stream` was
    # already an optional dep pulled by `mcp` for the same reason; the
    # console ships with tenancy, so tenancy is where it belongs now
    # rather than being reachable only from an unrelated feature.
    "dep:async-stream",
    "dep:argon2",
    "dep:cookie",
    # Deliberately the exact pair tenancy carried before #1208, not `_signing`
    # — that capability also pulls `base64`, which tenancy never depended on.
    # The capability features exist to stop the feature table lying about what
    # it pulls in; widening one while introducing them would defeat that.
    "dep:hmac",
    "dep:sha2",
    "dep:password-hash",
    "_rand",
    "dep:rpassword",
    "dep:serde_urlencoded",
    "_tower",
]

# `#[rustango::main]` runserver shim (slice 9.0 / 0.8.2). Pulls in
# `tracing-subscriber` so the macro can install the default
# `info,sqlx=warn` env-filter logger before the user body runs. Apps
# that don't want this dep can opt out by skipping the macro and
# using `#[tokio::main]` directly.
#
# v0.31.1 (#4): the macro emits
# `#[::rustango::__private_runtime::tokio::main]`, so user crates don't need
# tokio in their own Cargo.toml just to use the rustango entrypoint shim.
# That re-export no longer needs a feature to carry it — tokio is an
# unconditional dependency (see the note on the `tokio` line below).
runtime = ["dep:tracing-subscriber", "dep:tracing-appender", "dep:dotenvy"]

# Minimal axum runserver — `rustango::server::AppBuilder`. Bi-dialect
# single-pool bootstrap (SQLite / Postgres / MySQL). Pulls axum; tokio is
# unconditional. The full multi-tenant `Builder` lives behind `tenancy` and
# pulls in much more.
runserver = ["_axum"]

[dependencies]
# Proc-macros — separate by Rust language requirement.
rustango-macros = { version = "0.60.7", path = "../rustango-macros" }

# Always-on: core ORM + query layer + SQL writer + migration runner.
chrono       = { workspace = true }
indexmap     = { workspace = true }
inventory    = { workspace = true }
rust_decimal = { workspace = true }
serde        = { workspace = true }
serde_json   = { workspace = true }
sqlx         = { workspace = true }
thiserror    = { workspace = true }
tracing      = { workspace = true }
unicode-normalization = { workspace = true }
uuid         = { workspace = true }

# `config` feature dep — optional.
toml = { workspace = true, optional = true }
serde_path_to_error = { workspace = true, optional = true }

# `mcp` feature dep — optional. SSE stream macro for the server→client
# notification channel (same crate the `sse` module's docs point at).
async-stream = { workspace = true, optional = true }

# `admin` feature deps — optional.
axum           = { workspace = true, optional = true }
base64         = { workspace = true, optional = true }
ciborium       = { workspace = true, optional = true }
p256           = { workspace = true, optional = true }
http           = { workspace = true, optional = true }
http-body      = { workspace = true, optional = true }
http-body-util = { workspace = true, optional = true }
tera           = { workspace = true, optional = true }

# `tenancy` feature deps — optional.
argon2           = { workspace = true, optional = true }
async-trait      = { workspace = true, optional = true }
chacha20poly1305 = { workspace = true, optional = true }
cookie           = { workspace = true, optional = true }
fs4              = { workspace = true, optional = true }
hmac             = { workspace = true, optional = true }
password-hash    = { workspace = true, optional = true }
rand             = { workspace = true, optional = true }
rpassword        = { workspace = true, optional = true }
serde_urlencoded = { workspace = true, optional = true }
sha1             = { workspace = true, optional = true }
sha2             = { workspace = true, optional = true }
# Non-optional as of #1535: `crypto::constant_time_compare` is
# ungated and every credential comparison in the crate routes
# through it, so the constant-time guarantee cannot be a feature
# a build might leave off. `subtle` is no_std with no dependencies.
subtle           = { workspace = true }
# Non-optional since #1208 follow-up. `sqlx` is a hard dependency built with
# `runtime-tokio`, so tokio is already compiled into every build including
# `--no-default-features --features sqlite` — `optional = true` only controlled
# whether rustango was allowed to *name* the crate it was already linking.
# Meanwhile the modules that need it are core, not opt-in: the transaction
# on-commit registry (`sql::executor::atomic`), the audit source task-local,
# and the event bus are all built on `tokio::task_local!` / `tokio::sync`.
# Gating those would have silently removed transaction hooks from a bare-ORM
# build; making the dependency honest costs no extra crate.
tokio            = { workspace = true, features = ["time"] }
# Already linked through sqlx; `check --deploy` walks tenants with `buffer_unordered`.
futures-util     = { version = "0.3", default-features = false, features = ["alloc"] }
tower            = { workspace = true, optional = true }
tokio-util       = { version = "0.7", features = ["io"], optional = true }
redis            = { workspace = true, optional = true }

# `oauth2` feature deps — optional.
reqwest     = { workspace = true, optional = true }
urlencoding = { workspace = true, optional = true }

# `compression` feature dep — optional.
flate2 = { workspace = true, optional = true }

# `email-smtp` feature dep — optional. rustls TLS only (no openssl).
# `tokio1` enables the async transport; `builder` re-exports the
# Message builder we use for MIME assembly.
lettre = { version = "0.11", default-features = false, features = [
    "tokio1-rustls-tls",
    "smtp-transport",
    "builder",
], optional = true }

# `runtime` feature dep — used by `#[rustango::main]` to install a
# default tracing subscriber (env-filter) before the user body runs.
# `ansi` is what makes colour possible at all. Without it
# `tracing-subscriber` compiles the escape-code path out entirely — not
# "colour off by default", *absent* — so no rustango line has ever been
# coloured, `docs/logging.md` advertised colour that could not exist, and
# `with_ansi(true)` panics in debug builds (#1480). It pulls
# `nu-ansi-term`, which was correspondingly missing from `Cargo.lock`.
# Whether colour is *used* is decided at runtime by tty detection and the
# `[logging] color` setting; this only makes the choice available.
tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "env-filter", "json", "ansi"], optional = true }
tracing-appender = { workspace = true, optional = true }
# `#[rustango::main]` loads `.env` before installing its subscriber (#2204).
dotenvy = { workspace = true, optional = true }
# Not used directly — declared only to pin the transitive `time` away from
# the broken 0.3.48 (see the workspace.dependencies note). `time` is
# already pulled by cookie / tracing-appender / sqlx in every real build.
time = { workspace = true }

[target.'cfg(unix)'.dependencies]
libc = { workspace = true, optional = true }

[dev-dependencies]
# Self dev-dependency, for `testkit` alone. Integration tests are
# external crates, so `crate::testkit` is only public API with the
# feature on — and 36 test files used it without asking for it, so
# every selective-feature test build failed on a missing module.
# `default-features = false` matters: without it this would quietly
# re-enable postgres and make `--no-default-features` meaningless.
rustango       = { path = ".", default-features = false, features = ["testkit"] }
# `test-util` pauses the clock for timeout tests.
tokio          = { workspace = true, features = ["test-util"] }
chrono         = { workspace = true }
uuid           = { workspace = true }
serde_json     = { workspace = true }
rust_decimal   = { workspace = true }
tower          = { workspace = true }
http-body-util = { workspace = true }
axum           = { workspace = true }
base64         = { workspace = true }
dotenvy        = { workspace = true }
# Tests build form bodies with it; the lib only pulls it in via `oauth2`.
urlencoding    = { workspace = true }
tempfile       = "3"

# All example crates removed. Single-file examples are auto-detected
# by Cargo; multi-file or required-features examples are declared
# here.

# Multi-tenant blog demo (operator console + tenant admin + viewsets).
# Uses the PG-only `Tenant` extractor / `viewset` / `server::ApiRouter`
# stack — requires postgres feature alongside tenancy. Auto-discovered
# by Cargo because it lives under `examples/blog_demo/` with a
# `main.rs`; gated so sqlite/mysql builds skip it cleanly.
[[example]]
name              = "blog_demo"
path              = "examples/blog_demo/main.rs"
required-features = ["tenancy", "postgres"]

# SQLite ORM demo — single-file example, gated on the `sqlite` feature
# so default `cargo build --examples` doesn't try to compile it.
# Also requires postgres because it derives Model unconditionally
# (macro emits PG paths).
[[example]]
name              = "sqlite_orm_demo"
path              = "examples/sqlite_orm_demo.rs"
required-features = ["sqlite", "postgres"]

# AppBuilder + SQLite — minimal runnable bootstrap shape.
[[example]]
name              = "sqlite_app_demo"
path              = "examples/sqlite_app_demo.rs"
required-features = ["sqlite", "runserver"]

# Epic #246 demo — full GenericForeignKey surface: generic_fk
# accessor/setter codegen, list-view target links, register_admin_inline_generic!
# panels (read-only + editable), CT <select> picker. SQLite-backed,
# no tenancy. Run with `cargo run -p rustango --example gfk_demo
# --features sqlite,admin,runserver`.
[[example]]
name              = "gfk_demo"
path              = "examples/gfk_demo/main.rs"
required-features = ["sqlite", "admin", "runserver"]

# Multi-tenant SQLite demo (v0.33) — wires the DatabaseTenant<Sqlite>
# extractor for per-tenant-file SQLite databases. Pairs with the
# `database_pools_sqlite_*_live.rs` tests; this is the manual-running
# shape downstream apps consume until the Cli builder lands.
# Also requires postgres for the operator_console types it imports.
[[example]]
name              = "sqlite_tenant_demo"
path              = "examples/sqlite_tenant_demo.rs"
required-features = ["sqlite", "tenancy", "runtime", "postgres"]