1 Reference Admin
picklemustard edited this page 2026-09-07 15:49:27 -04:00

Administering the catalog

Modules, module versions, Godot versions, and the compatibility lists that tie them together all live in Postgres and are edited over the API by users holding an admin token. This is the operator's side of modules.md.

The mental model

Two things are curated, and one is derived:

  • Modules and their versions — what exists, where to clone it from, and the oldest Godot each release supports.
  • Godot versions — what users can build, with major and minor recorded explicitly.
  • The compatibility list — derived from the first two, per Godot major/minor, and then frozen. This is the thing users actually see.

Nearly everything below is a write to one of the first two. The list updates itself; you intervene only when it gets something wrong.

Getting a token

A fresh deployment seeds one admin token from GBB_ADMIN_BOOTSTRAP_TOKEN when api_tokens is empty. Without it there is no way into a new database.

It is a bootstrap credential, not a working one. Issue a real token, then revoke it:

curl -X POST https://gbb.example/api/v1/admin/tokens \
  -H "Authorization: Bearer $GBB_ADMIN_BOOTSTRAP_TOKEN" \
  -d '{"name": "alex - laptop", "is_admin": true}'
# {"id": 2, "token": "gbb_…"}   <- shown once, never again

curl -X DELETE https://gbb.example/api/v1/admin/tokens/1 \
  -H "Authorization: Bearer $NEW_TOKEN"

Only the sha256 hash is stored, so a lost token is reissued, never recovered. Every use of the bootstrap token is logged loudly for exactly this reason.

Adding a Godot version

curl -X POST https://gbb.example/api/v1/admin/godot-versions \
  -H "Authorization: Bearer $GBB_ADMIN_TOKEN" \
  -d '{
        "name": "4.5 stable",
        "ref": "4.5-stable",
        "major": 4,
        "minor": 5,
        "is_default": false
      }'
Field Meaning
name What the UI shows. Unique.
ref The git ref in repo.
repo Optional. Empty uses godot_repo from the config; set it for a fork.
major, minor Set these deliberately. The UI suggests them by parsing ref, but the parse is a convenience and the columns are the truth.
is_default Preselected in the UI.

If this is the first version with that major/minor, its compatibility list is generated on the spot: every module's newest version whose minimum is at or below it. If the major/minor already exists — a fork of an existing line — the new version simply shares the list that is already there.

A fork is the case to get right. studioteal/4.4 cannot be parsed by any version parser, so record major: 4, minor: 4 yourself and it offers exactly what 4.4-stable offers. Leaving it wrong is how a fork ends up offering nothing, or offering modules that do not build against it.

Adding a module and its versions

See modules.md for the field reference. The order matters only in that a module must exist before its versions do.

Adding a version has one side effect worth knowing: it refreshes that module's entry in the newest Godot major/minor's list, and nothing else. Older lists are frozen and stay frozen.

Reading a list

curl https://gbb.example/api/v1/admin/compat/4.4 \
  -H "Authorization: Bearer $GBB_ADMIN_TOKEN"
{
  "godot": {"major": 4, "minor": 4},
  "modules": [
    {"name": "deep_engine_tool", "version": "v0.4.0", "origin": "generated",
     "generated_at": "2026-03-02T11:04:00Z"},
    {"name": "telemetry_bridge", "version": "v0.2.0", "origin": "admin",
     "generated_at": "2026-03-04T09:20:00Z"}
  ]
}

origin is the field to read first. generated means the rule produced it and regeneration may change it; admin means a person decided, and regeneration will leave it alone.

The user-facing view of the same thing is GET /api/v1/modules?godot_ref=4.4-stable, which needs no token.

Overriding an entry

When the rule is wrong — a module's newest release is known-broken on this engine, or an old release is the one that works — pin it:

curl -X PUT https://gbb.example/api/v1/admin/compat/4.4/modules/telemetry_bridge \
  -H "Authorization: Bearer $GBB_ADMIN_TOKEN" \
  -d '{"version": "v0.2.0"}'

That writes origin = 'admin'. It survives every later regeneration, including ones triggered by new module versions. To hand the entry back to the rule, delete it and regenerate:

curl -X DELETE https://gbb.example/api/v1/admin/compat/4.4/modules/telemetry_bridge \
  -H "Authorization: Bearer $GBB_ADMIN_TOKEN"
curl -X POST https://gbb.example/api/v1/admin/compat/4.4/regenerate \
  -H "Authorization: Bearer $GBB_ADMIN_TOKEN"

Deleting the row without regenerating removes the module from 4.4 entirely, which is also a valid thing to want.

When to regenerate

POST /api/v1/admin/compat/{major}.{minor}/regenerate rebuilds every origin = 'generated' row for that minor and leaves admin rows untouched.

Reach for it when:

  • You corrected a module version's min_godot_major / min_godot_minor and an older list should now include or exclude it.
  • You added a module whose history predates a Godot version you already have, and you want that older version to see it.
  • You removed a pin and want the rule to take the entry back.

Do not run it across every minor as routine maintenance. Freezing old lists is the design, not an oversight; regenerating them all turns "what 4.3 shipped with" into "what the rule says today", which is a different and less useful fact.

Deleting things

A DELETE on a module or a module version that a frozen compat row still points at is refused with 409 and the list of minors that depend on it. Removing it would silently rewrite what an old Godot version offers.

The deliberate path is to remove the compat rows first, minor by minor, and then delete. If that feels like too much friction, that is the intended amount: it is the difference between retiring a module and quietly changing history.

Tokens for building, not just for curating

Since M6 the build routes need a token too, and it is the same table and the same POST /api/v1/admin/tokens. The difference is is_admin: the admin routes here require it, the build routes take any valid token. So a CI job or a developer gets a plain token —

curl -sX POST http://localhost:8080/api/v1/admin/tokens   -H "Authorization: Bearer $GBB_ADMIN_TOKEN"   -H 'Content-Type: application/json'   -d '{"name": "ci", "is_admin": false}'
# {"id": 3, "token": "gbb_…"}   <- shown once, never again

— and uses it as $GBB_TOKEN, or -token:

export GBB_TOKEN=gbb_…
gbb build --godot 4.4-stable --platform linuxbsd

A browser does not use the token directly. It posts it once to POST /api/v1/session and works from the cookie that returns, because EventSource cannot send an Authorization header and so a build's log could not otherwise be followed. See api.md.

Revoking a token ends its sessions at the same moment, with nothing else to clean up: the cookie carries the token, so there is no session record that could outlive it.

Build quotas

A token may carry an allowance, counted in builds per rolling window:

curl -sX POST http://localhost:8080/api/v1/admin/tokens \
  -H "Authorization: Bearer $GBB_ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name": "ci", "build_quota": 20, "quota_window": "24h"}'

Omit build_quota and the token is unlimited, which is what every token issued before this existed is. Zero is not the same as omitting it: zero is a real and useful state — a token that may list builds and download artifacts but never start one — which is why "no limit" is spelled by leaving the field out rather than by 0.

A token at its allowance gets 429 with a Retry-After counting the seconds until its oldest counted build leaves the window. Every build the token started counts, including failed and canceled ones: a build that failed ninety minutes in consumed ninety minutes of a machine, and excluding it would make this a quota on successes and quietly reward submitting specs that break early.

The window slides rather than resetting on a boundary, so an allowance cannot be spent twice either side of midnight.

The audit log

Who did what, and when:

curl -s "http://localhost:8080/api/v1/admin/audit?limit=20" \
  -H "Authorization: Bearer $GBB_ADMIN_TOKEN"

?token_id=, ?action=, ?since= (RFC3339), ?limit= and ?before= narrow it; before takes an event id, the same cursor convention the build listing uses. Actions are a closed vocabulary: build.submit, build.cancel, artifact.download, session.create, session.delete, token.create, token.revoke, job.expired, catalog.write.

catalog.write covers every mutation on this page — a module, a release, an engine version, a pin. It was added by M6-10 and was missing from this list until M9.

Two things it deliberately does not do. It does not record reads — a listing poll from an open browser tab would be a row, and a table whose signal is one part in ten thousand is a table nobody opens. Downloads are the exception, because an artifact leaving the system is the event rather than the query. And it never records a credential, in the table or in the log line.

Every event also goes to the server's structured log. The two are not redundant: the table is queryable and joins to the jobs, the log line survives the database being the thing that failed.

token_name is stored alongside token_id on purpose, so a revoked — or one day deleted — token is still named by the lines it wrote. That is what makes revocation a soft delete worth having.

Reading it back

Everything above is a write. Since M9 the same surface can be read, which is what the admin UI at /admin is built on:

curl -s localhost:8080/api/v1/admin/modules                  -H "Authorization: Bearer $GBB_ADMIN_TOKEN"
curl -s localhost:8080/api/v1/admin/modules/telemetry_bridge/versions -H "Authorization: Bearer $GBB_ADMIN_TOKEN"
curl -s localhost:8080/api/v1/admin/godot-versions           -H "Authorization: Bearer $GBB_ADMIN_TOKEN"
curl -s localhost:8080/api/v1/admin/tokens                   -H "Authorization: Bearer $GBB_ADMIN_TOKEN"

Three things worth knowing about them:

  • The releases list is newest-added first, which is the order the compatibility generator considers them in — not version order, because v0.10.0 sorts below v0.9.0 as a string and the list would then disagree with what the catalog actually offers.
  • An unknown module is 404; a module that exists and has released nothing is 200 with an empty list. They are different answers.
  • GET /admin/godot-versions carries the row id, which the public route does not. PATCH and DELETE address a version by it.

The token list carries each token's allowance and never a credential — neither the plaintext, which existed for exactly one response, nor the hash, which is what authenticates. A quota reads as one of three things: {"unlimited": true}, a positive limit, or {"limit": 0} — the token that may list and download but never build.

The admin UI

/admin in a browser does all of the above. It needs an is_admin token, and it checks that itself before calling anything: an admin route answers a non-admin with 401, byte-identical to an unauthenticated request, and the frontend treats any 401 as a lost session — so without the check a non-admin would be signed out rather than told no.

What is not here yet

Nothing from M6. Retention landed in M6-11; this section used to say otherwise.