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
Breaking Changes
Sea-ORM 2.0 + sqlx 0.9. Bumpsea-orm
/sea-orm-migration
to2.0
(app +migration
crate), directsqlx
to0.9
, update the Sea-ORM CLI, and
regenerate entities. Raw-Statement
calls gain a_raw
suffix; runtime SQL
strings needAssertSqlSafe
. MSRV is1.94(sea-orm 2.0.0 declares it). (Adopted from the SeaQL fork and
#1698.)Generated primary/foreign keys are now 64-bit (BIGINT / Also thei64
).
int
/unsigned
field 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. Postgres/SQLite auto-migrate a
upgradingpriority
column (no action).
([#1693](https://github.com/loco-rs/loco/pull/1693))(`Worker::perform_later()`
returns the job IDResult<String>
), and
`Queue::enqueue()`
returns`Result<Option<String>>`
. Existing
`perform_later(...).await?;`
keeps working. ([#1624](https://github.com/loco-rs/loco/pull/1624), fixes[#1623](https://github.com/loco-rs/loco/issues/1623))instead of flat`PageResponse<T>`
exposesmeta: PagerMeta
total_pages
/total_items
(also carriespage
/page_size
). (#1685, fixes#1683)View engine: useengines::TeraView::build_with_post_process(...)
instead
ofTeraView::build()?.post_process(...)
inafter_routes
.Dependency majors:thiserror
1→2,tower
0.4→0.5,heck
→0.5,
byte-unit
4→5,ipnetwork
0.20→0.21,strum
→0.27,redis
0.31→1,
bb8-redis
→0.26,opendal
0.54→0.57;serde_yaml
→serde_yaml_ng
.
Transitive for most apps.- Removed the dead
loco-cli
crate (superseded byloco-new
, the published
loco
binary). TheExtraDbInitializer
removed; useMultiDbInitializer
.
single-extra-connection initializer (initializers.extra_db
, which layered a
bareExtension<DatabaseConnection>
) is gone. UseMultiDbInitializer
with a
one-entryinitializers.multi_db
map instead, and extract the connection with
Extension<MultiDb>
+multi_db.get("<name>")
. This collapses two
near-identical initializers into one named-connections abstraction.Construct it withAppContext
is now#[non_exhaustive]
.
`AppContext::builder(environment, db, config)`
(or`builder(environment, config)`
without thewith-db
feature) followed by optional
`.queue_provider(..)`
/`.mailer(..)`
/`.storage(..)`
/`.cache(..)`
/`.shared_store(..)`
and`.build()`
. Direct struct-literal construction and exhaustive pattern
matches onAppContext
from outside the crate no longer compile; field
access (ctx.db
,ctx.config
,State
/FromRef
extraction) is unchanged.
This makes future context fields non-breaking to add.StorageMirrorStrategy
andBackupStrategy
merged intoReplicatedStrategy
.
The two strategies were the same primary-plus-secondaries replication engine;
they are now onestorage::strategies::replicated::ReplicatedStrategy
with a
singleFailurePolicy
enum. Migrate:MirrorStrategy::new(p, s, MirrorAll)
→
ReplicatedStrategy::mirror(p, s, FailurePolicy::FailIfAny)
;
`BackupStrategy::new(p, s, BackupAll)`
→`ReplicatedStrategy::backup(p, s, FailurePolicy::FailIfAny)`
. OldFailureMode
maps: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. down keyetc/passwd
read/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 TheQueueProvider
adapter interface.
`bgworker::Queue`
enum (`Postgres`
/Sqlite
/Redis
/None
) is now a newtype
overArc<dyn QueueProvider>
, so backends are pluggable (implement
QueueProvider
and 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 When the built-in fallback is404
.
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 returning200
must setcode: 200
explicitly. The
file-based fallback is unaffected (ServeFile
reports its own status).Previously the first{env}.local.yaml
now deep-merges over{env}.yaml
.
existing file won and the other was ignored, so a.local.yaml
had 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 old500
s will observe the corrected codes.It now takes a newJWT::algorithm()
restricted to the HMAC family.
`loco_rs::auth::jwt::JWTAlgorithm`
enum (`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.Previously this middleware walkedremote_ip
middleware rebuilt onaxum-client-ip
;trusted_proxies
removed.`X-Forwarded-For`
right-to-left,
skipping any address in a configurabletrusted_proxies
CIDR 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 exactlyone configured source
(source: ClientIpSource
, defaultRightmostXForwardedFor
) and doesno
CIDR filtering — for the default it takes the last comma-separated value of the
lastX-Forwarded-For
header 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 pointsource
at a provider header
(CfConnectingIp
,CloudFrontViewerAddress
,XRealIp
,ConnectInfo
, …).
Note: an old config'strusted_proxies:
key is silently ignored (unknown
field), so reviewremote_ip
before upgrading — this is a silent
security-relevant behavior change, not a load error. TheRemoteIP
extractor
and itsDisplay
output are unchanged.Updateauth_jwt
feature renamed toauth
.`features = ["auth_jwt"]`
→`["auth"]`
(it gates JWT auth and theApiToken
extractor, as before).Background-queue features collapsed.bg_pg
/bg_sqlt
→worker
(Postgres+SQLite; free oncesqlx
is compiled),bg_redis
→worker_redis
(addsdep:redis
).default
now hasworker
(not Redis). A Redis queue needs
worker_redis
; the queue backend is selected at runtime byqueue.kind
.Mailer(callTemplate::new(dir)
now returnsResult
Template::new(dir)?
).
Email templates render through a full Tera instance so they support inheritance
and shared templates. Standard usage viaMailer::mail_template
is unchanged.
(#1694)(wasVars::cli_arg
returnsResult<&str>
Result<&String>
). Callers that
relied on&String
(e.g..clone()
into aString
) should use.to_owned()
.
(#1732)
Added
First-class LLM / agent support. RootAGENTS.md
teaches agents to build
Loco apps;llms.txt
/llms-full.txt
are served from the site
(llmstxt.org). Everyloco new
app ships an app-levelAGENTS.md
.Priority queues withWorker::perform_later_with_priority(...)
; mailer
jobs default to priority100
. ([#1693](https://github.com/loco-rs/loco/pull/1693))**Mailer implicit TLS (SMTPS / port 465)** via`mailer.smtp.tls`
. ([#1774](https://github.com/loco-rs/loco/pull/1774), fixes[#1773](https://github.com/loco-rs/loco/issues/1773))**Run the scheduler without a worker**—`--scheduler`
flag +
StartMode::ServerAndScheduler
/WorkerAndScheduler
. ([#1742](https://github.com/loco-rs/loco/pull/1742), fixes[#1737](https://github.com/loco-rs/loco/issues/1737))- Email headers support in the mailer (
[#1700](https://github.com/loco-rs/loco/pull/1700)). **Multi-recipient emails.**`Mailer::mail_multi`
/mail_template_multi
and the
MultiEmail
/MultiArgs
types send one email to multiple To/CC/BCC
recipients (processed by a dedicatedMultiMailerWorker
). ([#1764](https://github.com/loco-rs/loco/pull/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 mailer
now scaffolds asrc/mailers/shared/
base layout that the welcome
template extends. ([#1694](https://github.com/loco-rs/loco/pull/1694))- "Create user" task (
[#1670](https://github.com/loco-rs/loco/pull/1670)). `UuidUniqWithDefault`
andUuidWithDefault
types ([#1642](https://github.com/loco-rs/loco/pull/1642)).- Allow overriding a secure header (
[#1659](https://github.com/loco-rs/loco/pull/1659)). `Mailer::deliver_now`
/mail_template_now
for synchronous sends.
Complements`Mailer::mail`
/mail_template
(which enqueue via the background
worker, like Railsdeliver_later
) with an inline send that bypasses the
queue (Railsdeliver_now
).InsideMiddlewareStackExt
for surgical middleware-stack edits.
`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.JWT
now implementsOptionalFromRequestParts
,
so a handler can takeOption<JWT>
to serve authenticated and anonymous
callers from one endpoint (Some
when a valid token is present,None
otherwise).Ergonomic verb-explicit route methods.Routes
now hasget
/post
/
put
/delete
/patch
/head
/options
/trace
builder methods —
Routes::new().get("/ping", ping)
alongside the existing
.add("/ping", get(ping))
. They record the HTTP verb directly (exact
cargo loco routes
output without relying on the debug-format regex).
Purely additive;add
is 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_tls
feature enablesrediss://
for both the queue and cache Redis
backends (webpki roots, pure-Rust`ring`
provider — no C toolchain). New
how-to: "Connect to Postgres and Redis over TLS". ([#1191](https://github.com/loco-rs/loco/issues/1191),[#1341](https://github.com/loco-rs/loco/issues/1341))**Typed, streaming**— counterpart to`db::dump::<A>()`
`db::seed::<A>()`
that
streams rows through their entityModel
straight to disk (memory bounded to
a single row) with full type fidelity. NewHooks::dump
(default dumps every
table; override it to call`db::dump`
per entity) backs`cargo loco db seed --dump`
. ([#1691](https://github.com/loco-rs/loco/issues/1691))building`logger::init_layer`
/`logger::init_env_filter`
are now public
blocks, so an app overridingHooks::init_logger
can reuse Loco's formatting
and filter policy while adding its own layers (e.g.tracing-flame
, OTLP).
(#1753)
Changed
- Wrap
TeraView
inArc
to reduce runtime memory usage (#1703). - Refactor users model to reuse
find_by_api_key
inAuthenticable
([#1706](https://github.com/loco-rs/loco/pull/1706)). - Split error detail generic parameters (
[#1709](https://github.com/loco-rs/loco/pull/1709)). - Update
loco-new
for the new Rhai version (#1704). - Replaced hand-rolled
Cargo.lock
parsing with thecargo-lock
crate; retired
duct_sh
. TheError
→ HTTP-status mapping is now exhaustive.IntoResponse for Error
match dropped its trailing`_ => 500`
wildcard: every variant (and every
nestedModelError
variant) 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
. TheError
enum 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 theloco
new-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
/RunOpts
now live in one module behind aDriver
trait
(internal refactor, no behavior or API-path change). - In-memory cache now uses
`moka::future::Cache`
instead 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 in`format`
are now single-sourced through
RenderBuilder
; theJWT
/JWTWithUser
extractors share one validate/decode
helper; the six validate extractors are generated from shared decoder fns +
two error-tier macros; the byte-identical`Job`
struct is shared across the
SQL and Redis queue backends; the twin`cli::main`
functions 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::json
andRenderBuilder::redirect_with_header_key
are now
infallible with respect to bad input (they return a500
response, matching
`axum::Json`
/`axum::response::Redirect`
) instead of returningErr
.
Fixed
Loco's timestamptz columnsdb seed --dump
datetime round-trip on SQLite.
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 fmt
error inloco-new
([#1669](https://github.com/loco-rs/loco/pull/1669)).- UUID pattern in form field generation (
[#1665](https://github.com/loco-rs/loco/pull/1665)). - Clippy warnings for recent Rust (
[#1705](https://github.com/loco-rs/loco/pull/1705)). - Add tests for the auth extractor (
[#1671](https://github.com/loco-rs/loco/pull/1671)). **Postgres/SQLite queue backends now behave consistently.** Two divergences
between the Postgres and SQLite job backends are fixed: (1)enqueue
on
Postgres previouslyswalloweda tag-serialization error (storingtags = null
); it now propagates the error like SQLite. (2)complete_job
without a
repeat interval on Postgres stampedrun_at = NOW()
on the completed row while
SQLite left it untouched; Postgres now leavesrun_at
as-is, matching SQLite
(the interval path still reschedulesrun_at
on both). The sharedto_job
row
mapper is now single-sourced across both backends.Storage mirror fan-out no longer stops at the first failing secondary.
rename
,copy
, andupload_stream
checked the failure modeinsidethe
secondary loop and returned early on the first failure, silently leaving later
mirrors stale (upload
/delete
were already correct). All five mutating
methods now share one helper that attempts every secondary (concurrently) and
applies the failure mode once.Postgres TheBOOLEAN
columns are no longer dropped fromdump_tables
.
decode probe chain had nobool
arm, so PG booleans (which don't fall back to
the numeric arms like SQLite's integer-backed booleans) were silently omitted.Hooks::on_shutdown
now runs in worker-only start modes.WorkerOnly
and
WorkerAndScheduler
bypassed`H::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 Building iturl
crate.
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_id
received a
normalized table name increate_table
but raw names in
add_reference
/remove_reference
, so irregular plurals produced mismatched FK
column/constraint names between creation and later add/remove.ViewEngine
extractor now rejects gracefully (HTTP500
) when the opt-in Tera
layer is absent, instead of declaringInfallible
and then panicking.cargo loco routes
lists every HTTP verb of a multi-method route (route
introspection previously reported only the first).- Removed a fossilized 3-second
sleep
on 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;PostgresTest
also builds its connection strings with the
url
crate rather than a corruption-prone substring replace. RenderBuilder::template
now threads the builder's chainedstatus
/header
/
etag
/cookies
through to the response; it previously delegated to the free
html()
and silently dropped them.llms.txt
: twoCore concepts
links 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-check
CI step now verifies the
curated LLM docs against the docs tree so these links can't drift silently.
Removed
Removed four low-value/dependency-leaking variantsError
enum narrowing.
that all mapped to HTTP 500 and were never matched:Error::EnvVar
,
`Error::SemVer`
,`Error::TaskJoinError`
, and`Error::Hash`
(hashing errors now
surface asError::Message
).Error
remains#[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.rs
module and a commented-out block of backtrace-blocklist regexes.