Table of Contents
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
majorandminorrecorded 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_minorand 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.0sorts belowv0.9.0as 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 is200with an empty list. They are different answers. GET /admin/godot-versionscarries the rowid, which the public route does not.PATCHandDELETEaddress 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.
Guide
- Home
- Getting Started
- Building Godot
- Configuration
- Modules and Administration
- API
- Architecture
- Deployment and Operations
- Security
- Troubleshooting
- Development
Reference
- API
- Architecture
- Modules
- Module registry
- Admin
- Artifacts
- Deployment
- Kubernetes execution
- Durable execution store
- Immutable build configuration
- Download gateway
- Public logs
- Anonymous verification
- Abuse enforcement
- Public service remediation
- Authelia administration
- CI/CD
- macOS SDK