Runnable REST API exercising every micro-lib tier in a containerless setup: N-layer architecture, SQLite persistence, header-based auth simulating Firebase output, and bit-mask RBAC enforcement. What's included: - cmd/todo-api: minimal main delegating to application.Run - internal/application: full object graph wiring — launcher, sqlite, httpserver, httpmw stack, routes in BeforeStart - internal/domain: User entity, ResourceTodos constant, PermReadTodo/PermWriteTodo bit positions - internal/repository: TodoRepository, UserRepository, DBPermissionProvider (SQLite via modernc) - internal/service: TodoService, UserService with interface-based dependencies - internal/handler: TodoHandler, UserHandler using httputil adapters and valid for input validation - internal/middleware: Auth (X-User-ID → rbac.Identity) and Require (bit-mask permission gate) - logAdapter: bridges logz.Logger.With return type to httpmw.Logger interface - SQLite schema: users, user_role (bitmask), todos; migrations run in BeforeStart - Routes: POST /users (open), GET+POST /todos (RBAC), GET /users (RBAC) Tested-via: todo-api POC integration Reviewed-against: docs/adr/
6.4 KiB
6.4 KiB
Changelog
All notable changes to this module will be documented in this file.
The format is based on Keep a Changelog.
[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, withapplicationas the only package that owns the full object graph application.Run()— top-level entry point; wires all dependencies, registers routes insidelc.BeforeStart, and callslc.Run()to start the launcher lifecycle- Local
logAdapterstruct inapplication— bridgeslogz.Logger(whoseWithreturnslogz.Logger) tohttpmw.Logger(whoseWithmust returnhttpmw.Logger), demonstrating the pattern for resolving return-type mismatches between micro-lib interfaces without modifying either library
Domain layer (internal/domain)
Todostruct —ID int64,Title string,Done bool,CreatedAt time.Time, JSON-taggedUserstruct —ID string,Name string,Email string,CreatedAt time.Time, JSON-taggedResourceTodos = "todos"constant — the resource key used in theuser_roletablePermReadTodo rbac.Permission = 0andPermWriteTodo rbac.Permission = 1— bit positions for the todos resource permission mask
Repository layer (internal/repository)
TodoRepositoryinterface —FindAll(ctx) ([]domain.Todo, error),Create(ctx, domain.Todo) (domain.Todo, error)UserRepositoryinterface —FindAll,FindByID,Create,SetPermissions(ctx, userID, resource string, mask rbac.PermissionMask) errorNewTodoRepository(db sqlite.Client) TodoRepository— SQLite-backed implementation;FindAllreturns an empty slice (never nil) when no rows exist;CreateusesRETURNINGto retrieve the generated ID and timestampNewUserRepository(db sqlite.Client) UserRepository— SQLite-backed implementation;FindByIDreturns axerrors.ErrNotFound-wrapped error onsql.ErrNoRows;SetPermissionsusesINSERT ... ON CONFLICT DO UPDATEfor upsertDBPermissionProviderstruct implementingrbac.PermissionProvider— resolves the integer bitmask fromuser_rolefor a given(uid, resource)pair; returns mask0(no permissions) for a missing row rather than an errorNewPermissionProvider(db sqlite.Client) *DBPermissionProvider
Service layer (internal/service)
CreateTodoRequeststruct —Title stringwithvalidate:"required,min=1,max=255"tagTodoServiceinterface —FindAll(ctx) ([]domain.Todo, error),Create(ctx, CreateTodoRequest) (domain.Todo, error)NewTodoService(repo repository.TodoRepository) TodoServiceCreateUserRequeststruct —Name string,Email string,CanRead bool,CanWrite bool, with validation tags;CanRead/CanWriteseed the permission bitmask forResourceTodosat creation timeUserServiceinterface —FindAll(ctx) ([]domain.User, error),Create(ctx, CreateUserRequest) (domain.User, error)NewUserService(repo repository.UserRepository, idGen func() string) UserService—idGenis injected (e.g.uuid.NewString) to mint user IDs
Handler layer (internal/handler)
TodoHandler—NewTodoHandler(svc service.TodoService, v valid.Validator) *TodoHandler;FindAlldelegates tohttputil.HandleNoBody[[]domain.Todo];Createdelegates tohttputil.Handle[CreateTodoRequest, domain.Todo]UserHandler—NewUserHandler(svc service.UserService, v valid.Validator) *UserHandler;FindAllandCreatefollow the samehttputiladapter pattern
Middleware layer (internal/middleware)
Auth(userRepo repository.UserRepository) func(http.Handler) http.Handler— readsX-User-IDheader, looks up the user, and stores anrbac.Identityin context viarbac.SetInContext; returns 401 JSON on missing header or unknown user ID; the output contract is identical to whathttpauth-firebase.EnrichmentMiddlewareproduces, so swapping in real Firebase auth requires no changes to handlers or servicesRequire(provider rbac.PermissionProvider, resource string, perms ...rbac.Permission) func(http.Handler) http.Handler— readsrbac.Identityfrom 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
launcherorchestratingsqliteandhttpserverthroughOnInit → OnStart → OnStophttpserverwithRequestID → Recover → RequestLoggermiddleware viahttpmwsqlite(modernc pure-Go, no CGO) as the persistence layer with inline schema migrations run inlc.BeforeStartrbacidentity and permission types as the shared contract across middleware, repository, and domainvalidfor request body validation in handlershttputilhandler adapters (Handle,HandleNoBody) for all route handlersxerrorsErrNotFoundfor typed not-found errors in the repository layer- Route registration in
lc.BeforeStartso 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— requiresAuthmiddleware andPermReadTodopermissionPOST /todos— requiresAuthmiddleware andPermWriteTodopermissionGET /users— requiresAuthmiddleware andPermReadTodopermission on the todos resource
Design Notes
- Header-based auth (
X-User-ID) deliberately mirrors therbac.Identityoutput contract ofhttpauth-firebase, proving that the RBAC contract is provider-agnostic; the POC can be upgraded to real Firebase auth by replacing theAuthmiddleware with no changes to any other layer - SQLite with no Docker dependency keeps the POC self-contained: a single
go run ./cmd/todo-apiis sufficient to run the full stack, with the database created astodos.dbin the working directory on first run - Routes are registered in
lc.BeforeStartrather than at construction time, making the lifecycle ordering explicit and auditable in a single closure: migrations run afterdb.OnInit, routes register after migrations, andsrv.OnStartbinds the port last