Skip to content
Go 1.27What changed

Start a Go API with auth, events and an outbox in place

Two services in one module. A token service with OAuth2, PKCE and JWKS, and an event-sourced user service that sends mail through a transactional outbox. Memory, MySQL or MongoDB, and one command to run it all.

gh repo create my-api --template vardius/go-api-boilerplate --private --clone
internal/user/domain/user.go
// ChangeEmail moves the account to the address to.
func (u *User) ChangeEmail(to Email) error {
switch {
case !u.registered():
return errNotRegistered
case to.IsZero():
return apperr.Invalid("email", "is required")
case to == u.email:
return ErrSameEmail
}
return es.Raise(u, EmailChanged{Old: u.email, New: to})
}
// Apply implements es.Aggregate.
func (u *User) Apply(event any) error {
switch e := event.(type) {
case Registered:
u.id, u.email = e.UserID, e.Email
case EmailChanged:
u.email = e.New
case GoogleLinked:
u.googleSubject = e.Subject
default:
return fmt.Errorf("user: can't apply %T", event)
}
return nil
}

Features

Event-sourced user service

Users are rebuilt from their own event stream. Event names are versioned, old shapes are upcast, and every append checks the stream’s version.

Your own token service

OAuth2 with PKCE S256, client credentials and refresh-token rotation with reuse detection. ES256 access tokens, verified with the keys it publishes as a JWKS.

Transactional outbox

Emails are queued in the same transaction as the change that causes them, then delivered at least once with backoff.

Three stores, one contract

Memory, MySQL and MongoDB pass the same contract suite. A build tag picks one per binary.

One command to run

docker compose up --wait starts both services and the web app on localhost:8080, and Mailpit on localhost:8025. No cluster, no hosts file.

OpenTelemetry

JSON logs with trace and request IDs. Traces and metrics go out over OTLP once you set an endpoint.

Ready for AI agents

An AGENTS.md in every area, written code standards, and checks that enforce the rules agents tend to break.

Architecture

A browser talks to one origin. Behind it, auth owns tokens and user owns users, and neither calls the other on a normal request.

How go-api-boilerplate fits togetherA browser and third-party OAuth apps reach the system through one web gateway on port 8080, which serves the web app and forwards /api/auth/ to the auth service and /api/user/ to the user service. Auth calls user's LoginService over Connect to redeem login codes; user fetches auth's JWKS to verify access tokens. Each service keeps its own store, memory, MySQL or MongoDB. The user service's outbox relay sends mail to Mailpit locally, or to any SMTP server.BrowserOAuth appweb gateway :8080the app, /api/auth/ and /api/user/authsessions, OAuth2and JWKSuserevent-sourcedlogin codesConnectJWKSauth storeclients, grantsuser storeevents, usersand outboxoutbox relayMailpitor your SMTPOne store per service:memory, MySQL or MongoDB
  1. One origin. The gateway serves the web app and forwards /api/auth/ and/api/user/, so cookies stay SameSite and nothing needs CORS.
  2. auth issues every token and sets the only cookies. Clients and grants are plain rows: short-lived secrets don't belong in a permanent log.
  3. user is event-sourced. A change appends events, updates the users projection and queues its emails in one transaction.
  4. No call per request. Each API verifies access tokens itself with auth's JWKS, cached.
  5. One internal RPC. auth redeems login codes with user over Connect, on a listener the gateway doesn't expose.
  6. Mail at least once. The outbox relay retries with backoff. Locally, Mailpit catches every message.

Run it on your machine in five minutes

Create your repository from the template, start it with Docker Compose, and sign in with a link from Mailpit.