v1.1.0

loco-rs/locov1.1.0Aug 16, 2026by jondot

AI Summary

This release modernizes the framework by upgrading the template engine to Tera 2, adding AWS Lambda deployment support, and improving configuration validity. It also introduces CLI improvements for job management and authentication scaffolding.

Key Highlights

  • Upgrade to Tera 2 for views and mailers.
  • New `cargo loco generate deployment lambda` command.
  • New `cargo loco jobs retry` command for failed jobs.
  • Config files are now valid YAML.
  • Added `--no-auth` flag for scaffolding.

Breaking Changes

  • Scaffolded `list` endpoints changed their JSON contract (pagination fields renamed).
  • `QueueProvider` gains a required `retry_failed` method.
  • Storage drivers/strategies gain required `list` and `stat` methods.
  • Template engine is now Tera 2 (filter signatures changed).
  • Newly generated apps require production secrets to be set.
  • `post_process` now runs before templates are loaded.

New Features

  • `--no-auth` on `generate scaffold`.
  • `cargo loco jobs retry`.
  • AWS Lambda deployment generator.
  • `AppContext::into_builder`.
  • `loco_rs::schema::rename_column`.
  • `PageResponse` is now `Serialize`/`Deserialize`.

Full Release Notes

1.0 went out a few weeks ago and people started actually building on it. That's
the useful part — you find out fast which of your ideas survive contact with
real apps.

**[Read the announcement →](https://loco.rs/blog/loco-1-1-0/)**

### What's new

- **Tera 2** for views and mailers. For most apps this is one line: bump
  `fluent-templates` from `0.13` to `0.15`. Your templates don't need
  rewriting unless they use `{% macro %}`, `{% import %}` or `v.0` — Loco's
  generated ones use none of those.
- **Deploy to Lambda** — `cargo loco generate deployment lambda`. Loco's
  router is a `tower::Service` and so is the Lambda runtime, so your app runs
  unchanged.
- **`--no-auth` on scaffold**, `--auth` on `generate controller`. Scaffolded
  routes took a JWT extractor on all five handlers with no way out and nothing
  saying so.
- **`cargo loco jobs retry`** — a failed job used to be terminal.
- **Storage** grew `exists`, `list` and `stat`; generated config files are
  valid YAML.

Two queue fixes, both now covered by tests that fail without them: on SQLite,
`queue.dangerously_flush: true` could leave `clear` unable to hand out jobs; on
Redis, completed and failed jobs were invisible to `jobs dump`, `jobs purge`
and the other operator commands.

### Upgrading

The [upgrade guide](https://loco.rs/docs/extras/upgrades/) has a prompt you can
paste straight into Claude Code or any coding agent — it covers the whole
1.0 → 1.1 change surface and applies only what your app actually uses.

---


Moves the template engine to Tera 2, makes configuration files valid YAML,
opens up the storage API, and adds an AWS Lambda deployment target.

**Upgrading:** for most apps this is a one-line change — bump
`fluent-templates` from `0.13` to `0.15` in `Cargo.toml`. That crate supplies
the `t()` i18n function in generated apps and pinned Tera 1; 0.15 moved to
Tera 2. Verified by generating an app and compiling it with no other edit.

Beyond that, only apps that register **their own** Tera filters or functions
need work (see the breaking note below), and only templates using
`{% macro %}`/`{% import %}`, `v.0` array access, or relying on undefined
variables rendering empty need editing — Tera 2 replaced macros with
components, requires `v[0]`, and errors on undefined variables.

That last one applies to **mailer** templates as well as views: they render
through the same Tera 2 instance, so a mail template referencing an optional
field that is sometimes absent now fails at send time where Tera 1 rendered
an empty string. Add `| default(value="")` to those references.

### Breaking

- **Scaffolded `list` endpoints changed their JSON contract**, and now use the
  framework's own pagination instead of a second, parallel one. The scaffold
  had its own `ListParams`, its own paginator arithmetic, and an envelope with
  no `total_pages` — while `query::PaginationQuery` + `query::paginate` already
  existed and carried it. The generated handler is now four lines over
  `query::paginate`, and `Page<T>` (`src/dtos/common.rs`) is built by
  `Page::from_query`.

  On the wire: `per_page` → `page_size`, `total` → `total_items`, and
  `total_pages` is added — the metadata field names are `PagerMeta`'s, so an
  app has one pagination vocabulary whichever envelope a handler returns. The
  query parameter is likewise `page_size`. Existing apps keep the code they
  already generated; regenerating, or updating by hand, is a rename plus one
  new field. The typed frontend's `bindings/Page.ts` regenerates from `ts-rs`.

- **`QueueProvider` gains a required `retry_failed` method.** Custom queue
  provider implementations must implement it; all three built-in drivers do.
  (`Queue::retry_failed` is the inherent forwarding method on the handle and
  requires nothing of anyone.)

- **Storage `StoreDriver` / `StorageStrategy` gain required `list` and `stat`
  methods** (and `StorageStrategy` also requires `exists`). Custom driver and
  strategy implementations must implement them. Built-in drivers/strategies
  already do. The `Storage` facade exposes matching `exists` / `list` / `stat`
  (and `*_with_policy`) APIs alongside the existing upload/download/delete
  surface. Under `ReplicatedStrategy` in mirror mode, `exists` and `list` treat
  a miss (`false` / `[]`) the way the other reads treat an error and fall back
  to secondaries; backup mode stays primary-only.
  ([#1805](https://github.com/loco-rs/loco/pull/1805))

- **The template engine is now Tera 2.** `tera::Tera` appears in Loco's public
  API (`PostProcessFn`, `HotReloadingTeraEngine::engine`, `TeraView::tera`,
  `Error::Tera`, `register_filters`), so custom filters and functions must move
  to Tera 2 signatures: filters now take `(Arg, Kwargs, &State)` over Tera's own
  `Value` rather than `(&Value, &HashMap)` over `serde_json::Value`.

  Loco absorbs the rest. Tera 2 dropped `get_env` — which every Loco config
  depends on — so Loco registers its own with Tera 1 semantics. View loading
  moved to `tera::load_from_glob`, which yields the same template names Tera 1
  produced, so existing `render("home/hello.html")` calls are unaffected.

- **Newly generated apps require their production secrets to be set.** The
  generated `config/production.yaml` takes no defaults for secrets or
  addresses, so a missing one stops the app at startup with the variable's
  name rather than falling back to a development value. Always required:
  `DATABASE_URL`, `JWT_SECRET`, `HOST`. Required too, when the app was
  generated with the corresponding component: `REDIS_URL` / `QUEUE_URL` for a
  Redis or database-backed queue, and `MAILER_HOST` / `MAILER_USER` /
  `MAILER_PASSWORD` for a mailer. Existing apps are unaffected — their config
  files are their own. See *Fixed* below.

- `doctor::Resource` is now `#[non_exhaustive]` and has gained a
  `ProductionSafety` variant. The variant is **first**, and `Resource` is the
  key type of the `BTreeMap` the doctor report is built from, so its derived
  `Ord` sets the report's display order — production safety now leads, and
  every other variant shifts by one. Code comparing or sorting `Resource`
  values sees the new order.

- **The three built-in number filters take Tera 2 signatures.**
  `views::tera_builtins::filters::number::{number_with_delimiter,
  number_to_human_size, number_to_percentage}` are `(value, Kwargs, &State)`
  rather than `(value, &HashMap)`. They are `pub`, so calling them directly —
  rather than through a template — needs updating. Templates are unaffected.

- **`auth.jwt.location` is parsed strictly.** Its `Deserialize` is hand-written
  rather than `#[serde(untagged)]`, so it accepts exactly the documented shapes
  — a map, or a list of maps — and reports a real error for anything else
  instead of a generic "did not match any variant". Config that happened to
  parse through untagged fallback will now be rejected with a message naming
  the problem. The serialized form is unchanged.

- `bgworker::pg::get_jobs` returned `Result<Vec<Job>, sqlx::Error>` where the
  SQLite driver's returned `loco_rs::Result`. It now returns `loco_rs::Result`
  too. Callers using `?` in a Loco context are unaffected; code matching on
  `sqlx::Error` needs updating.

- `loco_gen::AppInfo` gains a `working_dir` field, so post-generation checks
  read the tree that was generated into rather than the process's current
  directory. Callers using `new_generator()` should pass `".".into()`.

- `loco_gen::Component::Scaffold` and `::Controller` gain a required `auth:
  bool` field, backing the new `--no-auth` / `--auth` flags. Code constructing
  these variants directly — rather than going through the CLI — must supply it.

- `loco_gen::DeploymentKind` and the CLI's `DeploymentKind` both gain a
  `Lambda` variant. Neither is `#[non_exhaustive]`, so an exhaustive `match`
  over either stops compiling until the arm is added.

- **`post_process` now runs before templates are loaded**, in both the
  on-disk and embedded view engines. A `build_with_post_process` closure can
  no longer inspect loaded templates — it registers into an empty engine — and
  anything a template calls must be registered by that closure, or template
  loading fails. This is what lets a custom filter be visible to the templates
  that use it; previously registration happened too late.

### Added

- **`--no-auth` on `generate scaffold`, `--auth` on `generate controller`.**
  Scaffolded routes take an `auth::JWT` extractor on all five handlers, which
  is the right default but had no opt-out and nothing said so — the first
  `curl` against a fresh scaffold answered 401 with no explanation, and the
  tutorial's own examples were among the casualties. The scaffold now prints
  which flavor it generated, and `--no-auth` emits public routes. A generated
  controller has no model to protect and stays public by default; `--auth` is
  its opt-in mirror, and it adjusts the generated request test to assert the
  route rejects anonymous callers rather than expecting 200.

  `cargo loco generate` now runs a best-effort `cargo fmt` afterwards, the way
  `loco new` already did. A Tera template cannot know where rustfmt would break
  a line — with the auth argument gone, three handler signatures fit on one —
  and a generated app runs `cargo fmt --check` in its own CI, so non-canonical
  output would have failed a user's build on code they did not write.
- **`cargo loco jobs retry`** moves failed jobs back to `queued` — with
  `--id <ID>` for one, or bare for all of them. No queue driver has automatic
  retry or backoff, so a failed job used to be terminal: `requeue`, the verb
  that sounds like the recourse, only rescues jobs a crashed worker stranded in
  `processing` and cannot touch a failed one. `run_at` is reset so a job that
  failed on a future-dated schedule runs now rather than when that time
  arrives. On the Redis provider a retried job is queued to `default`: the
  queue a job was submitted to is not recorded once it fails. The command says
  so when it retries anything, but operators running multiple named queues
  should know before they need it.
- The `loco` CLI now declares `rust-version = "1.94"`, matching the framework.
  `cargo install loco` on an older toolchain refuses up front with the required
  version instead of failing partway through a build.
- `QueueConfig::dangerously_flush()` is now public, for tests and tooling that
  need to empty a queue outright.
- **AWS Lambda deployment generator** — `cargo loco generate deployment lambda`
  writes a `src/bin/lambda.rs` entrypoint, adds `lambda_http`, and writes a
  `[package.metadata.lambda]` block so `cargo lambda build` and
  `cargo lambda deploy` need no flags. Loco's router is a `tower::Service` and
  so is the Lambda runtime, so the app runs unchanged; deployment is delegated
  to `cargo-lambda` rather than embedding an AWS SDK. HTTP only — workers and
  the scheduler do not fit Lambda's model.
  ([#1699](https://github.com/loco-rs/loco/issues/1699))

- **`AppContext::into_builder`** — the escape hatch `Hooks::after_context` was
  missing. `AppContext` is `#[non_exhaustive]`, so `AppContext { storage, ..ctx }`
  (the idiom the storage how-to showed) does not compile outside `loco-rs`, and
  rebuilding from `AppContext::builder` silently drops the mailer, queue
  provider, cache and shared store that boot had already attached.
  `ctx.into_builder().storage(..).build()` replaces one component and keeps the
  rest.
- **`loco_rs::schema::rename_column`**, alongside `add_column`/`remove_column`.
- **`PageResponse` is now `Serialize`/`Deserialize`.** The pagination how-to
  showed `format::json(res)` returning one straight from a handler, and printed
  the JSON body it produces; that could not compile, because the struct derived
  only `Debug`. The documented body is now pinned by a test.

### Security

- **`opendal` 0.57 → 0.58.1**, which moves `quick-xml` from `^0.39.3` to
  `^0.41.0` in the S3, Azure and GCS service crates. `quick-xml` below 0.41.0
  carries RUSTSEC-2026-0194 (quadratic time checking a start tag for duplicate
  attribute names) and RUSTSEC-2026-0195 (unbounded namespace-declaration
  allocation); both are denial of service, and both are `patched = [">= 0.41.0"]`.

  This reaches only apps that enable a cloud storage feature — `storage_aws_s3`,
  `storage_azure`, `storage_gcp`, all off by default — and the XML being parsed
  is the storage backend's own responses, so exploiting it means controlling
  what that endpoint returns. Narrow, but real if you point Loco at an
  S3-compatible endpoint you do not run.

  **This fix ships in 1.1.0, not in 1.0.x.** The 1.0.0 section below described
  the bump as if it had shipped there; it had not — 1.0.0 and 1.0.1 both went
  out pinning `opendal = "0.57"`. That text has been corrected. If you enabled a
  cloud storage feature on 1.0.0 or 1.0.1, upgrading to 1.1.0 is the fix.

### Fixed

- **On the SQLite queue driver, clearing the queue stopped it handing out jobs
  — permanently and silently.** `dequeue` claimed an advisory lock by updating
  a row in a second table, `sqlt_loco_queue_lock`, while `clear` deleted every
  row in that table. With the row gone the lock could never be acquired, so
  `dequeue` returned "no jobs" forever, with the jobs sitting right there. Any
  app configured with `queue.dangerously_flush: true` hit this on every boot —
  `converge` runs `setup` and then `clear` — so its workers never ran a single
  job. The existing test asserted the row count was zero afterwards, locking
  the bug in.

  The lock table is gone entirely. It was a hand-rolled stand-in for `BEGIN
  IMMEDIATE`, which is how SQLite itself takes the write lock before the
  `SELECT` that picks a job — a plain `BEGIN` defers it until the first write
  and leaves the read unprotected, which is the race the table existed to
  close. `initialize_database` drops `sqlt_loco_queue_lock` if it is still
  there, so upgrading needs no action. The mutual-exclusion guarantee is now
  covered by a test that runs concurrent dequeues and fails without the fix.

- **`examples/reference_spa` did not build for anyone but its author.** Its
  manifest pinned `loco-rs` to an absolute path on the maintainer's machine
  (`path = "/Users/…/loco"`) — the shape `LOCO_DEV_MODE_PATH` generates — so
  every other checkout failed with `failed to load manifest for dependency
  loco-rs`. It is `path = "../.."` now, and CI runs both examples' test suites,
  which is what caught it.

- **snipdoc overwrote the translated READMEs.** Each translation carries the
  same `<snip>` regions as `README.md`, so every injection run replaced the
  translated tagline, install comment and `loco new` transcript with the English
  source — and CI's `snipdoc check` then failed whenever a translator put their
  version back. The Spanish and Vietnamese READMEs had already lost theirs.
  `snipdoc-config.yml` now excludes the translated filenames from the walk, so
  only the canonical `README.md` is injected, and the two translations are
  restored to what their translators wrote.

- **On the Redis queue driver, a job vanished from every operator tool the
  moment it stopped being runnable.** `get_jobs` enumerated jobs by walking the
  queue and processing keys, but `complete_job` and `fail_job` both remove the
  id from the processing set and add it to no queue — so `jobs dump --status
  failed`, `jobs purge`, `clear_by_status` and `clear_jobs_older_than` all
  silently reported nothing for completed and failed jobs. It now enumerates the
  `job:*` keys, which are the record of a job's existence, and consults the
  processing sets only to tell a job a worker is holding from one still queued.
  The existing test could not catch this: it asserted inside
  `for job in &failed_jobs`, which passes on an empty list — and the list was
  always empty.
- **Test snapshots no longer break west of UTC.** `get_cleanup_date`'s
  timestamp rule ended in `\+\d{2}:\d{2}`, matching only a *positive* UTC
  offset. It is the only rule that consumes the offset, so on a machine west of
  UTC the timestamp fell through to the offset-less rules, which stop at the
  seconds — redacting to `DATE-03:00` instead of `DATE` and failing every
  snapshot carrying a timestamp. A freshly generated app therefore had a red
  test suite out of the box for everyone west of UTC, and had since 0.14.
  CI never caught it because GitHub runners are UTC.
  ([#1802](https://github.com/loco-rs/loco/pull/1802))
- **Every npm advisory in `website/` and the reference SPA is cleared**, and the
  `loco new` frontend template no longer pins `vite`/`@vitejs/plugin-react` to
  a floating `"latest"`. Its `react-router` floor moves to `^8.3.0`, below which
  a generated app resolves a version with an RSC-mode CSRF bypass.
- **Config templating is now YAML-safe** (`<%= ... %>` instead of `{{ ... }}`).
  `{` is a YAML flow-mapping indicator, so `port: {{ get_env(...) }}` was never
  valid YAML at rest — it only parsed because Loco's template pass rewrote the
  file first. Any tool reading the file *as YAML* (prettier, yaml-language-server,
  format-on-save) restructured it into `{ { ... } }` and broke startup.
  `<` is not a YAML indicator, so the new form is an ordinary string scalar:
  config files are valid YAML before rendering and survive formatting untouched.
  Three tag forms are supported — `<%= expr %>`, `<% stmt %>`, `<%# text %>`.
  **Not a breaking change:** legacy `{{ }}` still renders, with a deprecation
  warning. ([#1727](https://github.com/loco-rs/loco/issues/1727))
- **Environment variables in generated configs are no longer baked in at scaffold
  time.** Because the generator and the runtime shared the same `{{ }}` delimiters,
  several lookups in a newly generated app were evaluated by `loco new` and frozen
  into the file — so `PORT`, `BINDING`, `LOG_LEVEL`, `DB_LOGGING` and `MAILER_HOST`
  silently had no effect at runtime. Only the handful of lookups that were manually
  `{% raw %}`-escaped survived. The two layers now use distinct delimiters, so every
  lookup reaches runtime as intended (and the `{% raw %}` escaping is gone from the
  templates).

- **Generated apps pinned a `loco-rs` that could not read their own config.**
  `LOCO_VERSION` — the version requirement written into every generated
  `Cargo.toml` — still said `1.0`, so a fresh app resolved the newest published
  1.0.x, which renders the new `<%= ... %>` config delimiters literally and then
  fails to parse them: the app compiled and died at boot. The floor now tracks
  the release, enforced by a test.

  The reason it went stale is its own bug: `cargo xtask bump` maintains that
  constant, but its search pattern was pinned to the literal `"0.13"` and a
  pattern that matched nothing was a printed note rather than an error. Every
  release since 0.14 reported success while leaving the floor untouched. A
  no-match is now a failure, and each of the four version sites the tool
  rewrites is covered by a test that it still matches.

- **`config/production.yaml` was generated as a 0-byte file** — copied verbatim
  instead of rendered, since the CLI generator rewrite — and the generated
  `.gitignore` excluded it, so even a correct one would never have reached a
  server. Production is now a real template: backtraces off, `json` logs,
  `0.0.0.0` binding rather than loopback (unreachable from outside a container),
  and a connection pool that isn't the development default of one. The ignore
  rule is gone; secrets live in the environment, so the file is infrastructure
  and belongs in the repository. `local.yaml` stays ignored.

- **`doctor --production` checked the wrong environment and ran fewer checks.**
  The flag never selected an environment — it filtered checks while the config
  under test stayed whatever was ambient, and the default environment is
  `development`. On a server without `LOCO_ENV` set it reported a clean bill of
  health for the development database and never opened the production config.
  It is now a deprecated alias for `--environment production`, and production
  additionally checks settings that are harmless in development and not live: a
  loopback binding, `dangerously_truncate`/`dangerously_recreate`, a queue that
  flushes on startup, backtraces left on.

- **A generated migration that could not be registered reported success.**
  Through rrgen 0.5, a `before:` injection whose anchor line was absent was not
  an error — it rewrote the file unchanged and still printed `injected: …`. A
  `migration/src/lib.rs` without the `inject-above` comment therefore accepted
  the `mod` declaration and silently dropped the `Box::new(..)` registration:
  the migration compiled, never ran, so the table was never created, `db
  entities` correctly wrote nothing, and the first insert 500'd at runtime.

  Fixed at the source in **rrgen 0.6**, which Loco now requires: an injection
  that cannot find its anchor fails, naming the file, the pattern and the
  content it could not place, and the failed generation writes nothing at all —
  so restoring the anchor and re-running does the whole job. This covers every
  injecting generator, not just migrations: controllers, scaffolds, tasks and
  the frontend route table all inject the same way.

  Loco additionally fails generation if any migration in `migration/src/` is
  unregistered. That catches what an injection cannot see: a registration that
  went missing on an earlier run or by hand.

- **Generated apps are now booted, not just compiled, by the test suite.** The
  wizard matrix starts each generated app and requires `/_ping` and `/_health`
  to answer 200, in `development` and again in `production` — the environment
  nothing exercised. The three fixes above all shipped in states that a full
  green suite could not see, because nothing in the repository ever ran the
  artifact a user receives.

- **`loco-gen` no longer depends on Tera.** The dependency was unused; `rrgen`
  carries its own Tera 1, which coexists with Tera 2 without API contact.
- **`loco new --assets serverside --embedded-assets` produced an app that did
  not compile.** The embedded view engine was missing the
  `build_with_post_process` constructor that the generated view-engine
  initializer calls, so the two engines were not interchangeable. Both now
  expose the same constructors, and the wizard test matrix builds this
  combination end to end instead of only asserting on wizard settings.
- **Generated server-side apps now test their own view rendering.** A new
  `tests/views/` case renders the shipped Tera template through the real view
  engine, including the i18n `t()` function, so view-engine regressions surface
  in an app's own test suite rather than at boot.
- **Scaffolding a second resource broke the SPA build.** Every resource's pages
  are named `List`/`New`/`Show`/`Edit`, and the route injection imported them
  bare — so the second `generate scaffold` in an app injected a duplicate
  binding for all four names into `frontend/src/routes.tsx`. Imports are now
  aliased per resource (`List as PostsList`). Existing `routes.tsx` entries are
  your code and are untouched; new scaffolds emit the aliased form.
- **A custom foreign-key column name never reached the scaffold.**
  `user:references:admin_id` names the FK column explicitly and the migration
  honours it, but the DTO and controller derived `user_id` regardless and
  referenced a column the entity does not have.
- **`code:string!^` failed to parse.** Only one of the two flag suffixes was
  stripped, leaving `string!` as the type name and reporting an unknown base
  type the user never wrote. Both flags now parse in any order and in either
  position (`decimal_len!^:8:24` and `decimal_len:8:24^!` are the same column).
  `^` already implies non-null, so the combination is redundant — but it should
  not have been an error.
- **`Pager` could not deserialize its own output.** A serialize-only rename
  emitted `{"results": .., "pagination": ..}` while the deserializer looked for
  `info`, so the derived `Deserialize` — public API — failed with
  `missing field \`info\``. No wire format changed.
- **The legacy config-delimiter deprecation warning was unreachable.** It was a
  `tracing::warn!` emitted during `load_config`, which runs before
  `logger::init`, so no subscriber existed to receive it. It now goes to stderr,
  where config-time diagnostics belong.
- **The generated config shows how to move the JWT out of the header.**
  `auth.jwt.location` (cookie or query parameter) had no example in a generated
  app.
- **`auth.jwt.location` reports what is actually wrong with it.** The setting
  was an `#[serde(untagged)]` enum, so a misspelled `from: cookie`, a `Cookie`
  with no `name`, and a bare scalar all produced the identical `data did not
  match any variant of untagged enum JWTLocationConfig`. The inner error now
  propagates — `unknown variant \`cookie\`, expected one of \`Bearer\`,
  \`Query\`, \`Cookie\``. The accepted YAML and the serialized form are
  unchanged.
- **`generate migration Rename<Old>To<New>On<Table>` now generates a real
  migration** instead of a `todo!()` stub announced as ready to run. Adds
  `loco_rs::schema::rename_column`. When a name genuinely can't be inferred the
  stub remains — silently succeeding would record it as applied — but the
  generator now says it is unimplemented, that `db migrate` will panic, and
  which names it does understand.
- **The starter's tests no longer snapshot whole models.** Adding one column to
  `users` broke five generated tests at once, because they pinned an entire
  `users::Model` `Debug` dump. They snapshot the fields under test now, and
  assert the rest directly.
- **The generated `development.yaml` names a mail catcher and the `PORT`
  override.** The dev mailer targets `localhost:1025` with no `stub`, so mail
  failed unless something was listening and nothing said what; and every app
  defaults to port 5150, so a second one collides.
- **A missing database is documented as the one-way door it is.** `--db none`
  turns off `with-db`, and no generator reverses it; there is now a how-to with
  the exact procedure, and the clientside React/`ts-rs` mode — previously
  undocumented in full — has a guide of its own.