Architecture
High-Level Topology
โโโโโโโโโโโโโโโโโโโโโโโ
โ CLI / API client โ
โ (Cobra / grpc, โ
โ Connect, or REST) โ
โโโโโโโโโโฌโโโโโโโโโโโโโ
โ HTTP/1.1, HTTP/2 (h2c), or gRPC
โโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ
โ Traefik Proxy โ
โ :8080 (http) / :9000 (grpc)โ
โ Routes by path prefix โ
โโโโโโโโฌโโโโโโโโโโโโโโโโโโโฌโโโ
โ โ
โโโโโโโโโโโโโโผโโโโโ โโโโโโโโโโผโโโโโโโโโโโโโโโโโ
โ Pogona API :9000โ โ Ibis Service :9000 โ
โ cmd/api/main.go โโโโบโ (external service) โ
โโโโโโโโโโฌโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโผโโโโโโโโโ
โ PostgreSQL :5432โ
โโโโโโโโโโโโโโโโโโโ
There is currently no web UI. A go-app/WebAssembly UI (cmd/ui, lib/ui) previously existed but was removed (feat(BVR-2): remove scaffolded UI dirs (lib/ui, cmd/ui)); a rewrite is being tracked separately on the rewrite-ui branch and isn’t part of main. go.mod still lists github.com/maxence-charriere/go-app/v10 as a direct dependency, but nothing in the current tree imports it โ it’s a leftover that hasn’t been tidied up yet. The only shipped entrypoints today are the API server and the CLI.
Binary Entrypoints
cmd/api/main.go โ API Server
Starts an HTTP/2 server on :9000 using h2c. Registers three ConnectRPC service handlers:
| Handler | Mount path prefix |
|---|---|
ItemServiceHandler | /schemas.pogona.services.v1.ItemService/ |
SceneServiceHandler | /schemas.pogona.services.v1.SceneService/ |
GameServiceHandler | /schemas.pogona.services.v1.GameService/ |
CORS middleware wraps each handler using connectrpc.com/cors + github.com/rs/cors. Every request is wrapped in a logging middleware (method, path, OTel trace/span IDs) and otelhttp. telemetry.Init (from github.com/holmes89/archaea/telemetry) sets up OpenTelemetry and is a graceful noop when OTEL_EXPORTER_OTLP_ENDPOINT is unset.
Two plain HTTP endpoints are also registered outside of ConnectRPC:
GET /healthโ returns{"status":"ok"}GET /infoโ returns{"service","commit","build_time"}, populated via-ldflags -X main.commit=... -X main.buildTime=...at build time
The GameService additionally takes a GamePersistence interface, implemented by lib/clients/ibis/persistance.go’s Client.Save, which is called synchronously whenever a game is created or updated.
main.go + cmd/ โ CLI
Cobra CLI dialing localhost:8080 over insecure gRPC (cmd/app.go). Exposes list, get, create subcommands for game, scene, item, inventory (under the item service), and option. All handlers currently just fmt.Println(res) the raw proto response โ no formatted/table output.
There is no cmd/ui โ the UI binary entrypoint was removed along with the WASM frontend (see above). Note: a stray, pre-built ui binary (compiled for macOS arm64) is still checked into the repo root from that earlier era; it is not produced by anything in this codebase and is not referenced by the Makefile or Dockerfile.
Layer Structure
lib/
clients/ibis/ โ Ibis persistence client
pogona/
game/ โ Game domain (service, interface, mocks)
game/grpc/ โ Connect-RPC adapter
item/ โ Item domain + grpc/ (also backs Inventory RPCs)
scene/ โ Scene domain + grpc/ (also backs ConditionalDescription RPCs)
repo/
repository.go โ Generic Repository[T] interface (Create/Update/List/Get/Delete/Close)
local/ โ BoltDB implementation (not wired in production)
sql/local/ โ PostgreSQL implementation + embedded migrations
schemas/
pogona/v1/ โ Generated proto types + Connect stubs
ibis/v1/ โ Generated Ibis proto types + Connect stubs
There is currently no lib/ui directory (see above).
Database Layer
Production: PostgreSQL (lib/repo/sql/local/)
local.Conn wraps database/sql. On startup, NewDatabase runs goose.Up from embedded SQL migration files (lib/repo/sql/local/migrations/: game, item, scene). Queries are built with Masterminds/squirrel.
All three entity repos are methods on *Conn, so the single *Conn value satisfies GameRepository, ItemRepository, and SceneRepository simultaneously.
Scene’s conditional_descriptions and options columns are stored as JSON blobs. item.game_id and scene.game_id are ON DELETE CASCADE foreign keys to game.uuid; game.start_scene is a plain UUID column with no FK constraint.
The generic Repository[T] interface (lib/repo/repository.go) does define Delete, and the SQL/BoltDB repos implement it โ but none of the domain service interfaces (GameService, ItemService, SceneService) or their ConnectRPC handlers expose a delete RPC, so delete is not reachable through the API today.
Alternative: BoltDB (lib/repo/local/)
A generic, embedded key-value store backed by go.etcd.io/bbolt. Implements the same repository interfaces but is not wired in production.
Ibis Integration
When a game is created/updated, the API:
- Persists the game record to PostgreSQL
- Fetches all items and scenes for that game
- Marshals them into a
GameFileproto message (schemas/pogona/v1/game_file.proto: uuid, updated_at,Gamemetadata, repeatedItem, repeatedScene) as proto-binary - Calls
ibis.Client.Save, which POSTs to Ibis’sCreateGameRPC (servicesv1connect.GameServiceClient) with anibis.v1.Game{Uuid, CreatedAt, Title, Path, Platform: "pogona"}record plus the binary payload
ibis.Client.Save currently only logs on error (log.Print) โ it does not return an error or fail the enclosing game create/update.
Ibis is a separate, external service (../ibis in the sibling checkout referenced by docker-compose.yaml) that stores game metadata in its own PostgreSQL schema and the binary blob in an S3-compatible bucket (MinIO in local/dev compose). Pogona talks to Ibis directly over IBIS_ENDPOINT, not through Traefik.
There is no direct integration with “Goblin Shark” in this codebase. A local, uncommitted discovery note (docs/discovery/scene-builder.md) sketches a possible future tile-based scene builder that references goblin-shark as an example downstream game consumer and a sibling harpy-eagle tileset service, but none of that is implemented โ it’s exploratory design, not shipped architecture.
Traefik Configuration
Dynamic configuration in traefik.d/:
service-pogona.yamlโ RoutesPathPrefix(/schemas.pogona.services.v1)โh2c://pogona:9000service-ibis.yamlโ Defines anibisservice (h2c://ibis:9000); its router is commented out, so Traefik does not currently proxy to Ibis directlyfile-server.yamlโ RoutesPathPrefix(/nes)to annginx-backed static file server
Entry points (traefik.yaml): http on :8080, grpc on :9000, prom on :9090.
Unimplemented Areas
- No delete RPC exposed anywhere in the API (see Database Layer above) โ the repository layer supports it, nothing above it does
ListInventorys,GetInventory,CreateInventory(onItemService) โpanic("not implemented")ListConditionalDescriptions,GetConditionalDescription,CreateConditionalDescription(onSceneService) โpanic("not implemented")- CLI output โ all commands print raw proto structs (
fmt.Println(res)) - Web UI โ removed from
main; rewrite in progress on a separate branch