Skip to content

Migrating from the 2022 layout

The rewrite replaces the API outright. There is no compatibility layer: the old API was built around generic command dispatch, and that shape is what made authorization easy to get wrong. This page maps each old endpoint and concept to its replacement, so you can move a client or a fork across.

The old code is tagged legacy-2022, with the October 2026 security fixes applied; see the changelog. Some reasons below describe the code before those fixes.

2022 Now Why
POST /users/v1/dispatch/user/user-register-with-email POST /api/user/v1/login-links The account is created when the code is redeemed, so typing an address creates nothing.
POST /users/v1/dispatch/user/user-request-access-token POST /api/user/v1/login-links, then POST /api/auth/v1/sessions with the code The emailed link carried the 365-day token itself. It now carries a single-use, 15-minute code.
POST /users/v1/dispatch/user/user-change-email-address POST /api/user/v1/me/email-change, then .../email-change/confirm The new route acts on the caller, never on an ID from the body, and changes nothing until the new address confirms.
POST /users/v1/dispatch/user/user-register-with-google GET /api/user/v1/google/start Google sign-in runs only through the OIDC callback, where Google vouches for the email.
POST /users/v1/dispatch/user/user-register-with-facebook removed Facebook login is dropped: it isn’t OIDC and adds nothing to the example.
POST /users/v1/google, POST /users/v1/google/callback GET /api/user/v1/google/start, GET /api/user/v1/google/callback OIDC with PKCE, email_verified required. The old callbacks were POST-only and couldn’t complete.
POST /users/v1/facebook, POST /users/v1/facebook/callback removed As above.
GET /users/v1/me GET /api/user/v1/me Unchanged in purpose; needs user:read.
GET /users/v1/ removed There is no user list. An admin listing, if you add one, needs its own scope.
GET /users/v1/{id} removed Use /me. A user reads only their own account.
GET /health, GET /readiness GET /livez, GET /readyz Readiness now turns 503 while a service drains.
2022 Now Why
GET and POST /auth/v1/authorize GET and POST /api/auth/oauth/authorize Adds a consent screen and requires PKCE S256. Consent used to be silent.
POST /auth/v1/token POST /api/auth/oauth/token Authorization code with PKCE, client credentials and refresh only. The password grant is gone.
POST /auth/v1/dispatch/client/client-create-credentials POST /api/auth/v1/clients The secret is shown once and stored as an argon2id hash, not kept in plaintext events.
POST /auth/v1/dispatch/client/client-remove-credentials DELETE /api/auth/v1/clients/{id} Deleting a client now revokes its grants.
POST /auth/v1/dispatch/token/token-create removed Tokens come only from a session or the token endpoint.
POST /auth/v1/dispatch/token/token-remove DELETE /api/auth/v1/sessions/current, DELETE /api/auth/v1/sessions/{id}, POST /api/auth/oauth/revoke Sign out, revoke a device, or revoke an app’s token.
GET /auth/v1/clients GET /api/auth/v1/clients Only the caller’s clients, never a secret, paginated with a ceiling.
GET /auth/v1/clients/{clientID} removed Use the list.
GET /auth/v1/clients/{clientID}/tokens removed Access tokens are JWTs and aren’t stored.
GET /auth/v1/users/{userID}/tokens GET /api/auth/v1/sessions Sessions replace stored login tokens. The list shows the caller’s own devices, with no token material.
2022 Now Why
UserService.DispatchUserCommand removed Each change has its own route and use case instead of generic dispatch.
UserService.GetUser, UserService.ListUsers removed Nothing needs them: each API reads its own data.
AuthenticationService.ValidationBearerToken removed Each API verifies tokens itself with auth’s JWKS, with no call per request.
AuthenticationService.DispatchClientCommand, DispatchTokenCommand removed Clients and sessions have explicit REST routes.
user.v1.LoginService/RedeemLoginCode, in proto/user/v1/login.proto The one internal call, from auth only, with a scoped token.
2022 Now Why
HS512 JWT signed with AUTH_SECRET or USER_SECRET ES256 JWT signed in internal/auth/keys, private key in auth only, public keys at GET /api/auth/.well-known/jwks.json With a shared secret, every service that can verify a token can also forge one.
One 365-day token with every permission 15-minute access token and a 30-day refresh token, rotated with reuse detection at POST /api/auth/v1/sessions/refresh and POST /api/auth/oauth/token A leaked token is worth minutes, not a year.
?authToken= query parameter Authorization: Bearer header or the access_token cookie, read by internal/platform/authn Query strings end up in logs and browser history.
at cookie, readable from JavaScript, not cleared on logout access_token and refresh_token cookies, HttpOnly, set by POST /api/auth/v1/sessions and cleared by DELETE /api/auth/v1/sessions/current A script on the page can’t read them.
Permissions from the client’s registered scopes, scope all Scopes user:read, user:write, clients:read, clients:write in internal/auth/app/scopes.go, granted at consent There is no catch-all scope.
2022 Now Why
pkg/ internal/platform/ Nothing imported it, and internal stops forks depending on a kit that isn’t a stable API.
cmd/<service>/internal/{application,domain,infrastructure,interfaces} internal/user/{domain,app,store,httpapi,mail,rpc}, internal/auth/{app,keys,store,httpapi}, and a thin cmd/user/main.go and cmd/auth/main.go Grouped by what the code is about.
Command bus and command handlers Use-case methods in internal/user/app A bus hid the call graph and once crashed the process on a cancelled request. A method call does neither.
Event bus and async event handlers Projections in the append transaction (internal/platform/es/repository.go), side effects through internal/platform/outbox Async handlers dropped errors and let read models drift for good.
Event types named by their Go type Registered, versioned names such as user.registered.v1, with upcasts: internal/platform/es/registry.go, and the names in internal/user/domain/events.go Renaming a type no longer orphans stored events.
No version check on append es.ErrConcurrency in internal/platform/es/es.go, a 409 Two writers could both append the same version.
pkg/eventstore/{memory,mysql,mongo} internal/platform/{memdb,mysql,mongodb}, behind one contract suite in internal/platform/es/estest The MySQL event store couldn’t write or read. A shared suite holds every backend to the same rules.
pkg/errors with stack traces internal/platform/apperr kinds and fmt.Errorf("…: %w") Nobody read the stacks. Kinds decide the status code.
{"code", "message"} error bodies RFC 9457 problem details, written by internal/platform/httpx/errors.go A standard shape with a request ID, and no internals in 5xx bodies.
golog, trace, expvar and the debug adapter log/slog and OpenTelemetry, set up in internal/platform/obs Standard tools, off by default where they cost anything.
gocontainer, message-bus, pubsub, pushpull, shutdown, gollback the standard library, internal/platform/run for shutdown, or removed Each has a standard replacement, or the redesign removed its job.
Build tags inside packages Store build tags only in cmd/user/store_*.go and cmd/auth/store_*.go Packages compile the same way for every store.
cmd/migrate and an init container <service> migrate, run by the Helm hook in deploy/helm/go-api-boilerplate/templates/migrate.yaml, or MIGRATE_ON_START in compose.mysql.yaml and compose.mongodb.yaml One image, one schema source.
Terraform, Telepresence, /etc/hosts aliases, a self-signed TLS setup compose.yaml on localhost, and one Helm chart in deploy/helm/go-api-boilerplate No cluster or hosts edits needed to run it.
maildev Mailpit, the mailpit service in compose.yaml
cmd/web web/: Vite, React 19, strict TypeScript Rebuilt small, with cookies only and no tokens in JavaScript.
go-oauth2/oauth2 A small OAuth2 server in internal/auth/app Three grants are small enough to own, and the library had no refresh-token reuse detection.
Ingress v1beta1 Gateway API HTTPRoute, Ingress optional, in deploy/helm/go-api-boilerplate/templates/routes.yaml v1beta1 was removed in Kubernetes 1.22.