1.0.0 - 2026-07-25
1.0.0 is the first stable Loco release β a single, intentionally-breaking
milestone. Its headline is the move to Sea-ORM 2.0, alongside first-class
LLM/agent support, priority queues, a broad dependency modernization, and a deep
hardening pass across the queue, storage, config, error, remote-IP, and
middleware subsystems. Follow the step-by-step
0.16 β 1.0 upgrade guide.
Breaking Changes
- Sea-ORM 2.0 + sqlx 0.9. Bump
sea-orm/sea-orm-migrationto2.0
(app +migrationcrate), directsqlxto0.9, update the Sea-ORM CLI, and
regenerate entities. Raw-Statementcalls gain a_rawsuffix; runtime SQL
strings needAssertSqlSafe. MSRV is 1.94 (sea-orm 2.0.0 declares it).
(Adopted from the SeaQL fork and
#1698.) - Generated primary/foreign keys are now 64-bit (BIGINT /
i64). Also the
int/unsignedfield types generate 64-bit columns. Only affects newly
generated code; existing tables are untouched. - Priority queues β Redis backend change. The Redis worker moved from Lists
to Sorted Sets (ZSET) to support priority; drain existing Redis queues before
upgrading. Postgres/SQLite auto-migrate aprioritycolumn (no action).
(#1693) Worker::perform_later()returns the job ID (Result<String>), and
Queue::enqueue()returnsResult<Option<String>>. Existing
perform_later(...).await?;keeps working. (#1624, fixes #1623)PageResponse<T>exposesmeta: PagerMetainstead of flat
total_pages/total_items(also carriespage/page_size). (#1685, fixes #1683)- View engine: use
engines::TeraView::build_with_post_process(...)instead
ofTeraView::build()?.post_process(...)inafter_routes. - Dependency majors:
thiserror1β2,tower0.4β0.5,heckβ0.5,
byte-unit4β5,ipnetwork0.20β0.21,strumβ0.27,redis0.31β1,
bb8-redisβ0.26,opendal0.54β0.57;serde_yamlβserde_yaml_ng.
Transitive for most apps. - Removed the dead
loco-clicrate (superseded byloco-new, the published
locobinary). ExtraDbInitializerremoved; useMultiDbInitializer. The
single-extra-connection initializer (initializers.extra_db, which layered a
bareExtension<DatabaseConnection>) is gone. UseMultiDbInitializerwith a
one-entryinitializers.multi_dbmap instead, and extract the connection with
Extension<MultiDb>+multi_db.get("<name>"). This collapses two
near-identical initializers into one named-connections abstraction.AppContextis now#[non_exhaustive]. Construct it with
AppContext::builder(environment, db, config)(orbuilder(environment, config)without thewith-dbfeature) followed by optional
.queue_provider(..)/.mailer(..)/.storage(..)/.cache(..)/.shared_store(..)
and.build(). Direct struct-literal construction and exhaustive pattern
matches onAppContextfrom outside the crate no longer compile; field
access (ctx.db,ctx.config,State/FromRefextraction) is unchanged.
This makes future context fields non-breaking to add.- Storage
MirrorStrategyandBackupStrategymerged intoReplicatedStrategy.
The two strategies were the same primary-plus-secondaries replication engine;
they are now onestorage::strategies::replicated::ReplicatedStrategywith a
singleFailurePolicyenum. Migrate:MirrorStrategy::new(p, s, MirrorAll)β
ReplicatedStrategy::mirror(p, s, FailurePolicy::FailIfAny);
BackupStrategy::new(p, s, BackupAll)βReplicatedStrategy::backup(p, s, FailurePolicy::FailIfAny). OldFailureModemaps:AllowMirrorFailure/
AllowBackupFailureβAllowAll,AtLeastOneFailureβAllowSingleFailure,
CountFailure(n)βFailAtFailures(n). Secondary writes for the former
backup strategy now run concurrently (previously sequential); the collected
errors and failure decision are unchanged. - Local storage driver no longer defaults its root to
/.
storage::drivers::local::new()previously rooted the filesystem store at
/, so any key β including one derived from user input β resolved against the
whole disk (e.g. downloading keyetc/passwdread/etc/passwd). It now roots
at the current working directory. Apps that relied on absolute-path keys should
switch tolocal::new_with_prefix("/your/root")to opt back into an explicit
absolute root. - Background queue reworked into a
QueueProvideradapter interface. The
bgworker::Queueenum (Postgres/Sqlite/Redis/None) is now a newtype
overArc<dyn QueueProvider>, so backends are pluggable (implement
QueueProviderand wrap withQueue::from_provider). All existing methods
(enqueue,register,run,ping,cancel_jobs,clear_by_status,
requeue, β¦) keep the same signatures and behavior. Only two source-level
changes affect callers: construct a no-op queue withQueue::empty()instead
ofQueue::None, and code that pattern-matched the enum variants (e.g.
Queue::Postgres(pool, ..)to reach the raw pool) no longer compiles β use the
provider methods instead. - Fallback middleware defaults to
404. When the built-in fallback is
enabled without an explicitcode, it now returns404 Not Found(matching
its docs and the bundled not-found page) instead of200 OK. Apps that relied
on the enabled fallback returning200must setcode: 200explicitly. The
file-based fallback is unaffected (ServeFilereports its own status). {env}.local.yamlnow deep-merges over{env}.yaml. Previously the first
existing file won and the other was ignored, so a.local.yamlhad to restate
the whole config. Both files are now layered with local precedence: mappings
merge recursively; scalars and sequences in local replace the base value
(sequences are not concatenated). Base keys now persist unless explicitly
overridden.- More accurate HTTP status codes for errors.
IntoResponse for Error
previously collapsed ~28 of 35 variants to500.Model(EntityNotFound)now
returns404,Model(EntityAlreadyExists)returns409, model validation
and form-body rejections return4xx(matching JSON rejections) instead of
500. Genuinely-internal errors still return a generic500. Handlers that
asserted on the old500s will observe the corrected codes. JWT::algorithm()restricted to the HMAC family. It now takes a new
loco_rs::auth::jwt::JWTAlgorithmenum (HS256/HS384/HS512) instead of
jsonwebtoken::Algorithm. Asymmetric algorithms β which could never work with
Loco's shared base64 secret and silently produced broken tokens β are no longer
representable.remote_ipmiddleware rebuilt onaxum-client-ip;trusted_proxies
removed. Previously this middleware walkedX-Forwarded-Forright-to-left,
skipping any address in a configurabletrusted_proxiesCIDR list (or a
built-in RFC-1918 + loopback list), so it could see through a chain of one or
more trusted proxies. It now trusts exactly one configured source
(source: ClientIpSource, defaultRightmostXForwardedFor) and does no
CIDR filtering β for the default it takes the last comma-separated value of the
lastX-Forwarded-Forheader verbatim, private or not. Single reverse-proxy
deployments are unaffected. Multi-hop topologies (CDN β LB β ingress) must
now configure their innermost hop to set the client IP (e.g. nginx
set_real_ip_from/real_ip_recursive), or pointsourceat a provider header
(CfConnectingIp,CloudFrontViewerAddress,XRealIp,ConnectInfo, β¦).
Note: an old config'strusted_proxies:key is silently ignored (unknown
field), so reviewremote_ipbefore upgrading β this is a silent
security-relevant behavior change, not a load error. TheRemoteIPextractor
and itsDisplayoutput are unchanged.auth_jwtfeature renamed toauth. Updatefeatures = ["auth_jwt"]β["auth"]
(it gates JWT auth and theApiTokenextractor, as before).- Background-queue features collapsed.
bg_pg/bg_sqltβworker
(Postgres+SQLite; free oncesqlxis compiled),bg_redisβworker_redis
(addsdep:redis).defaultnow hasworker(not Redis). A Redis queue needs
worker_redis; the queue backend is selected at runtime byqueue.kind. - Mailer
Template::new(dir)now returnsResult(callTemplate::new(dir)?).
Email templates render through a full Tera instance so they support inheritance
and shared templates. Standard usage viaMailer::mail_templateis unchanged.
(#1694) Vars::cli_argreturnsResult<&str>(wasResult<&String>). Callers that
relied on&String(e.g..clone()into aString) should use.to_owned().
(#1732)
Added
- First-class LLM / agent support. Root
AGENTS.mdteaches agents to build
Loco apps;llms.txt/llms-full.txtare served from the site
(llmstxt.org). Everyloco newapp ships an app-levelAGENTS.md. - Priority queues with
Worker::perform_later_with_priority(...); mailer
jobs default to priority100. (#1693) - Mailer implicit TLS (SMTPS / port 465) via
mailer.smtp.tls. (#1774, fixes #1773) - Run the scheduler without a worker β
--schedulerflag +
StartMode::ServerAndScheduler/WorkerAndScheduler. (#1742, fixes #1737) - Email headers support in the mailer (#1700).
- Multi-recipient emails.
Mailer::mail_multi/mail_template_multiand the
MultiEmail/MultiArgstypes send one email to multiple To/CC/BCC
recipients (processed by a dedicatedMultiMailerWorker). (#1764) - Email template inheritance & shared templates. Mailer templates support
Tera{% extends %}/{% block %}and can share a common layout via
Mailer::mail_template_with_shared/Template::new_with_shared.loco generate mailernow scaffolds asrc/mailers/shared/base layout that the welcome
template extends. (#1694) - "Create user" task (#1670).
UuidUniqWithDefaultandUuidWithDefaulttypes (#1642).- Allow overriding a secure header (#1659).
Mailer::deliver_now/mail_template_nowfor synchronous sends.
ComplementsMailer::mail/mail_template(which enqueue via the background
worker, like Railsdeliver_later) with an inline send that bypasses the
queue (Railsdeliver_now).MiddlewareStackExtfor surgical middleware-stack edits. Inside
Hooks::middlewares, tweak the default stack instead of rebuilding it:
stack.insert_before("cors", ..),.insert_after(..),.replace(..), and
.delete("logger")β matched by middleware name (Rails'
config.middleware.insert_before/delete). Available via the prelude.- Optional JWT extraction.
JWTnow implementsOptionalFromRequestParts,
so a handler can takeOption<JWT>to serve authenticated and anonymous
callers from one endpoint (Somewhen a valid token is present,None
otherwise). - Ergonomic verb-explicit route methods.
Routesnow hasget/post/
put/delete/patch/head/options/tracebuilder methods β
Routes::new().get("/ping", ping)alongside the existing
.add("/ping", get(ping)). They record the HTTP verb directly (exact
cargo loco routesoutput without relying on the debug-format regex).
Purely additive;addis unchanged. - Opt-in background-job reaper (visibility timeout). Each queue backend's
config accepts areaper: { age_minutes, interval_seconds }block. When set,
the worker periodically requeues jobs stranded inProcessing(e.g. by a
crashed worker) back toQueued, instead of requiring a manual
cargo loco jobs requeue. Disabled by default β existing behavior is unchanged. - TLS to managed Postgres and Redis. Postgres TLS works via the connection
URL (sslmode=require,sslrootcert=...) with no feature flag, and a new
redis_tlsfeature enablesrediss://for both the queue and cache Redis
backends (webpki roots, pure-Rustringprovider β no C toolchain). New
how-to: "Connect to Postgres and Redis over TLS". (#1191, #1341) - Typed, streaming
db::dump::<A>()β counterpart todb::seed::<A>()that
streams rows through their entityModelstraight to disk (memory bounded to
a single row) with full type fidelity. NewHooks::dump(default dumps every
table; override it to calldb::dumpper entity) backscargo loco db seed --dump. (#1691) logger::init_layer/logger::init_env_filterare now public building
blocks, so an app overridingHooks::init_loggercan reuse Loco's formatting
and filter policy while adding its own layers (e.g.tracing-flame, OTLP).
(#1753)
Changed
- Wrap
TeraViewinArcto reduce runtime memory usage (#1703). - Refactor users model to reuse
find_by_api_keyinAuthenticable(#1706). - Split error detail generic parameters (#1709).
- Update
loco-newfor the new Rhai version (#1704). - Replaced hand-rolled
Cargo.lockparsing with thecargo-lockcrate; retired
duct_sh. Errorβ HTTP-status mapping is now exhaustive. TheIntoResponse for Errormatch dropped its trailing_ => 500wildcard: every variant (and every
nestedModelErrorvariant) is now classified explicitly, so adding a new
error variant is a compile error until its status is chosen β it can no longer
silently default to500. TheErrorenum is also reorganized into
client-facing vs internal/infra regions. Behavior is unchanged (all infra
errors still map to500); no variant was renamed, so existing code is
unaffected.- Rust edition 2024.
loco-rs,loco-gen,xtask, and theloconew-app
generator now compile on edition 2024 (MSRV floor unchanged at 1.94; edition
2024 needs β₯ 1.85). Editions are per-crate, so apps depending on Loco need not
change. Newly generated apps stay on edition 2021 for now. - Deduplicated the Postgres and SQLite background-queue providers: the shared
Job/JobRegistry/RunOptsnow live in one module behind aDrivertrait
(internal refactor, no behavior or API-path change). - In-memory cache now uses
moka::future::Cacheinstead of wrapping the
synchronous cache behind#[async_trait](removes a sync-behind-async smell;
no API change). - Cookie token extraction now uses
axum_extra'sCookie::value()instead of
hand-parsing the cookie string (byte-identical behavior). - Internal de-duplication pass (no public-API-path or behavior change unless
noted): the response helpers informatare now single-sourced through
RenderBuilder; theJWT/JWTWithUserextractors share one validate/decode
helper; the six validate extractors are generated from shared decoder fns +
two error-tier macros; the byte-identicalJobstruct is shared across the
SQL and Redis queue backends; the twincli::mainfunctions share one
dispatch_common; and duplicate env-var name constants were removed. format's two response paths were converged onto axum's canonical behavior:
RenderBuilder::jsonandRenderBuilder::redirect_with_header_keyare now
infallible with respect to bad input (they return a500response, matching
axum::Json/axum::response::Redirect) instead of returningErr.
Fixed
db seed --dumpdatetime round-trip on SQLite. Loco's timestamptz columns
default toCURRENT_TIMESTAMP, which SQLite stores as space-separated text
("YYYY-MM-DD HH:MM:SS"); dumps captured that verbatim and then failed
chrono's RFC3339 parse on re-seed (Json("premature end of input")). Dumps now
normalize such datetimes to RFC3339 (already-RFC3339 text is untouched).
(#1736, #1691)cargo fmterror inloco-new(#1669).- UUID pattern in form field generation (#1665).
- Clippy warnings for recent Rust (#1705).
- Add tests for the auth extractor (#1671).
- Postgres/SQLite queue backends now behave consistently. Two divergences
between the Postgres and SQLite job backends are fixed: (1)enqueueon
Postgres previously swallowed a tag-serialization error (storingtags = null); it now propagates the error like SQLite. (2)complete_jobwithout a
repeat interval on Postgres stampedrun_at = NOW()on the completed row while
SQLite left it untouched; Postgres now leavesrun_atas-is, matching SQLite
(the interval path still reschedulesrun_aton both). The sharedto_jobrow
mapper is now single-sourced across both backends. - Storage mirror fan-out no longer stops at the first failing secondary.
rename,copy, andupload_streamchecked the failure mode inside the
secondary loop and returned early on the first failure, silently leaving later
mirrors stale (upload/deletewere already correct). All five mutating
methods now share one helper that attempts every secondary (concurrently) and
applies the failure mode once. - Postgres
BOOLEANcolumns are no longer dropped fromdump_tables. The
decode probe chain had noboolarm, so PG booleans (which don't fall back to
the numeric arms like SQLite's integer-backed booleans) were silently omitted. Hooks::on_shutdownnow runs in worker-only start modes.WorkerOnlyand
WorkerAndSchedulerbypassedH::serve(the hook's only caller); the shutdown
hook is now invoked on their shutdown path too.- Postgres admin/maintenance URI is derived with the
urlcrate. Building it
viadb_uri.replace(db_name, "/postgres")corrupted the URI when the database
name also appeared in the host or credentials. - Foreign-key names are normalized consistently.
reference_idreceived a
normalized table name increate_tablebut raw names in
add_reference/remove_reference, so irregular plurals produced mismatched FK
column/constraint names between creation and later add/remove. ViewEngineextractor now rejects gracefully (HTTP500) when the opt-in Tera
layer is absent, instead of declaringInfallibleand then panicking.cargo loco routeslists every HTTP verb of a multi-method route (route
introspection previously reported only the first).- Removed a fossilized 3-second
sleepon every Redis queue boot (a leaked
test-isolation artifact; Postgres/SQLite had no equivalent). - Password redaction in test snapshots (
cleanup_user_model) now targets the
quoted value precisely; the previous pattern had a degenerate quantifier that
swallowed the field followingpassword. - Postgres test-database cleanup now completes synchronously (a joined worker
thread) instead of a fire-and-forget task, so parallel test runs no longer
leak databases;PostgresTestalso builds its connection strings with the
urlcrate rather than a corruption-prone substring replace. RenderBuilder::templatenow threads the builder's chainedstatus/header/
etag/cookiesthrough to the response; it previously delegated to the free
html()and silently dropped them.llms.txt: twoCore conceptslinks pointed at doc pages that don't exist
(the-app/configuration/,the-app/testing/); repointed to the sections that
actually document them. A newcargo xtask llms-checkCI step now verifies the
curated LLM docs against the docs tree so these links can't drift silently.
Removed
Errorenum narrowing. Removed four low-value/dependency-leaking variants
that all mapped to HTTP 500 and were never matched:Error::EnvVar,
Error::SemVer,Error::TaskJoinError, andError::Hash(hashing errors now
surface asError::Message).Errorremains#[non_exhaustive], so exhaustive
matches already require a wildcard arm and are unaffected.- Deleted shipped-but-dead code: the never-compiled
controller/middleware/_archive/content_etag.rsmodule and a commented-out
block of backtrace-blocklist regexes.