# todo-api POC application demonstrating the full micro-lib stack: N-layer architecture, SQLite persistence, header-based auth, and RBAC permission enforcement. ## Purpose This is not a library. It is a runnable REST API that exercises every micro-lib tier in a realistic but containerless setup. The goal is to validate that the micro-lib modules compose correctly, that the RBAC contract between `httpmw.Auth`-equivalent middleware and `rbac.PermissionMask` works end-to-end, and that the launcher lifecycle manages database and HTTP server startup cleanly. ## Tier & Dependencies **Tier 5** (application). Imports: - `code.nochebuena.dev/go/launcher` — lifecycle orchestration - `code.nochebuena.dev/go/httpserver` — HTTP server (Tier 4) - `code.nochebuena.dev/go/httpmw` — request ID, recovery, request logger middleware - `code.nochebuena.dev/go/logz` — structured logger - `code.nochebuena.dev/go/sqlite` — SQLite client (Tier 1) - `code.nochebuena.dev/go/rbac` — identity and permission types - `code.nochebuena.dev/go/valid` — input validation - `code.nochebuena.dev/go/httputil` — handler adapters - `code.nochebuena.dev/go/xerrors` — typed errors (NotFound) - `github.com/go-chi/chi/v5` — used in application for route grouping - `github.com/google/uuid` — user ID generation ## Key Design Decisions - **N-layer architecture** (ADR-001): `cmd → application → handler → service → repository → domain`. Each layer depends only on its inner neighbours via interfaces. `application` is the only package that knows the full object graph. - **Header-based auth** (ADR-002): `middleware.Auth` reads `X-User-ID`, looks up the user, and puts an `rbac.Identity` in context — identical output contract to `httpauth-firebase`. Swapping in real Firebase auth requires only replacing the middleware. - **Domain-owned permission bits** (ADR-003): `domain.PermReadTodo = 0` and `domain.PermWriteTodo = 1` define bit positions. `domain.ResourceTodos = "todos"` is the key in `user_role`. All layers import these constants from `domain`. - **Routes registered in BeforeStart**: Route registration happens in `lc.BeforeStart(...)` so SQLite tables exist before handlers are reachable. This is the correct place in the launcher lifecycle — after `db.OnInit` migrates the schema, before `srv.OnStart` binds the port. - **logAdapter**: `logz.Logger.With(...)` returns `logz.Logger`; `httpmw.Logger.With(...)` must return `httpmw.Logger`. A local `logAdapter` struct in `application` bridges this mismatch without requiring either library to change. - **SQLite — no container required**: The database file is `todos.db` in the working directory. Local development and testing require no Docker, no Postgres, no network access. ## Patterns **Layer interfaces** (defined at consumption site in Go convention): ``` repository.TodoRepository — consumed by service repository.UserRepository — consumed by service and middleware rbac.PermissionProvider — consumed by middleware.Require service.TodoService — consumed by handler service.UserService — consumed by handler ``` **Full middleware + route wiring in BeforeStart:** ```go lc.BeforeStart(func() error { // run migrations db.GetExecutor(ctx).ExecContext(ctx, migrate) // open endpoint srv.Post("/users", userH.Create) // auth-gated group srv.Group(func(r chi.Router) { r.Use(appMW.Auth(userRepo)) r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermReadTodo)). Get("/todos", todoH.FindAll) r.With(appMW.Require(permProvider, domain.ResourceTodos, domain.PermWriteTodo)). Post("/todos", todoH.Create) }) return nil }) ``` **Creating a user with permissions (curl):** ```sh # Create a user with read+write on todos curl -X POST http://localhost:3000/users \ -H 'Content-Type: application/json' \ -d '{"name":"Alice","email":"alice@example.com","can_read":true,"can_write":true}' # Use the returned ID to authenticate curl http://localhost:3000/todos -H 'X-User-ID: ' ``` **Database schema:** ```sql CREATE TABLE users ( id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE user_role ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, resource TEXT NOT NULL, permissions INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (user_id, resource) ); CREATE TABLE todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done BOOLEAN NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); ``` ## What to Avoid - Do not add business logic to `application`. It is a wiring package only. - Do not import `application` from any `internal/` sub-package. The dependency arrow points into `application`, never out. - Do not define permission constants in `repository` or `middleware`. They belong in `domain` (ADR-003). - Do not use the header auth pattern in production. `X-User-ID` is unauthenticated. Replace with `httpauth-firebase` or equivalent before any real deployment. - Do not reuse or reassign existing permission bit positions. Append new bits at the next available position. - Do not run migrations outside of `BeforeStart`. Running them in `OnInit` would execute before other components are ready; running them in `OnStart` races with route availability. - Do not add telemetry imports to this POC unless demonstrating the telemetry module specifically. The OTel no-op default applies — adding `telemetry.New` is a one-line addition if needed. ## Testing Notes - There are no tests in this POC currently. The structure is designed for testability: each layer depends only on interfaces, so handlers, services, and repositories can be tested with mock implementations. - Service-layer tests: implement `repository.TodoRepository` / `repository.UserRepository` as in-memory stubs, construct the service, and call methods directly. - Handler-layer tests: implement `service.TodoService` as a stub, use `httptest.NewRequest` + `httptest.NewRecorder` with the handler's `ServeHTTP`. - Middleware tests: use `httptest.NewRequest`, set `X-User-ID`, and verify that `rbac.Identity` is present in context after `Auth` runs. - Integration tests (optional): start a real SQLite in-memory database (`sqlite.Config{Path: ":memory:"}`), run migrations, and exercise the full stack.