Ticketing Addon Backend (Backend-Nextkt)
Related documentation: Addon Modules Backend · Backend modules overview · Data ownership · Permissions Catalog · Promoters Module Backend · Venue Module Backend · Finance Module Backend · Frontend Nextkt
Workspace repo: Backend-Nextkt — GitHub https://github.com/Primuse-Pte-Ltd/Backend-Nextkt (Go module github.com/Primuse-Pte-Ltd/Backend-Nextkt). Canonical in-repo docs: AGENTS.md, docs/HANDOFF.md, TICKETING_MODULE_DESIGN.md, docs/DOCUMENTATION.md (full API), docs/TODO.md, and the autogenerated /openapi.json + /docs. This page records cross-service positioning for the dot connector; deep API/schema detail lives in-repo.
Status (read first) — now a STANDALONE platform
Section titled “Status (read first) — now a STANDALONE platform”⚠️ Direction change (2026-06-15):
Backend-Nextktis an independent ticketing platform, no longer a Kisum-auth module. It owns its own auth and tenancy (organizations, operator users + roles, ticket buyers, per-org API keys). Kisum is just a client: it integrates as a white-label partner (provisions an org
- drives
/adminheadlessly via a per-org API key — “Mode A”) and is a read-only source of venue + artist data. There is no Kisum JWT/JWKS, no/auth/me/access, no Core entitlement row, and noticketing.*seed in Kisum Auth — those are obsolete.
- Built + DB-verified. Oversell core, consumer commerce (cart, platform fee,
discounts, points, Xendit, idempotent webhook, issuance,
/t/{code}, my-tickets, email), full operator surface, deposits/guest-gate/reassign/refund, host/exclusivity/allocations, network BFF + Finance settlement, Redis rate-limit/cache, S3 uploads, hold sweeper, autogenerated OpenAPI. ~86 endpoints, 10 migrations, tests green. Self-contained (own Go module / deps / migrations / Postgres /.env). - Standalone pivot complete (Phases 1–8):
organizationstenant registry (organizations.id=company_id).- Owned operator auth —
users+organization_members, login → module-signed JWT, role→permission matrix (replaced Kisum operator auth). - Onboarding + system-admin approval — self-serve signup;
/sysadmin/*approve/reject/suspend; selling gated onAPPROVED. - White-label partner API —
/partner/*(master key) provisions orgs idempotently (same id, auto-approved,PARTNER) + mints per-org API keys;X-API-Keydrives/adminheadlessly. 5–6. Storefront — public cross-org country-filtered discovery (/storefront/events)- white-label branding/domain resolution (
/storefront/site,/admin/branding).
- white-label branding/domain resolution (
- Artists data client — public
/artistsproxy (mirrors/venues). - Retired the Kisum operator-auth code; repositioned these docs.
Naming note: an older commercial sketch described ticketing as a
finance.ticketing.*capability slice in Backend Core Addons §3.3. That is superseded — ticketing is a standalone platform with its ownticketing.*permission model resolved in-code by member role, not seeded in Kisum Auth.
Purpose
Section titled “Purpose”Backend-Nextkt is the Kisum ticketing / box-office engine, sold as an add-on module. Any Kisum company — promoter, venue, club, beach club — can turn it on and sell tickets: define ticket inventory against an event, run an online cart → checkout → confirm → issue flow with oversell protection, take deposits / discounts / loyalty points, charge a per-ticket platform fee (the revenue model), record offline manual sales, handle refunds / guest lists / table reassignment / door scanning, resolve host/exclusivity, grant collaborator allocations, and push settlements to Finance.
It is a Go modular monolith (chi + pgx + sqlc, PostgreSQL), deployed as a container (ECS/Fargate) behind Cloudflare.
Where it sits in Kisum
Section titled “Where it sits in Kisum”The ticketing module follows the same satellite pattern as Venue: it does not own the canonical Event. The Promoter module owns the event (publicEventId); ticketing attaches ticketing-side state keyed by promoter_event_id.
Promoter module Venue module Ticketing module (Backend-Nextkt)───────────────── ───────────────── ─────────────────────────────────Event (canonical) ←── venue-side ops state ←── ticketing-side statepublicEventId (UUID) (promoter_event_id ref) (promoter_event_id ref): inventory · tiers · holds · reservations · sales · fee · scansflowchart TD
BUYER[Ticket buyer / storefront]
STAFF[Organizer / system admin<br/>OWNED login]
KISUM[Kisum or any partner<br/>white-label client]
FIN[Kisum Finance<br/>POST /api/income]
VEN[Kisum Venue + Artists<br/>internal reads]
XEN[Xendit gateway]
TKT[Backend-Nextkt<br/>standalone ticketing platform]
DB[(Own Postgres<br/>orgs · users · company_id on every table)]
STAFF -->|operator JWT + x-org| TKT
BUYER -->|x-tenant + optional buyer JWT| TKT
KISUM -->|provision + per-org X-API-Key| TKT
TKT -->|owned auth + tenancy| DB
TKT -->|consumer payments| XEN
TKT -->|push settlement income| FIN
TKT -->|proxy venue/artist data| VEN
Kisum is now a client of the platform (top), and an upstream data source (venue/artist) — not the auth/entitlement authority it used to be.
Identity realms (all OWNED, never mixed, fail-closed)
Section titled “Identity realms (all OWNED, never mixed, fail-closed)”| Realm | Routes | Auth | On failure |
|---|---|---|---|
| Operator (organizer / sysadmin) | /admin/*, /public/*, /sysadmin/* | Owned operator login → module-signed JWT (internal/operator) + x-org (active org) → organization_members role → ticketing.* permission set (system admins get all). /sysadmin/* requires is_system_admin. | 400 bad x-org · 401 bad token · 403 not-a-member / permission / org suspended |
| Per-org API key (white-label partner, e.g. Kisum) | /admin/*, /public/* | X-API-Key (per-org, sha256-stored) → acts AS its org with full ticketing.*. | 401 bad/revoked key · 403 suspended org / x-org mismatch |
| Buyer / fan | /consumer/* | Owned: x-tenant + optional buyer JWT (bcrypt + HS256) + x-cart-session. Gated to APPROVED orgs. Guest checkout allowed per-tenant. | 400 missing x-tenant · 401 bad buyer token · 403 storefront unavailable |
| Partner master key | /partner/* | X-Partner-Key (platform master) → provision orgs + mint/list/revoke API keys. | 503 if unset · 401 bad key |
| Service-to-service | /network/* | X-Internal-API-Key + x-org. | 400/401 |
| Public | /storefront/*, /venues, /artists, /t/{code} | none | — |
Rule: auth is now local (no external call → no 503-on-auth). Permissions
are resolved from the member’s role (or full for API keys / sysadmins), never from
JWT claims alone. Full error contract: docs/DOCUMENTATION.md §9.
Data ownership & tenancy
Section titled “Data ownership & tenancy”Conforms to the platform Data ownership model:
- Tenant key =
company_id(UUID) =organizations.id— the platform’s own tenant registry (not a Core reference). For partner-provisioned orgs the id is the partner’s company id, reused verbatim. It is on every domain table; every SQL query carriesWHERE company_id = $1(no ORM interceptor) — a query physically cannot read across tenants. - Owned identity tables:
organizations(tenants),users+organization_members(operators),buyers(global fan identity),api_keys(per-org). These define accounts/tenants rather than per-tenant domain rows. - Deliberate cross-tenant reads (commented): global
buyers, system sweeps (ExpireAllStaleHolds,/t/{code}, settlements), and the public storefront discovery (/storefront/events*, cross-org by country). - External references only by bare UUID: the Promoter event via
promoter_event_id(=publicEventId), and venue/artist ids surfaced through the read-only proxies. Never invents event identity. - Owns: ticketing-side state only —
ticketing_events(satellite), venue maps + hotspots,event_inventory(ticket types),inventory_holds,reservations/items,ticket_issuances,scan_events,payment_records,manual_sales,guest_list_entries, discount codes + redemptions, loyaltypoint_accounts/point_ledger,checkout_settings,event_allocations,settlements. Money is whole-IDRint64in Go (storedDECIMAL(18,2)). - The module’s Postgres is the module’s own DB — never the Stage/monorepo DB, and not a second source of truth for companies, memberships, entitlements, or canonical events.
Oversell safety (the core invariant)
Section titled “Oversell safety (the core invariant)”A STANDING-pool hold runs its capacity check and its hold insert inside one SERIALIZABLE transaction (internal/domain/capacity via store.WithSerializable), so two concurrent buyers cannot both pass. TABLE seats flip state atomically (AVAILABLE → HELD → SOLD). Pool usage is counted with the same scope by every seller (active holds + live reservation items + live manual-sale items + non-cancelled guest heads). Proven by an 8-buyer concurrency integration test. Confirm is idempotent — a Xendit webhook retry is a no-op (no double issuance / double charge / double points).
Money & Finance settlement
Section titled “Money & Finance settlement”- Activation: the Kisum
ticketingadd-on is a $0 access gate (Core entitlement only). Kisum Checkout bills subscriptions; per-ticket fees don’t fit that, so this module builds no subscription billing. - Revenue: a per-ticket platform fee computed at the module’s own consumer checkout (flat-per-ticket or % of subtotal; on-top or absorbed; per-tenant in
checkout_settings). - Collection: the host company’s Xendit gateway collects gross.
- Settlement: on the CONFIRMED transition a
settlementrow records gross / platform fee / host net, then the server pushes income to Kisum Finance (POST /api/income, best-effort, status recorded). A venue-hosted show with a promoter is a 3-way split (platform fee → us, promoter cut → promoter, remainder → venue) whose payout math is computed by Finance from this record + the Venue Deal terms.
Host / exclusivity / collaboration
Section titled “Host / exclusivity / collaboration”Every event has exactly one host company (= the row’s company_id). Resolution: exclusive Kisum venue → venue hosts; operator owns the venue → self-host; otherwise promoter self-hosts. A collaborator (e.g. a promoter selling inside a venue-hosted event) gets an event_allocation keyed by their Kisum company id and lists their grants via GET /admin/collaborations. Cross-company write access via x-venue-org delegation is pending the Kisum contract (see open items).
Network BFF (cross-module reads)
Section titled “Network BFF (cross-module reads)”Exposes /network/* so entitled Kisum modules read ticketing state for an event:
| Endpoint | Returns |
|---|---|
GET /network/events/{id}/sales | sales summary (confirmed, tickets, gross, collected, platform fee) |
GET /network/events/{id}/settlements | settlement totals + Finance push status |
GET /network/availability | live availability for partner modules |
This mirrors the platform’s other network BFFs (/api/venues-network/*, /api/artists-network/*). It is not a path for browsers to skip BFF access rules or Auth/Core ownership.
Consumes — Venue + Artist directories (public proxies)
Section titled “Consumes — Venue + Artist directories (public proxies)”The storefront needs venue + artist list/detail without holding the upstream modules’ credentials, so ticketing exposes public, no-auth pass-throughs:
| Endpoint | Proxies to (Venue module) |
|---|---|
GET /venues | GET /internal/venues (forwards page/limit/q/status/city) |
GET /venues/{id} | GET /internal/venues/{id} |
GET /artists | Artists module GET /api/v1/artists (forwards q/page/limit/genre/sort/country) |
GET /artists/{id} | Artists module GET /api/v1/artists/{id} (forwards include) |
Venue uses VENUE_INTERNAL_*; Artists uses ARTISTS_INTERNAL_*. Both are
read-only outbound proxies authenticated upstream with an internal API key; the
ticketing system stores neither venue nor artist data. These two are the only
remaining Kisum touchpoints for the standalone ticketing platform.
The upstream is authenticated with VENUE_INTERNAL_API_KEY (+ VENUE_INTERNAL_BASE_URL) inside the ticketing server; the caller needs nothing. The upstream status + JSON body are forwarded verbatim; transport failure → 502, unconfigured upstream → 503. Venue stays the source of truth — ticketing stores no venue data (Venue API, Internal Base-facing venue reads).
⚠️ This route is deliberately public per the storefront requirement, yet it serves Venue’s internal reads. It is kept read-only (list/detail) and query-filtered; widening it to tenant-private venue data would require adding auth.
API surface (high level)
Section titled “API surface (high level)”~65 endpoints across the realms (full table: docs/DOCUMENTATION.md §8; machine spec: /openapi.json):
- System/docs (PUB):
/health,/readyz,/openapi.json,/docs. - Consumer/storefront (BUY,
x-tenant): browse events/inventory/availability, buyer signup/login, cart,POST /consumer/checkout(→ XenditinvoiceUrlor free confirm), my-tickets, reservation + split-pay share token. - Public (PUB):
GET /t/{code}(ticket page),POST /webhooks/xendit(verified byx-callback-token, idempotent confirm),GET /venues[/{id}]+GET /artists[/{id}](public proxies to the Venue + Artists modules — see Consumes). - Operator setup (OP): settings, venue maps (+ SVG label extract), events, host-mode, inventory/tiers, complimentaries, uploads.
- Operator sales & ops (OP): manual sales, reservations, cancel/refund/refund-requests, reassign, guest list, scan, products, discount codes, allocations, collaborations.
- Availability (OP) & Network BFF (NET).
Tech & layout
Section titled “Tech & layout”Go 1.26 · chi v5 · pgx v5 + sqlc · PostgreSQL. Layers: cmd/server (wiring + in-process hold sweeper) · internal/httpapi (routes/handlers) · internal/domain/* (identity-agnostic logic: capacity, checkout, issuance, totals, discount, manualsale, guestlist, host, hotspots) · internal/store (pgx pool + Serializable tx) · internal/db/{migrations,query,sqlcgen} · internal/kisum/auth · internal/consumer · internal/{cache,storage,email,payment,finance,openapi,svgmap,money,httpx}. sqlc rule: write SQL in internal/db/query/*.sql, run make sqlc; never hand-edit internal/db/sqlcgen. After route changes run make genspec.
Infra: Redis (REDIS_URL) backs rate-limiting (falls back to in-memory); S3/R2 (BUCKET_AWS_*) backs uploads (no-op when unset); SES for email (log fallback); a background sweeper releases TTL-expired holds across tenants every ~2 min.
Open items (no longer gate the platform’s own auth)
Section titled “Open items (no longer gate the platform’s own auth)”The standalone pivot removed the Kisum-auth dependencies (no Core entitlement row,
no Auth ticketing.* seed, no /auth/me/access). These remain integration
contracts to confirm with the relevant teams, but they don’t block the platform
running independently:
- Finance income contract — exact
POST /api/incomeshape / multi-party split (one income+lines vs income+bills). - Cross-company collaborator writes — a promoter selling inside a venue-hosted event (today: read via
event_allocations; partner can model it via its own org). ticketingControlhome — where venue-exclusivity is declared upstream.- Per-phase deferred work (operator publish-gate, team invitations, email verification, scoped API keys, branding-asset upload, caching, multi-country events) — see
docs/TODO.md§0.5.
When behavior, routes, schema, auth, or cross-service contracts change, update the
repo docs (AGENTS.md, docs/*, /openapi.json) and this page in the same task.