Forge is an independent Go module in forge/, using the frontend's net/http
listener model. It has no CMS storage, rendering-engine, controller or Kubernetes
API dependency. Its UI is embedded HTML/CSS/JavaScript with server-rendered public
pages and Goldmark GFM rendering; raw HTML/unsafe URL rendering is disabled.
See the operator/user guide for setup and
workflows, and return to the reference index for other interfaces.
Public API
All routes below support unauthenticated GET/HEAD. kind is exactly plugins
or themes; objects from the other catalogue return 404. All collection routes
accept offset and limit (default 24, maximum 100) and return
{items: [], total, offset, limit}. Item lists additionally accept q to search
the published name/description. Results are ordered newest publication first,
with item ID breaking ties. User lists use stable ID order.
| Route | Response |
|---|---|
/api/v1/{kind} |
Visible published items. |
/api/v1/{kind}/{id} |
Published item details. |
/api/v1/{kind}/{id}/download |
Attachment ZIP; ETag, Last-Modified, conditional and byte-range requests. |
/api/v1/users |
Makers with visible published work, biographies, totals and items. |
/api/v1/users/{id} |
One public maker, including visible items. |
/api/v1/users/{id}/items |
Paginated visible items belonging to that maker. |
Public item JSON contains id, kind, ownerId, packageId, name, version,
license, description (Markdown source), publishedAt (RFC3339), downloads,
size and downloadUrl (origin-relative). It never contains a private draft.
Public maker JSON contains id, name, biography (Markdown source), downloads
and items. No email, subject, token or issuer appears in these public records.
An unpublished/hidden item or suspended owner returns 404 for all public item
routes; makers with no remaining visible items return 404 too.
The HTML routes are /, /plugins, /themes, /{kind}/{id}, /users and
/users/{id}. The public catalogue and user pages render without JavaScript;
member/admin mutations and the local name/version filter use embedded JavaScript.
Member API
These routes require forge_member or forge_admin. Browser requests use an
HttpOnly encrypted/signed reference cookie. A validated Bearer token is also
supported. Issuer and subject together identify the owner; email/name never
determine access. The account ID is the first 16 bytes of
SHA-256(issuer + NUL + subject), hex encoded.
| Method and route | Request/response |
|---|---|
GET /api/v1/member/me |
{profile, roles, csrfToken}. Ensures the account profile exists. |
PUT /api/v1/member/profile |
{name, biography, revision} → updated profile. |
GET /api/v1/member/{kind} |
Paginated owned items, including private drafts/public snapshots. |
POST /api/v1/member/{kind} |
{name, version, license, description} → 201 created private item. |
GET /api/v1/member/{kind}/{id} |
One owned item. |
PUT /api/v1/member/{kind}/{id} |
{name, version, license, description, revision} → updated draft/item. |
PUT /api/v1/member/{kind}/{id}/archive |
Raw ZIP body; X-Forge-Revision header → updated draft/item. |
PUT /api/v1/member/{kind}/{id}/publication |
{published: true/false, revision} → updated item. |
Profile JSON is {id, name, biography, suspended, revision}. Private item JSON
is {id, kind, ownerId, draft, release, downloads, hidden, revision, createdAt}.
The draft includes editable metadata plus blob, size, packageId and
packageVersion, which only the upload endpoint can set. release is null or
a snapshot of the draft with a publishedAt timestamp. Revisions increment on
metadata, upload, publication and moderation changes, but not downloads.
For browser writes, bootstrap /api/v1/member/me and send its csrfToken as
X-CSRF-Token, plus a matching Origin/Referer. The token is HMAC-bound to the
authenticated session ID. Bearer writes need no CSRF token or Origin header.
GET never publishes, deletes or changes visibility. First authenticated access
creates a profile, but the profile remains absent publicly until publication.
JSON bodies are limited to 512 KiB; unknown fields/trailing JSON are rejected. Names/licenses are at most 160 bytes, versions 128 bytes and Markdown fields 256 KiB. ZIP bodies are limited to 64 MiB, 4096 entries and 256 MiB total expanded data. Validation checks all entry CRCs, the correct root manifest and version, unsafe/duplicate paths and non-regular entries. Forge never extracts or executes packages. Full package/schema/renderer compatibility validation remains the CMS installer's responsibility. The initial release keeps the current draft and current public snapshot, rather than exposing a release-history API.
/member is the workshop/profile/create UI; /member/{kind}/{id} edits an item.
Management and authentication
Management requires forge_admin, including the /admin UI. It inherits member
capabilities but is still subject to ownership checks on member routes.
| Method and route | Behaviour |
|---|---|
GET /api/v1/admin/summary |
Account/item/published/download totals and build version. |
GET /api/v1/admin/users |
Paginated profiles including suspensions. |
GET /api/v1/admin/items |
Paginated items including drafts and hidden status. |
PATCH /api/v1/admin/users/{id} |
{suspended: boolean, revision}; rejects self-suspension. |
PATCH /api/v1/admin/items/{id} |
{hidden: boolean, revision}; owners cannot override a hide. |
OAuth endpoints follow CMS authentication: GET /oauth2/start?redirect=/member,
GET /oauth2/callback, POST /oauth2/logout and POST /api/session/refresh.
GET /api/me returns the OIDC identity. Login uses single-use browser-bound
state, nonce, and PKCE for public clients; verification checks issuer, audience,
expiry and signatures. Logout and refresh require same-origin CSRF protection.
Session cookies and encryption derivation use Forge-specific domains. OIDC
transport policy checks discovery, lazy JWKS calls and redirects, including
the opt-in loopback-only HTTP exception.
Errors are JSON {error: "message"} for APIs: 400 invalid data, 401 missing/invalid
authentication, 403 role/ownership/suspension/CSRF rejection, 404 invisible/missing
object, 409 stale revision, 413 oversized archive, 503 auth unavailable. Some
OAuth and HTTP router errors use plain text. Browser authentication failures
redirect to login. Writes use optimistic revisions to reject stale tabs rather
than silently overwrite; reload and review edits after a conflict.
Storage and telemetry
catalogue.json uses schema version 1. A process-level flock ensures one writer
per data directory. Updates clone the catalogue under a mutex, sync a temporary
file, rename it and sync the directory. Failed pre-rename writes do not change
memory. An error syncing the directory after a completed rename is logged and
returned, with memory updated to match the installed catalogue. Reload before
retrying a mutation with an ambiguous error.
Uploads sync the SHA-256 blob before referencing it from a draft. Old blobs are retained, so a failed catalogue save cannot invalidate a published snapshot. Publishing checks the package's immutable ID reservation and version match. Read visibility uses suspension, moderation and publication together. Download counts are committed after successful response writes; recording failures are logged, so a transferred archive can be undercounted if storage fails at that point. Counts do not deduplicate clients or range requests.
The separate internal listener provides health/readiness and Prometheus, with
outer build identity/security/HTTP metrics middleware matching the public
listener. Metric families include http_requests_total,
http_request_duration_seconds, http_response_size_bytes,
http_requests_in_flight, forge_accounts, forge_items,
forge_account_http_requests_total, forge_account_items,
forge_account_downloads_total, forge_item_views_total and
forge_item_downloads_total. Stored IDs bound account/item labels to real
catalogue objects; HTTP method labels use the frontend's fixed method set.
The Docker build runs format checks, vet and Go tests before stamping the binary. The workspace, container pipeline and standalone image all include Forge. See Forge's README for the optional real-browser workflow.
