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:

HandlerMount 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:

  1. Persists the game record to PostgreSQL
  2. Fetches all items and scenes for that game
  3. Marshals them into a GameFile proto message (schemas/pogona/v1/game_file.proto: uuid, updated_at, Game metadata, repeated Item, repeated Scene) as proto-binary
  4. Calls ibis.Client.Save, which POSTs to Ibis’s CreateGame RPC (servicesv1connect.GameServiceClient) with an ibis.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 โ€” Routes PathPrefix(/schemas.pogona.services.v1) โ†’ h2c://pogona:9000
  • service-ibis.yaml โ€” Defines an ibis service (h2c://ibis:9000); its router is commented out, so Traefik does not currently proxy to Ibis directly
  • file-server.yaml โ€” Routes PathPrefix(/nes) to an nginx-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 (on ItemService) โ€” panic("not implemented")
  • ListConditionalDescriptions, GetConditionalDescription, CreateConditionalDescription (on SceneService) โ€” 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