Skip to content
v0.5 isn't released yet: these pages follow the main branch. To try it, install the release candidate, v0.5.0-rc.1.
Upgrading to v0.5

Upgrading to v0.5

v0.5 is in progress; this page lists its changes so far.

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.4v0.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:

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.4v0.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.5Since
DB_CONNECTIONDB_DRIVER
DB_DATABASEDB_NAME
DB_USERNAMEDB_USER
MIGRATE_ON_RUNDB_MIGRATE_ON_START
MIGRATE_READINESSDB_MIGRATE_READINESS
SEARCH_LANGUAGE, SEARCH_RANKINGDB_SEARCH_LANGUAGE, DB_SEARCH_RANKING
CACHE_STORECACHE_DRIVER
SESSION_LIFETIME, SESSION_MAX_LIFETIMESESSION_TTL, SESSION_MAX_TTL
AUTH_REMEMBER_LIFETIMEAUTH_REMEMBER_TTL
QUEUE_POLLQUEUE_POLL_INTERVAL
LOCALE_URLAPP_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:

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.4v0.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.ActOptiona.WithUser(ctx, id), auth.WithUser(ctx, id), auth.UserOption
auth.ErrNoPendingSignIn, auth.ErrNoUserauth.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:

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, whose keys changed, rename three keys, in the YAML files and the code that uses them:

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.5Since
./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 langgo tool anetos locale:add
go tool anetos make:admin:resourcego 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:

// 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.5Since
auth.DefaultHomeURL, mailer.BaseURL, mailer.DefaultFrom, i18n.WarnMissingauth.WithDefaultHomeURL, mailer.WithAppURL, mailer.WithDefaultFrom, i18n.WithWarnMissing
storage.BaseURL, storage.Public, storage.SignWithstorage.WithBaseURL, storage.WithPublic, storage.WithSigner
admin.Title, admin.UserNameadmin.WithDefaultTitle, admin.WithUserName
web.TrustedOrigins, gcs.ClientOptions, s3.Transportweb.WithTrustedOrigins, gcs.WithClientOptions, s3.WithTransport
postmark.BaseURL, HTTPClient, Streampostmark.WithBaseURL, WithHTTPClient, WithStream
events.BusOption, audit.Optionevents.Option, audit.TrackOption
session.NewContext, web.RouteFromContext, web.ClientIPFromsession.WithSession, web.CurrentRoute, web.ClientIP
anetos.Unit, UnitFunc, app.StartUnit, AroundUnits, HasAroundUnitsanetos.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.Forgetpubsub.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.5Since
run --only=httprun --only=web (serve is unchanged)
run --only=workersrun --only=worker
run --only=listenersrun --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.5Since
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.ConstraintKeywordsai.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).

Behaviour. 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).

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); 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.