Rewrite API in Go #87

Merged
Doridian merged 1 commit from doridian/golang-rewrite into main 2026-09-29 16:05:30 -07:00 AGit
Owner

Rewrites the Elixir/Phoenix API in Go. The goal is a drop-in replacement: same routes, same JSON shapes, same auth, same database.

Layout / build

Follows the current SpaceAge/TTS and FoxDen Go conventions:

  • cmd/space_age_api, util/info.go (gitrev via ldflags), internal/api, internal/db
  • buildGoModule with vendor-hash.txt (+ .doc for auto-nix-hash), maid tasks/go renovate preset
  • managed-go.yml replaces elixir-lint.yml (golangci-lint, build, test)
  • The binary is still bin/space_age_api, so the existing NixOS module keeps working unchanged. start runs the server; stop/restart are accepted as no-ops for the current ExecStop/ExecReload. It reads the same env vars: SECRET_KEY_BASE, PORT, MYSQL_SOCKET, MYSQL_USERNAME, MYSQL_PASSWORD, MYSQL_DATABASE, POOL_SIZE.

Compatibility

  • Database: no migration needed. Migrations are embedded SQL files with the same versions as the Ecto ones and share Ecto's schema_migrations table, so on the existing DB space_age_api migrate is a no-op. On a fresh MariaDB, SHOW CREATE TABLE of every table is byte-identical to what Ecto produces. Migrations don't run on start unless AUTO_MIGRATE is set, same as the old release.
  • JWTs: HS256 with SECRET_KEY_BASE, iss=Joken, same aud values and lifetimes. Tokens issued by the old API are accepted by the new one and the reverse, so a deploy doesn't log anyone out. Unit tests use real Joken-signed fixture tokens.
  • Input handling: follows Ecto's cast/validate rules as the Lua client relies on them. Partial upserts keep existing fields, blank strings fall back to schema defaults, floats are truncated on player/server upserts, "1"/"true" count as booleans, and errors come back as {"errors": {"field": ["is invalid" | "can't be blank"]}}.
  • HTTP details: response keys are alphabetical like Jason. Also matched: status codes, {} 404s, the ErrorView bodies, Cache-Control per route, Client-Identity, CORS headers and preflight, text responses without a Content-Type, and the steam:// redirect.

Intentional differences

All of these were 500s (crashes) in the old API:

  • POST /v2/rabbitmq/user queried servers.authkey, which doesn't exist (Ecto.QueryError on every call). It now checks server_configs name + authkey, which is what the STOMP central login (server name + serverToken) needs.
  • A cast error on servers.players crashed while formatting the error (to_string on the {:array, :string} type). It now returns a normal 400.
  • An Authorization header with no token, or Discord requests with missing or non-hex signature headers, now get 401 instead of a 500.
  • GMod serializes empty tables either way, so an empty [] is accepted for map fields (research, station_storage) and an empty {} for players. Previously these rejected the whole save.
  • /salink for a Discord account already linked to another SteamID now moves the link. Before, it hit the unique index and 500'd.
  • PUT /v2/servers/self ignores a name in the body and always uses the authenticated server. The client never sends one.
  • create_servers uses ENGINE = INNODB instead of MEMORY. The original migration can't run on MariaDB or MySQL at all (MEMORY doesn't support JSON/TEXT columns; checked with the old release too). This only affects fresh installs, since existing DBs already have the version recorded.

Testing

  • Differential test: the old release (built from main) and the new binary ran side by side against identical MariaDB 11.8 databases, and a script replayed ~110 requests shaped like the Lua client's (players, JWTs in both directions, applications, bans, servers, factions, RabbitMQ, Discord rejection paths, CORS, and error cases). Result: 0 unexpected differences; the only differences are the ones listed above. Final DB contents matched except the one row from the intentional players: {} leniency.
  • Discord /salink link, move, unlink and invalid-code flows were tested end to end against the Go build with a test key.
  • go test ./..., golangci-lint (0 issues), nix build, nixfmt-tree --ci.

Deploying

Nothing is required. As an optional follow-up in FoxDen/core, spaceage/api.nix can drop ExecStop/ExecReload and add ExecStartPre = "... migrate" if you want migrations applied automatically.

🤖 Generated with Claude Code

Rewrites the Elixir/Phoenix API in Go. The goal is a drop-in replacement: same routes, same JSON shapes, same auth, same database. ## Layout / build Follows the current SpaceAge/TTS and FoxDen Go conventions: - `cmd/space_age_api`, `util/info.go` (gitrev via ldflags), `internal/api`, `internal/db` - `buildGoModule` with `vendor-hash.txt` (+ `.doc` for auto-nix-hash), maid `tasks/go` renovate preset - `managed-go.yml` replaces `elixir-lint.yml` (golangci-lint, build, test) - The binary is still `bin/space_age_api`, so the existing NixOS module keeps working unchanged. `start` runs the server; `stop`/`restart` are accepted as no-ops for the current `ExecStop`/`ExecReload`. It reads the same env vars: `SECRET_KEY_BASE`, `PORT`, `MYSQL_SOCKET`, `MYSQL_USERNAME`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`, `POOL_SIZE`. ## Compatibility - **Database:** no migration needed. Migrations are embedded SQL files with the same versions as the Ecto ones and share Ecto's `schema_migrations` table, so on the existing DB `space_age_api migrate` is a no-op. On a fresh MariaDB, `SHOW CREATE TABLE` of every table is byte-identical to what Ecto produces. Migrations don't run on `start` unless `AUTO_MIGRATE` is set, same as the old release. - **JWTs:** HS256 with `SECRET_KEY_BASE`, `iss=Joken`, same `aud` values and lifetimes. Tokens issued by the old API are accepted by the new one and the reverse, so a deploy doesn't log anyone out. Unit tests use real Joken-signed fixture tokens. - **Input handling:** follows Ecto's cast/validate rules as the Lua client relies on them. Partial upserts keep existing fields, blank strings fall back to schema defaults, floats are truncated on player/server upserts, `"1"`/`"true"` count as booleans, and errors come back as `{"errors": {"field": ["is invalid" | "can't be blank"]}}`. - **HTTP details:** response keys are alphabetical like Jason. Also matched: status codes, `{}` 404s, the ErrorView bodies, `Cache-Control` per route, `Client-Identity`, CORS headers and preflight, text responses without a Content-Type, and the `steam://` redirect. ## Intentional differences All of these were 500s (crashes) in the old API: - `POST /v2/rabbitmq/user` queried `servers.authkey`, which doesn't exist (`Ecto.QueryError` on every call). It now checks `server_configs` name + authkey, which is what the STOMP central login (server name + serverToken) needs. - A cast error on `servers.players` crashed while formatting the error (`to_string` on the `{:array, :string}` type). It now returns a normal 400. - An `Authorization` header with no token, or Discord requests with missing or non-hex signature headers, now get 401 instead of a 500. - GMod serializes empty tables either way, so an empty `[]` is accepted for map fields (`research`, `station_storage`) and an empty `{}` for `players`. Previously these rejected the whole save. - `/salink` for a Discord account already linked to another SteamID now moves the link. Before, it hit the unique index and 500'd. - `PUT /v2/servers/self` ignores a `name` in the body and always uses the authenticated server. The client never sends one. - `create_servers` uses `ENGINE = INNODB` instead of `MEMORY`. The original migration can't run on MariaDB or MySQL at all (MEMORY doesn't support JSON/TEXT columns; checked with the old release too). This only affects fresh installs, since existing DBs already have the version recorded. ## Testing - Differential test: the old release (built from `main`) and the new binary ran side by side against identical MariaDB 11.8 databases, and a script replayed ~110 requests shaped like the Lua client's (players, JWTs in both directions, applications, bans, servers, factions, RabbitMQ, Discord rejection paths, CORS, and error cases). Result: 0 unexpected differences; the only differences are the ones listed above. Final DB contents matched except the one row from the intentional `players: {}` leniency. - Discord `/salink` link, move, unlink and invalid-code flows were tested end to end against the Go build with a test key. - `go test ./...`, `golangci-lint` (0 issues), `nix build`, `nixfmt-tree --ci`. ## Deploying Nothing is required. As an optional follow-up in `FoxDen/core`, `spaceage/api.nix` can drop `ExecStop`/`ExecReload` and add `ExecStartPre = "... migrate"` if you want migrations applied automatically. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Rewrite API in Go
Some checks failed
Go check / check-go (pull_request) Successful in 1m39s
Nix check / check-nix (pull_request) Failing after 14m51s
5d4b1d9818
Port the Elixir/Phoenix API to Go, keeping the same routes, response
shapes, auth (Joken-compatible JWTs) and database schema. Migrations
share Ecto's schema_migrations table so existing databases need no
changes. Build follows the FoxDen Go service layout (buildGoModule with
vendor-hash.txt, managed-go workflow).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Doridian referenced this pull request from a commit 2026-09-29 16:05:32 -07:00
Doridian referenced this pull request from a commit 2026-09-29 16:13:14 -07:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
SpaceAge/space_age_api!87
No description provided.