# Changelog All notable changes to this module will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.9.0-poc] - 2026-03-18 ### Added This entry describes what the POC demonstrates, not a versioned library API surface. **Application structure** - N-layer architecture: `cmd/todo-api` → `internal/application` (wiring only) → `internal/handler` → `internal/service` → `internal/repository` → `internal/domain`; each layer depends on its inner neighbours via interfaces, with `application` as the only package that owns the full object graph - `application.Run()` — top-level entry point; wires all dependencies, registers routes inside `lc.BeforeStart`, and calls `lc.Run()` to start the launcher lifecycle - Local `logAdapter` struct in `application` — bridges `logz.Logger` (whose `With` returns `logz.Logger`) to `httpmw.Logger` (whose `With` must return `httpmw.Logger`), demonstrating the pattern for resolving return-type mismatches between micro-lib interfaces without modifying either library **Domain layer** (`internal/domain`) - `Todo` struct — `ID int64`, `Title string`, `Done bool`, `CreatedAt time.Time`, JSON-tagged - `User` struct — `ID string`, `Name string`, `Email string`, `CreatedAt time.Time`, JSON-tagged - `ResourceTodos = "todos"` constant — the resource key used in the `user_role` table - `PermReadTodo rbac.Permission = 0` and `PermWriteTodo rbac.Permission = 1` — bit positions for the todos resource permission mask **Repository layer** (`internal/repository`) - `TodoRepository` interface — `FindAll(ctx) ([]domain.Todo, error)`, `Create(ctx, domain.Todo) (domain.Todo, error)` - `UserRepository` interface — `FindAll`, `FindByID`, `Create`, `SetPermissions(ctx, userID, resource string, mask rbac.PermissionMask) error` - `NewTodoRepository(db sqlite.Client) TodoRepository` — SQLite-backed implementation; `FindAll` returns an empty slice (never nil) when no rows exist; `Create` uses `RETURNING` to retrieve the generated ID and timestamp - `NewUserRepository(db sqlite.Client) UserRepository` — SQLite-backed implementation; `FindByID` returns a `xerrors.ErrNotFound`-wrapped error on `sql.ErrNoRows`; `SetPermissions` uses `INSERT ... ON CONFLICT DO UPDATE` for upsert - `DBPermissionProvider` struct implementing `rbac.PermissionProvider` — resolves the integer bitmask from `user_role` for a given `(uid, resource)` pair; returns mask `0` (no permissions) for a missing row rather than an error - `NewPermissionProvider(db sqlite.Client) *DBPermissionProvider` **Service layer** (`internal/service`) - `CreateTodoRequest` struct — `Title string` with `validate:"required,min=1,max=255"` tag - `TodoService` interface — `FindAll(ctx) ([]domain.Todo, error)`, `Create(ctx, CreateTodoRequest) (domain.Todo, error)` - `NewTodoService(repo repository.TodoRepository) TodoService` - `CreateUserRequest` struct — `Name string`, `Email string`, `CanRead bool`, `CanWrite bool`, with validation tags; `CanRead`/`CanWrite` seed the permission bitmask for `ResourceTodos` at creation time - `UserService` interface — `FindAll(ctx) ([]domain.User, error)`, `Create(ctx, CreateUserRequest) (domain.User, error)` - `NewUserService(repo repository.UserRepository, idGen func() string) UserService` — `idGen` is injected (e.g. `uuid.NewString`) to mint user IDs **Handler layer** (`internal/handler`) - `TodoHandler` — `NewTodoHandler(svc service.TodoService, v valid.Validator) *TodoHandler`; `FindAll` delegates to `httputil.HandleNoBody[[]domain.Todo]`; `Create` delegates to `httputil.Handle[CreateTodoRequest, domain.Todo]` - `UserHandler` — `NewUserHandler(svc service.UserService, v valid.Validator) *UserHandler`; `FindAll` and `Create` follow the same `httputil` adapter pattern **Middleware layer** (`internal/middleware`) - `Auth(userRepo repository.UserRepository) func(http.Handler) http.Handler` — reads `X-User-ID` header, looks up the user, and stores an `rbac.Identity` in context via `rbac.SetInContext`; returns 401 JSON on missing header or unknown user ID; the output contract is identical to what `httpauth-firebase.EnrichmentMiddleware` produces, so swapping in real Firebase auth requires no changes to handlers or services - `Require(provider rbac.PermissionProvider, resource string, perms ...rbac.Permission) func(http.Handler) http.Handler` — reads `rbac.Identity` from context, resolves the permission mask, and enforces each required bit; returns 403 JSON on missing identity or insufficient permissions; returns 500 JSON if the provider errors **Micro-lib stack composition demonstrated** - `launcher` orchestrating `sqlite` and `httpserver` through `OnInit → OnStart → OnStop` - `httpserver` with `RequestID → Recover → RequestLogger` middleware via `httpmw` - `sqlite` (modernc pure-Go, no CGO) as the persistence layer with inline schema migrations run in `lc.BeforeStart` - `rbac` identity and permission types as the shared contract across middleware, repository, and domain - `valid` for request body validation in handlers - `httputil` handler adapters (`Handle`, `HandleNoBody`) for all route handlers - `xerrors` `ErrNotFound` for typed not-found errors in the repository layer - Route registration in `lc.BeforeStart` so migrations complete before any route is reachable and the port is not bound until routes are registered **Routes** - `POST /users` — open; no auth required (user bootstrapping) - `GET /todos` — requires `Auth` middleware and `PermReadTodo` permission - `POST /todos` — requires `Auth` middleware and `PermWriteTodo` permission - `GET /users` — requires `Auth` middleware and `PermReadTodo` permission on the todos resource ### Design Notes - Header-based auth (`X-User-ID`) deliberately mirrors the `rbac.Identity` output contract of `httpauth-firebase`, proving that the RBAC contract is provider-agnostic; the POC can be upgraded to real Firebase auth by replacing the `Auth` middleware with no changes to any other layer - SQLite with no Docker dependency keeps the POC self-contained: a single `go run ./cmd/todo-api` is sufficient to run the full stack, with the database created as `todos.db` in the working directory on first run - Routes are registered in `lc.BeforeStart` rather than at construction time, making the lifecycle ordering explicit and auditable in a single closure: migrations run after `db.OnInit`, routes register after migrations, and `srv.OnStart` binds the port last