Skip to content

mdhender/ecv4

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

75 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ecv4 — Go Game Server

A contract-first REST API for an experimental Go game web server. Players use the API to write their own clients, so the API contract is the product — there is no official frontend. api/openapi.yaml is the source of truth for the transport contract; the Go server is generated from it and implements the behavior behind it.

The stack is deliberately boring and inspectable:

  • Go standard-library net/http (Go 1.22+ pattern routing)
  • OpenAPI 3.0.3 as the public API contract
  • oapi-codegen for generated DTOs and a strict server interface
  • JWT Bearer authentication, with application-level authorization in Go
  • SQLite (pure-Go zombiezen.com/go/sqlite) with append-only migrations

RPC, gRPC, Connect, and GraphQL are intentionally out of scope. REST is preferred because players can exercise it with common HTTP tools and write clients in any language — Go, Python, JavaScript, shell, a spreadsheet — without adopting an RPC toolchain. The contract and its examples should be clear enough to authenticate, manage games and rosters, and fetch turns without reading server source.

Status

Authentication, account administration, and game management — creating games, their lifecycle (draft → recruiting → active → paused → complete → archived), and roster/membership management — are implemented and in the MVP. The game engine (turns, order validation, and submission) is intentionally still stubbed: those routes answer 501 Not Implemented. The OpenAPI spec's info.description walks through the create → recruit → activate workflow, and the game-management authorization model (roles, status chain, visibility) is documented in CLAUDE.md.

Agents working in this repo should also read CLAUDE.md, which covers the architecture internals and coding conventions in more depth.

Layout

api/openapi.yaml         Public API contract (source of truth)
api/oapi-codegen.yaml    oapi-codegen configuration
internal/api/            Generated code only (do not hand-edit)
internal/handlers/       Adapts HTTP/API DTOs to services; implements the strict server
internal/auth/           JWT issue/verify + bearer middleware
internal/store/          Typed data-access layer over the SQLite pool
internal/database/       Database create/open + append-only migrations
internal/cli/            game-server command tree (serve + database/account/game verbs)
cmd/game-server/         Thin process shell over internal/cli
cmd/earl/                curl-like smoke-testing client for a running server
docs/                    curl examples, MVP status, background notes

Build and run

make install-tools   # one-time: go install oapi-codegen
make generate        # regenerate internal/api/openapi.gen.go from the spec
make test            # go test ./...
make build           # build to bin/game-server
make run             # run the server

The server listens on :9987 by default. For a live-rebuild dev loop, run air (config in .air.toml), which also serves on :9987.

curl http://localhost:9987/healthz
curl http://localhost:9987/openapi.yaml

See docs/curl-examples.md for login and authenticated request examples.

CLI

cmd/game-server is an ff-based command tree. With no subcommand it runs the server. Subcommands:

game-server version                          # print the version
game-server database create <PATH>           # create ecv4.db in an existing dir (or :memory: to verify migrations)
game-server database account create --email <e> [--is-admin] [--is-inactive] [--secret <s>]
game-server database account update --email <e> [--is-active[=false]] [--is-admin[=false]] [--secret <s> | --generate-secret]
game-server database account reset-password --email <e> [--secret <s> | --generate-secret]   # generates one if omitted
game-server database account list            # print all accounts (id, active, admin, email); read-only, no server needed
game-server database game create --code <CODE> --name <name> [--description <text>]          # create a game (draft, active)
game-server database game list               # print all games incl. hidden (id, active, status, code, name); read-only
game-server database game add-member --code <CODE> --email <e> [--handle <h>] [--is-gm]      # seed a roster member
game-server database game assign-gm --code <CODE> --email <e> [--handle <h>]                 # add a GM (alias for add-member --is-gm)

The database game verbs are an offline admin bootstrap — they seed games and rosters directly against the database file with no running server and no authorization gate (only store-level integrity is enforced), the direct-DB analog of database account create.

The shared --development flag enables the POST /admin/shutdown route when serving and seeds a known admin when used with database create. The separate --allow-openapi-docs flag serves the embedded Swagger UI at /docs when serving.

Configuration

Config comes from flags or ECV4_-prefixed environment variables. .env files load before flags are parsed, selected by ECV4_ENV (default development). Key variables:

  • ECV4_DB_DIR — directory holding ecv4.db
  • ECV4_JWT_SECRET — HMAC signing key, must be ≥32 bytes for HS256. Required when ECV4_ENV=production — startup fails if it is unset there. In any other environment an unset secret yields a random ephemeral one that invalidates all tokens on restart.
  • ECV4_DEVELOPMENT, ECV4_DEVELOPMENT_ADMIN_EMAIL, ECV4_DEVELOPMENT_ADMIN_SECRET — control development mode and the optional seeded admin.

Development loop

The OpenAPI file is edited first, then code is regenerated to match:

$EDITOR api/openapi.yaml     # 1. change the contract (keep operationId names stable)
make generate                # 2. regenerate transport code
go test ./...                # 3. fix handler compile errors and implement behavior
                             # 4. commit the contract and generated code together

Committing generated code alongside the spec keeps API diffs easy to review.

Auth model

The contract declares Bearer JWT auth (Authorization: Bearer <token>), enforced by middleware that verifies the token and attaches claims to the request context. The spec's security requirements are the single source of truth for which routes need a token; public routes opt out with security: [].

Authentication proves who you are; authorization is object-level and lives in handlers/services, not in generated code or the middleware:

  • Can this user see this game?
  • Can this user submit orders for this faction?
  • Can this GM close this turn?

Access tokens are short-lived (15m); refresh tokens (24h) are persisted, rotated on /auth/refresh, and revoked on /auth/logout. Presenting an already-rotated refresh token revokes the whole token family as a theft signal.

License

See LICENSE. Copyright © 2026 Michael D Henderson.

About

Go API server (RESTish, OpenAPI, contract driven)

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages