# Upgrading to v0.5

Source: https://docs.anetos.dev/upgrade/v0.5/

v0.5 is the first public release. It renames things so the whole
framework uses one vocabulary, and most old names keep working, with a
deprecation, until v0.6. A project made by `anetos new` builds
unchanged and gets warnings that say what to change; code that calls
the constructors from explicit parts (mostly tests) needs the renames
below.

1. Update the modules: `go get anetos.dev/anetos@v0.5.0`, and the tool
   (`go get -tool anetos.dev/anetos/cli/cmd/anetos@v0.5.0`), `admin`
   and the drivers you use to their v0.5.0; then `go mod tidy`.
2. Fix what `go vet ./...` reports (the renamed constructors, below),
   then run `go fix ./...`, `go tool anetos generate` and
   `go tool anetos agent:install` (the files coding agents read).
3. Run `go tool anetos check`, and read the changes below that apply.

## Services are built with New

The services an app builds from its settings in `setup` are now made by
their package's `New`, the word Go uses for it; `ForApp` is gone from the
docs and stays only as a deprecated name until v0.6. `db.Connect` and
`redis.Connect` keep their names: they connect to a server.

| v0.4 | v0.5 |
|---|---|
| `cache.ForApp(app)`, `queue.ForApp(app)`, `session.ForApp(app)`… | `cache.New(app)`, `queue.New(app)`, `session.New(app)`… |
| `openapi.ForApp(app, srv, cfg)` | `openapi.Register(app, srv, cfg)` |

With Go 1.26 or later, `go fix ./...` rewrites these calls for you:

```sh
go fix ./...
```

It leaves `auth.ForApp` and `social.ForApp` alone (Go's inliner doesn't
yet rewrite calls to generic functions): change those two to `auth.New`
and `social.New` by hand.

`New` was already the name of each package's constructor from explicit
parts, without the app, which apps mostly use in tests. Those are
renamed, and `go fix` can't rewrite them: the compiler points at each
call.

| v0.4 | v0.5 |
|---|---|
| `cache.New(store, prefix)` | `cache.NewWithStore(store, prefix)` |
| `queue.New(store, cfg, …)` | `queue.NewWithStore(store, cfg, …)` |
| `pubsub.New(broker, …)` | `pubsub.NewWithBroker(broker, …)` |
| `mailer.New(transport, …)` | `mailer.NewWithTransport(transport, …)` |
| `storage.New(disk, …)` | `storage.NewWithDisks(disk, …)` |
| `ai.New(provider, …)` | `ai.NewWithProvider(provider, …)` |
| `events.New(…)` | `events.NewBus(…)` |
| `schedule.New(…)` | `schedule.NewScheduler(…)` |
| `i18n.New(cfg, …)` | `i18n.NewTranslator(cfg, …)` |
| `rbac.New(perms, roles…)` | `rbac.NewRegistry(perms, roles…)` |
| `encryption.New(key, previous…)` | `encryption.NewEncrypter(key, previous…)` |
| `auth.New(cfg, users, enc, …)` | `auth.NewWithConfig(cfg, users, enc, …)` |
| `social.New(a, resolve, url, creds, providers, …)` | `social.NewWithConfig(…)` |
| `session.New()` (an empty session, in tests) | `session.NewSession()` |

`go fix` needs code that compiles, tests included: rename the calls
`go vet ./...` reports first, then run `go fix ./...`. A project made by `anetos new`
calls only `ForApp`, so `go fix` and the two `auth`/`social` edits do
it.

A second `New` (or `db.Connect`) for the same app is now an error
(`cache: New called twice for one app`) where some packages used to open
a second connection or replace the first service. Code that needs a
service built elsewhere asks the app for it:
`anetos.Resolve[*cache.Cache](app)`. `encryption.New(app)` returns the
same encrypter every time.

## Settings with one name per idea

Every setting now starts with its area, and the one that picks a
backend is `<AREA>_DRIVER`. The former names are still read, with a
warning at startup and in `go run . doctor`, until v0.6:

| Before v0.5 | Since |
|---|---|
| `DB_CONNECTION` | `DB_DRIVER` |
| `DB_DATABASE` | `DB_NAME` |
| `DB_USERNAME` | `DB_USER` |
| `MIGRATE_ON_RUN` | `DB_MIGRATE_ON_START` |
| `MIGRATE_READINESS` | `DB_MIGRATE_READINESS` |
| `SEARCH_LANGUAGE`, `SEARCH_RANKING` | `DB_SEARCH_LANGUAGE`, `DB_SEARCH_RANKING` |
| `CACHE_STORE` | `CACHE_DRIVER` |
| `SESSION_LIFETIME`, `SESSION_MAX_LIFETIME` | `SESSION_TTL`, `SESSION_MAX_TTL` |
| `AUTH_REMEMBER_LIFETIME` | `AUTH_REMEMBER_TTL` |
| `QUEUE_POLL` | `QUEUE_POLL_INTERVAL` |
| `LOCALE_URL` | `APP_LOCALE_STRATEGY` |

Rename them in your `.env` files, the server's environment and the
deploy files (`deploy/production.env.example`, the systemd unit, the
Dockerfile, a Render or Fly file). In the project's folder:

```sh
for f in .env .env.example .env.testing Dockerfile deploy/*; do
  [ -f "$f" ] && sed -i.bak \
    -e 's/\bDB_CONNECTION\b/DB_DRIVER/g' -e 's/\bDB_DATABASE\b/DB_NAME/g' \
    -e 's/\bDB_USERNAME\b/DB_USER/g' -e 's/\bCACHE_STORE\b/CACHE_DRIVER/g' \
    -e 's/\bMIGRATE_ON_RUN\b/DB_MIGRATE_ON_START/g' \
    -e 's/\bMIGRATE_READINESS\b/DB_MIGRATE_READINESS/g' \
    -e 's/\bSEARCH_LANGUAGE\b/DB_SEARCH_LANGUAGE/g' \
    -e 's/\bSEARCH_RANKING\b/DB_SEARCH_RANKING/g' \
    -e 's/\bSESSION_MAX_LIFETIME\b/SESSION_MAX_TTL/g' \
    -e 's/\bSESSION_LIFETIME\b/SESSION_TTL/g' \
    -e 's/\bAUTH_REMEMBER_LIFETIME\b/AUTH_REMEMBER_TTL/g' \
    -e 's/\bQUEUE_POLL\b/QUEUE_POLL_INTERVAL/g' \
    -e 's/\bLOCALE_URL\b/APP_LOCALE_STRATEGY/g' "$f"
done
```

(`\b` needs GNU sed; on macOS, `brew install gnu-sed` and use `gsed`,
or rename them in your editor. `_` counts as a letter for `\b`, so
`QUEUE_POLL_INTERVAL` and `DB_SEARCH_LANGUAGE` aren't renamed twice.) The
image's `HTTP_ADDR=:8080` line can go too: the port is the default, and
a platform's `PORT` is used when `HTTP_ADDR` isn't set. The
fields of the config structs follow: `db.Config`'s `Connection`,
`Database` and `Username` are `Driver`, `Name` and `User`;
`cache.Config.Store` is `Driver`; `session.Config`'s `Lifetime` and
`MaxLifetime` are `TTL` and `MaxTTL`; `auth.Config.RememberLifetime` is
`RememberTTL`; `queue.Config.Poll` is `PollInterval`;
`migrate.Config.OnRun` is `OnStart`; `i18n.Config.URL` is `Strategy`, and
`i18n.URLNone`, `URLPrefix` and `URLSubdomain` are `StrategyNone`,
`StrategyPrefix` and `StrategySubdomain` (`go fix` rewrites those). Apps
rarely set these fields: the compiler points at any that do.

`go run . doctor` also points at a key in your `.env` files that is
another framework's name for a setting (`QUEUE_CONNECTION`,
`MAIL_MAILER`), a former name left beside its new one, or a typo of a
setting.

`app.Source()` now returns the app's own source, which records the keys
read: code that type-asserted it (to a `config.Map`, in a test) should
pass the map to `anetos.WithSource` and keep it instead.

## Logging in

The framework says "log in" and "log out" everywhere now, in its API,
the pages `make:auth` writes and the messages, where it used to say
"sign in" in some places. The old names stay, deprecated, until v0.6.

| v0.4 | v0.5 |
|---|---|
| `a.SignIn(ctx, u, remember)` | `a.Login(ctx, u, remember)` (see below) |
| `a.SignOutOthers(ctx, u)`, `a.SignOutEverywhere(ctx, u)` | `a.LogoutOthers(ctx, u)`, `a.LogoutEverywhere(ctx, u)` |
| `a.CanTwoFactor()`, `a.CanRemember()`, `a.CanSignOutEverywhere()` | `a.SupportsTwoFactor()`, `a.SupportsRemember()`, `a.SupportsLogoutEverywhere()` |
| `a.ActAs(ctx, id)`, `auth.ActAs(ctx, id)`, `auth.ActOption` | `a.WithUser(ctx, id)`, `auth.WithUser(ctx, id)`, `auth.UserOption` |
| `auth.ErrNoPendingSignIn`, `auth.ErrNoUser` | `auth.ErrNoPendingLogin`, `auth.ErrUserNotFound` |
| `*social.ErrNoAccount` | `*social.NoAccountError` |
| `app.SocialSignIn(redirect, account)` (anetostest) | `app.SocialLogin(redirect, account)` |

`go fix ./...` rewrites `auth.ActAs`, `auth.ActOption`,
`social.ErrNoAccount` and `app.SocialSignIn`. It doesn't rewrite the
methods of `*auth.Auth[U]` (a generic type, which Go's inliner leaves
alone so far) or the error variables; the linters flag them. In the
project's folder:

```sh
grep -rlE --include='*.go' 'SignIn\(|SignOut(Others|Everywhere)|Can(TwoFactor|Remember|SignOutEverywhere)\(|\.ActAs\(|ErrNoPendingSignIn|ErrNoUser\b' . |
  xargs -r sed -i.bak -E \
    -e 's/\.SignIn\(/.Login(/g' \
    -e 's/\.CanSignOutEverywhere\(/.SupportsLogoutEverywhere(/g' \
    -e 's/\.SignOut(Others|Everywhere)\(/.Logout\1(/g' \
    -e 's/\.Can(TwoFactor|Remember)\(/.Supports\1(/g' -e 's/\.ActAs\(/.WithUser(/g' \
    -e 's/ErrNoPendingSignIn/ErrNoPendingLogin/g' -e 's/ErrNoUser\b/ErrUserNotFound/g'
find . -name '*.go.bak' -delete   # once the diff looks right
```

(GNU sed and xargs; on macOS, `gsed` and `gxargs`.) Its `\.SignIn\(` also
matches a method of your own called `SignIn`: check the diff.

**`Login` now asks for the two-factor code.** For a user with two-factor
authentication on, `a.Login` returns `auth.ErrTwoFactorRequired` and the
login waits for a code at `AUTH_CHALLENGE_URL`, as `SignIn` did; it used
to log them in without one. The pages `make:auth` wrote call `Login`
only after registration, when no one has the code on, so they behave as
before. If your code calls `Login` for a user who may have it on, handle
the error as the login page does, or send them to the challenge page. A
user without a password who logs in through `Login` counts as having
confirmed their password lately, as with `SignIn`.

**The admin** says "Impersonate" where it said "Act as user", and
"Log out everywhere" where it said "Sign out everywhere"; that action's
URL is `…/actions/logout` (was `…/actions/sign-out`), and the audit log
records it as `user.logged_out_everywhere` (was
`user.signed_out_everywhere`): search for both in older entries.

**Messages.** Errors that tests compare by text say "log in" and
"impersonate" too: `auth: not logged in` (`ErrUnauthenticated`, was `auth:
not signed in`), `auth: user not found` (was `auth: no such user`),
`auth: no login waits for a code`, `auth: two-factor authentication is
on already` and `isn't on` (`ErrTwoFactorOn`, `ErrTwoFactorOff`), `auth:
not impersonating another user` (`ErrNotImpersonating`). The framework's messages (`i18n/locales/en.yaml`, the
`social_*` errors) say "log in". In a project, `make:auth`'s pages and
`locales/*/auth.yaml` are yours: they keep working as they are. To match
v0.5's pages and the translations of
[anetos.dev/locales](https://github.com/anetos-dev/locales), whose keys
changed, rename three keys, in the YAML files and the code that uses
them:

```sh
grep -rlE 'sign_in_(with|again|first)' app views locales |
  xargs -r sed -i.bak -E 's/sign_in_(with|again|first)/login_\1/g'
find app views locales -name '*.bak' -delete   # once the diff looks right
```

(`locales/` is embedded whole, `.bak` files included, so delete them.)

and replace "Sign in", "sign in" and "two-factor sign-in" in your pages
and `locales/en/auth.yaml` as you like. An API project's
`handlers.SignInResponse` is `LoginResponse` in new projects; yours can
keep its name.

## Commands and the project

**Command names.** The app's and the `anetos` tool's commands use one
form, `<group>:<action>` with a singular group, and an existing tool's
name where one has the command (`route:list`, `queue:work`). The old names still run, with a warning, until
v0.6: change them in scripts, CI and deploy files. (`gen` runs without
one: names may now be shortened while they stay unique, and `gen` is
`generate` shortened.)

| Before v0.5 | Since |
|---|---|
| `./app routes:list` | `./app route:list` |
| `./app plugins:list`, `./app plugins:env` | `./app plugin:list`, `./app plugin:env` |
| `./app lang:check` | `./app locale:check` |
| `./app schedule:run <task>` | `./app schedule:test <task>` |
| `go tool anetos gen [-check]` | `go tool anetos generate [--check]` |
| `go tool anetos lang:add`, `add lang` | `go tool anetos locale:add` |
| `go tool anetos make:admin:resource` | `go tool anetos make:admin-resource` |

`schedule:run` without a task is new: it runs the tasks due this minute,
once, for a crontab entry or a Kubernetes CronJob in place of a
long-running scheduler. `queue:work` and `schedule:work` are new names
for `run --only=worker` and `run --only=scheduler`. An app whose own
command has one of the new names (`queue:work`, `schedule:test`,
`schedule:work`, or a former name such as `routes:list`) now fails to
register it: rename yours.

`anetos generate` writes `Code generated by anetos generate` at the top
of `models_gen.go`: run it once after upgrading the tool, or
`generate --check` (in CI) reports the files as out of date. Change
`//go:generate go tool anetos gen` in `main.go` to `generate` (the old
line still works).

**`key:generate` writes `.env`.** It sets `APP_KEY` there when it is
missing or empty, and prints what it did on stderr, so the old
`go tool anetos key:generate >> .env` still works. A key that is set is
kept unless `--force`, which moves it to `APP_PREVIOUS_KEYS`. To print a
key for a server's environment, use `--show`:
`APP_KEY=$(go tool anetos key:generate --show | cut -d= -f2-)`.

**Route names.** `make:crud` names a resource's routes after its
handlers: `posts.create` and `posts.delete`, where it wrote
`posts.store` and `posts.destroy`, and `make:auth` names the token
routes `tokens.create` and `tokens.delete`. Routes you generated keep
their names: they are your code. Rename them only with every
`web.URL`, `web.MustURL` and `web.RouteIs` call that uses them; in an
API project, the route name is the OpenAPI operation ID, which clients
generated from `openapi.json` may use. The admin's own routes changed
the same way: its form is `admin.<resource>.new` (it was `.create`),
saving is `.create` (was `.store`), deleting `.delete` (was `.destroy`),
and its password confirmation's form posts to `admin.confirm.check`
(was `admin.confirm.store`). A link of yours to `admin.<resource>.create`
still resolves, to the list's URL: point it at `.new`.

**`make:auth`'s group of logged-in routes** is `loggedIn` in new
projects, in both stacks (it was `members` in a web project and `me` in
an API one). Yours keeps its name; the guides say `loggedIn`.

**`public/` is the web root.** New web projects serve the files of
`public/` at `/` (`public/robots.txt` at `/robots.txt`,
`.well-known/` too), after the routes; `public/static/` stays at
`/assets/`. In a project made before v0.5, embed the folder and add the
route:

```go
// illustrative
// public/public.go: replace "//go:embed static" and "var files embed.FS" with
//go:embed *
var Files embed.FS
// and use Files where it used files (fs.Sub(Files, "static")).

// routes/web.go, after the /assets route:
r.Static("/", public.Files)
```

`//go:embed *` embeds every file of `public/`, the top-level dotfiles
too (not served), and fails to build when a folder of it holds only
hidden files (a `uploads/.gitkeep`): give it a file, or keep it out of
`public/`.

**Commands that describe the app.** Every app has `about` and
`security:check`, and `migrate.New` adds `db:schema`: an app's own
command by one of these names is now refused at startup ("taken"); rename it. `route:list`,
`migrate:status`, `schedule:list`, `plugin:list` and `help` take
`--json`. `web.RouteInfo.Middleware` now has an entry for every
middleware of the route, each with its `Name`, not only those that
called `web.Documented`: code that counted its entries, or read the
first one as the first documented middleware, filters on `Security` or
`Responses` instead. `web.RouteInfo` also has `File`, `Line` and
`Public`: a test that compares `Routes()` with `reflect.DeepEqual`
clears `File` and `Line` first. `anetos.Finding` has a `Rule`, so
unkeyed literals (`anetos.Finding{anetos.Warning, "…"}`, which `go vet`
warns about) need the field names.

## Names across the API

The rest of the API now uses the words Go and web developers already
know. The old names stay, deprecated, until v0.6, and `go fix ./...` (Go 1.26 or
later) rewrites the calls to them:

| Before v0.5 | Since |
|---|---|
| `auth.DefaultHomeURL`, `mailer.BaseURL`, `mailer.DefaultFrom`, `i18n.WarnMissing` | `auth.WithDefaultHomeURL`, `mailer.WithAppURL`, `mailer.WithDefaultFrom`, `i18n.WithWarnMissing` |
| `storage.BaseURL`, `storage.Public`, `storage.SignWith` | `storage.WithBaseURL`, `storage.WithPublic`, `storage.WithSigner` |
| `admin.Title`, `admin.UserName` | `admin.WithDefaultTitle`, `admin.WithUserName` |
| `web.TrustedOrigins`, `gcs.ClientOptions`, `s3.Transport` | `web.WithTrustedOrigins`, `gcs.WithClientOptions`, `s3.WithTransport` |
| `postmark.BaseURL`, `HTTPClient`, `Stream` | `postmark.WithBaseURL`, `WithHTTPClient`, `WithStream` |
| `events.BusOption`, `audit.Option` | `events.Option`, `audit.TrackOption` |
| `session.NewContext`, `web.RouteFromContext`, `web.ClientIPFrom` | `session.WithSession`, `web.CurrentRoute`, `web.ClientIP` |
| `anetos.Unit`, `UnitFunc`, `app.StartUnit`, `AroundUnits`, `HasAroundUnits` | `anetos.Operation`, `OperationFunc`, `app.StartOperation`, `AroundOperations`, `HasAroundOperations` |
| `anetos.Roles`, `app.Supervisor().Roles()` | `anetos.ProcessTypes`, `ProcessTypes()` |
| `db.Untracked`, `db.TxWith`, `db.PruneTrashed`, `page.HasMore()` | `db.AllowRepeatedQueries`, `db.TxWithOptions`, `db.Prunable`, `page.HasNext()` |
| `t.UniqueLive(…)`, `.UniqueLive()` (migrations) | `UniqueWithoutTrashed` |
| `pubsub.MaxAttempts`, `cache.Forget` | `pubsub.Tries`, `cache.Delete` |
| `c.Events()`, `web.View(…)`, `s.String(key)` (a session) | `c.EventStream()`, `web.Render(…)`, `s.GetString(key)` |
| `r.HandleStd(method, pattern, h)` | `r.Get(pattern, web.WrapHandler(h))` (`go fix` writes `r.Handle(method, pattern, web.WrapHandler(h))`) |
| `ai.Func(name, description, fn)` | `ai.NewTool(name, description, fn)` |
| `admin.Field[T](title, column)` | `admin.TextColumn[T](column, label)`: the arguments the other way round: the column first, then its label |

`go fix` leaves calls whose type arguments Go infers, `ai.Func(…)` and
`admin.UserName(fn)`, and `page.HasMore()` (a method of a generic
type): rename those by hand. The validation rule `unique_live` is
`unique_without_trashed`: change it in your `validate` tags (the old
name works, with a warning, until v0.6), and rename a translation of
its message (`validation.unique_live`) with it.

**Interfaces you implement.** A user model's
`CommunicationLocale() string` (`i18n.CommunicationPreference`) is
`PreferredMailLocale() string` (`i18n.MailLocalePreference`), as
`PreferredLocale` and `PreferredTimeZone`; a storage backend's
`SignedURL` is `TemporaryURL` (`storage.TemporaryURLBackend`), as
`Disk.TemporaryURL`, and `storagetest.Features.SignedURLs` is
`TemporaryURLs`. The old methods are still used, with a warning, until
v0.6: rename them.

**Process types.** What `run --only` selects is a *process type*, the
word of Heroku's `Procfile`; "role" is left to roles and permissions.
Three changed names:

| Before v0.5 | Since |
|---|---|
| `run --only=http` | `run --only=web` (`serve` is unchanged) |
| `run --only=workers` | `run --only=worker` |
| `run --only=listeners` | `run --only=listener` |

`scheduler` stays. The old names still work, with a warning in the log,
until v0.6: change them in your `Dockerfile`, `fly.toml`, `Procfile`,
Compose and Kubernetes files, and your own components'
`anetos.ProcessTypes("workers")`. The supervisor's log line says
`process_types=` (it said `roles=`).

**Changes the compiler points at.** These have no deprecated copy:

- `web.ClientIP(r)` takes the request's context: `web.ClientIP(r.Context())`,
  or `web.ClientIP(c)` in a handler. Outside a `web.Router` (a handler
  of your own wrapping it), it returns "" without `RealIP`: use
  `r.RemoteAddr` there.
- `storage.From(ctx)` returns the default disk; a named one is
  `storage.DiskFrom(ctx, "avatars")` (was `storage.From(ctx, "avatars")`).
- An admin resource's fields take the usual admin-panel names:
  `PluralLabel` (was `Title`), `Label` (was `Singular`) and `RecordTitle` (was the function
  `Label`); a column's, action's, bulk action's, filter's and dashboard
  link's `Title` is `Label`.
- `db.Chunk.Model` is `EmbeddingModel`, `db.RepeatedQuery.Unit` is
  `Operation`, `schedule.Config.Timezone` is `TimeZone`, and
  `supervisor.Spec.Roles` and `ComponentStatus.Roles` are
  `ProcessTypes`.
- `gcppubsub.Driver(opts...)` takes its own options:
  `gcppubsub.Driver(gcppubsub.WithClientOptions(opts...))`. An `s3.Option`
  no longer changes the MinIO client's options: `s3.WithTransport` is
  the one there is.
- If you register a listener's options in a variable typed
  `events.Option`, it is now `events.ListenOption`.

The repeated-query warning's attribute is `operation=` (it was
`unit=`): update log queries that look for it.

## Shapes that changed

These changes have no deprecated copy: the compiler points at each.

| Before v0.5 | Since |
|---|---|
| `ai.Embed(ctx, 0, texts...)`, `ai.Embed(ctx, 768, texts...)` | `ai.Embed(ctx, texts)`, `ai.Embed(ctx, texts, ai.Dimensions(768))` |
| `ai.EmbedQuery(ctx, dims, text)` | `ai.EmbedQuery(ctx, text)` (with `ai.Dimensions(dims)` if it wasn't 0) |
| `ai.ConstraintKeywords` | `ai.ConstraintKeywords()` |
| `d.Watch(table, w, n)` | `d.Watch(table, w, db.WatchBulkValues(n))` |
| `ai.Request.Options` (`any`, in a provider of your own) | a `[]any`: range over it and take the values of your types |

`ai.Embed` and `EmbedQuery` now apply `ai.Using`, `ai.ForUser` and
`ai.Timeout`, which they ignored, so `AI_TIMEOUT` bounds each request
for embeddings too. In tests, `app.AI().Embeddings()` is
`EmbedRequests()` (`go fix` rewrites it). `ai.ProviderOptions` adds to the
call's options rather than replacing them, so the options of several
drivers can travel together. In the gemini driver, `Options.Config` is
`Params` (the old field works until v0.6).

**Behavior.** `r.As("api.")` returns a router of its own, as `Group`
does, and leaves `r`'s route names alone: code that called `As` on a
router and then registered on that same router (not on `As`'s result)
must use the result. `HTTPError.Wrap` returns a copy instead of
changing the error (and the copy still matches it with `errors.Is`).
A plugin's `Listen(bus)` is `Listeners(bus)` (`Listen` is still called,
with a warning, until v0.6, but a `var _ ext.HasListeners =
(*Plugin)(nil)` check stops compiling until you rename it). A
`web.CSRFOption` of your own, written against
`*http.CrossOriginProtection`, no longer compiles: use
`web.WithTrustedOrigins`.

**Storage backends of your own** with temporary URLs
(`storage.TemporaryURLBackend`) must have their store serve HTML, SVG
and JavaScript as attachments (`Content-Disposition: attachment`, set
when `Put` stores such a file), as the s3 and gcs drivers do;
`storagetest` with `TemporaryURLs` now checks it.

**Removed** (nothing an app was meant to use): `db.WithTestTx`,
`db.SQLiteTimeFormat`, `db.SearchIndexesTable`, `db.Plural`,
`migrate.Runner.Command` and `migrate.Commands` (run `./app migrate`
and the other commands through `app.Execute`), the database drivers'
`DSN`, `storage.DiskSource`, `storage.AppConfig`, `ai.SearchResult`,
`ext.Info` and `postmark.Event`. `db.Dialect` can no longer be
implemented outside package db.

## Generated pages call views/ui

Since v0.5, `anetos new` writes `views/ui`, a package of components
(`ui.Card`, `ui.Field`, `ui.Button`…) that carries the markup and the
CSS framework's classes, and the layout and the pages that `make:crud` and
`make:auth` write call it instead of writing class names ([Style your
app](https://docs.anetos.dev/guides/styling/index.md)).

**A project made before v0.5** has no `views/ui`, and nothing changes
until a generator runs. The first `make:crud` or `make:auth` writes
`views/ui` before its pages, and says so: the starter theme's
components when `public/static/app.css` has the theme's `.card` rule,
else plain HTML without classes. Your stylesheet, layout and other
pages are left as they are, and the generators still find the header's
nav (`</nav>`) to add their links.

To build the layout from the components too (optional), compare
`views/layout.templ` with a new project's (`anetos new` in another
folder):

1. In `<head>`, replace the stylesheet's `<link>` with `@ui.Head()`.
2. Replace `<header class="topbar">…</header>` with
   `@ui.Header("Name", web.MustURL(ctx, "home")) { … }`, its nav with
   `@ui.Nav(label) { … }`, and keep `@AccountMenu()` after the nav's
   block.
3. Make `navLink` call
   `@ui.NavLink(web.MustURL(ctx, route), label, web.RouteIs(…))`.
4. Replace `<main class="container">` with `@ui.Main() { … }`, the
   flash's `<p class="flash">` with `@ui.Flash(ui.Success) { … }`, and
   the footer with `@ui.Footer() { … }`.

The theme's stylesheet from a new project styles `.flash` neutral and
adds `.flash.success` and `.info`: copy it too, or add those rules, if
your layout or pages use `ui.Flash`.

The generator also writes `views/ui/css.json`, which records the
components it wrote. Once the layout calls the components,
`anetos css:use <framework>` can switch the project to another CSS framework ([Style
your app](https://docs.anetos.dev/guides/styling/index.md#8-switch-css-frameworks)); it asks for `--force`
the first time, since your `app.css` isn't the one anetos wrote. It adds the menu
button's name, `nav.menu`, to `locales/en/app.yaml`.

## --css=none writes plain HTML

`anetos new --css=none` used to keep the starter theme's class names in
the markup, with an empty stylesheet. Its `views/ui` now writes plain
HTML without classes: add your own classes in `views/ui`'s components.
Projects made before are unchanged.
