UniventsBackend
Coding Conventions
How Univents backend code is written — spec-first, the store, webhooks, realtime.
Spec-first development
api-spec.ymlis the contract and the single source of truth (every operation, schema, error sentinel).- Regenerate bindings after touching it:
just generate-oapi univents(internal/openapi/*.gen.go, not committed). - The embedded spec is served at
/docs/openapi.yml— this site's API reference is that file.
Feature layout
One directory per feature under internal/handlers/ (events, editions, ticket_types, products, programs, checkouts, purchases, badges, certifications, signatures, realtime, webhooks). Routes are registered in internal/app/router.go; authz goes through the auth resolver — never inline checks in handlers.
The store: checkout → purchase → webhook
POST /editions/{id}/checkoutreserves items and creates the Payssage intent in one request; prices are always server-computed from the DB — never trusted from the client.- The purchase is the record of truth (
purchases+purchase_items), correlated bypayssage_intent_id. Statuses flowpending → approvedfrom the Payssage webhook,expiredfrom a river worker. - The webhook receiver (
/webhooks/payssage) verifiesX-Payssage-Signature(HMAC over the exact bytes POSTed,PAYSSAGE_WEBHOOK_SECRET) before touching state. Return 200 to stop retries. - Availability is a ledger:
purchase_itemsrows are lockedFOR UPDATEinside the checkout transaction. One active ticket per person per edition — a second checkout 409s.
Response envelope & errors
- Every response is the
fun.Responseenvelope; failures areAppErrorwith sharederrxsentinels. - Owner-only resources return
404, not403— no existence leak (the whole catalog is public reads). - Async work (purchase expiry, badge/cert emissions) goes in river jobs, not in the request path.
Realtime
- WS frames (
/ws):purchase.snapshot/intent.updated/purchase.confirmed/purchase.expired/purchase.cancelled—{"type", "payload"}. - SSE (
/editions/{id}/store/stream): snapshot +event: stockdeltas; numbers are always recomputed from the DB — publishers send only item ids, never stale counts. ws_tokensare 32-byte random, SHA-256 at rest, 10-min TTL, one-time.
Profiles boundary
The Univents backend never reads IdentityX profile data. Holder names/avatars are derived by the frontend via IdentityX's public profile endpoint. Univents emits only its own data (registration/ticket names, event names, the badge action URL).
Database
- Schema in
db/, sqlc queries ininternal/sqlc, repos ininternal/repos. - Anything multi-row that must be atomic (checkout, emissions) runs in one transaction.
Tests & quality
- Parity tests in
internal/appkeep the router surface in sync with the spec. just univents test(gotestsum),just univents lint(golangci-lint).
Don't
- Don't reimplement HTTP serving/telemetry — boot through
lib/go/httpserver. - Don't trust client-sent prices or stock.
- Don't return the envelope root as the payload.