Skip to content

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-Nextkt is 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 /admin headlessly 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 no ticketing.* 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):
    1. organizations tenant registry (organizations.id = company_id).
    2. Owned operator authusers + organization_members, login → module-signed JWT, role→permission matrix (replaced Kisum operator auth).
    3. Onboarding + system-admin approval — self-serve signup; /sysadmin/* approve/reject/suspend; selling gated on APPROVED.
    4. White-label partner API/partner/* (master key) provisions orgs idempotently (same id, auto-approved, PARTNER) + mints per-org API keys; X-API-Key drives /admin headlessly. 5–6. Storefront — public cross-org country-filtered discovery (/storefront/events)
      • white-label branding/domain resolution (/storefront/site, /admin/branding).
    5. Artists data client — public /artists proxy (mirrors /venues).
    6. 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 own ticketing.* permission model resolved in-code by member role, not seeded in Kisum Auth.


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.


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 state
publicEventId (UUID) (promoter_event_id ref) (promoter_event_id ref):
inventory · tiers · holds ·
reservations · sales · fee · scans
flowchart 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)”
RealmRoutesAuthOn 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.


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 carries WHERE 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, loyalty point_accounts/point_ledger, checkout_settings, event_allocations, settlements. Money is whole-IDR int64 in Go (stored DECIMAL(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.

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).


  • Activation: the Kisum ticketing add-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 settlement row 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.

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).


Exposes /network/* so entitled Kisum modules read ticketing state for an event:

EndpointReturns
GET /network/events/{id}/salessales summary (confirmed, tickets, gross, collected, platform fee)
GET /network/events/{id}/settlementssettlement totals + Finance push status
GET /network/availabilitylive 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:

EndpointProxies to (Venue module)
GET /venuesGET /internal/venues (forwards page/limit/q/status/city)
GET /venues/{id}GET /internal/venues/{id}
GET /artistsArtists 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.


~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 (→ Xendit invoiceUrl or free confirm), my-tickets, reservation + split-pay share token.
  • Public (PUB): GET /t/{code} (ticket page), POST /webhooks/xendit (verified by x-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).

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:

  1. Finance income contract — exact POST /api/income shape / multi-party split (one income+lines vs income+bills).
  2. 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).
  3. ticketingControl home — where venue-exclusivity is declared upstream.
  4. 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.