Venue Module Backend
Related documentation: Backend Modules · Venue Module Backend API · Data ownership · Access Control · Frontend Venues
Venues accepts Auth-issued X-Service-Token credentials additively and can use them for Artists and Promoters calls behind S2S_TOKENS_ENABLED; see the canonical service-token contract.
Current implementation snapshot
Section titled “Current implementation snapshot”Backend-Kisum-Venuesnow exists as a real Go/Fiber service with PostgreSQL DI, JWT +x-orgaccess enforcement, internal API-key routes, repository-backed MVP handlers, and a production-intended legacy sleeping-venue importer.- The current runtime already implements venues, spaces, availability blocks, bookings, customer overlays, and internal Base-facing venue reads.
- Spaces include structured operating profile fields: capacity models, setup/load-in/load-out buffers, pricing defaults, amenities/equipment, accessibility/age/curfew/sound/technical notes, media/floorplan metadata, and marketplace publish/profile data.
- The current runtime also exposes Admin-facing internal venue CRUD, venue-type, sleeping-inventory, takeover-approval, takeover-audit, reconciliation, and activation endpoints so Admin can use Venue as source of truth without direct DB access.
- Phase 7 (reporting) is live. Four
/api/v1/reports/*endpoints back the same query contract (fromAt,toAt,bucket,venueId,spaceId,businessUnitId,marketCompanyId,marketPersonId): utilization (pure Venue truth), revenue (Venue deposits merged with Finance-derived invoiced/paid), profitability (Finance income − Finance bills), customers (Venue bookings grouped by Artists party id, optionally enriched with Finance totals). When the Finance HTTP client is unconfigured, reports returnfinanceLinked = falsewith an explicitfinanceMessage— never silent zeros. Aggregations live ininternal/venue/repositories/reports_postgres.go(ad-hoc SQL, no materialized views). - Phase 6 (events + operations) is live on the Venue side. Events are persisted as references to the canonical Promoter-owned event (
promoterEventId) or as placeholders for off-platform promoters (isPlaceholder = true) with a reconciliation path viaPOST /api/v1/events/:id/link-promoter. Operations checklists, reusable templates, readiness state, and event/space assignment are all persisted. - Phase 4 commercial routes (deals, contracts, deposits, event finance-visibility) are repository-backed on the Venue side. Real external Adobe Sign upstream sync and the deeper Finance handoff (booking obligations, settlements, profitability source of truth) are still pending. Deposit invoice sync is live through Finance
incomemachine mode; reporting read-path is live throughGET /api/incomeandGET /api/bills. - Pending: real Adobe Sign upstream, deeper Finance handoff beyond deposit invoices + reports read-path, Venue HTTP client to call Promoters
GET /internal/events/:id(resolver live on Promoters since 2026-05-27), Finance-side aggregation endpoints so reports don’t have to pull every row. - SQL migrations: tracked in
venues_schema_migrations(pkg/sqlmigrate,cmd/migrate,make migrate-*). Apply explicitly viamake migrate-up/migrate-up-one— not on API startup.migrations/destructive/is never auto-run. DBs that already ran manualpsqlfiles mustmake migrate-mark-allonce before applying new migrations. - This page documents the enforced module boundary plus the current runtime direction.
Overview
Section titled “Overview”Backend-Kisum-Venues is the domain backend for the venue module.
It exists so Kisum can manage a venue as an operating business, not only list venues or expose legacy CRUD.
Venue module responsibilities include:
- venue master records
- space records
- space-scoped availability and booking workflows
- multi-space booking support
- venue-side customer overlays on top of Market parties
- venue-rental contract lifecycle state
- Adobe signature orchestration state
- deposit workflow metadata
- venue operational readiness
- venue and per-space reporting
- imported sleeping venue inventory
Product contract
Section titled “Product contract”Current v1 product rules that the backend must support:
1 company = 1 venue1 venue = many spaces- a space behaves like a lightweight BU-style operational scope
- one booking may include one or many spaces
- whole-venue rentals are valid
- managers can edit operational details
- admin / tenant superadmin finalize approval-sensitive actions
- Finance remains the source of truth for financial persistence
Future multi-venue ownership must stay possible, but it is not the current tenant contract.
Ownership model
Section titled “Ownership model”Venue owns
Section titled “Venue owns”- venue records
- imported sleeping venue inventory before takeover
- space records
- venue type catalog imported from legacy Mongo
- availability blocks
- booking records (incl. optional
eventId/promoterEventIdlinkage carried from creation through confirmation) - venue-side event references / placeholders (NOT the canonical promoter event — see Promoter ↔ Venue event linkage below)
- venue-side deal structures
- contract workflow state
- signature orchestration state
- venue-side deposit metadata
- customer overlay data
- operational templates, checklists, tasks, and readiness state
- venue reporting views
Venue does not own
Section titled “Venue does not own”- user identity
- sessions / JWT issuance
- memberships
- company / business-unit master data
- package / entitlement truth
- canonical Artists party master data
- canonical geo master data
- invoice / settlement / ledger truth
- canonical event records — those live in the Promoter module; Venue stores only references or placeholders for off-platform promoters
- external directory data — OpenStreetMap and similar upstreams are consulted live for venue discovery; Kisum does not mirror or bulk-import them (see External venue directory below)
External venue directory (2026-07-28)
Section titled “External venue directory (2026-07-28)”When a venue search finds little or nothing in Kisum, Venue falls back to an OpenStreetMap
geocoder so the operator gets candidates instead of a dead end
(the existing GET /api/v1/venues/sleeping, which gains an optional meta.external block —
no new endpoint, and data keeps its current shape).
The boundary is deliberately narrow:
- An external record is a suggestion, never a venue. It becomes a Kisum row only when a person explicitly picks it. Nothing is bulk-imported.
- How it becomes a row depends on who is asking. A venue operator claiming their business
goes through creation requests — proof of ownership plus admin approval. A promoter naming
where a show happens goes through
POST /internal/venues/from-external— no proof, no approval, because ownership is not the question being asked. That path creates the row sleeping and claimable, so the real operator can still claim it later through the takeover flow. - Kisum does not become a mirror of OpenStreetMap. Bulk loading (Overture/Overpass) was
evaluated and rejected: it would fill
venueswith non-bookable POIs and create a duplicate-matching burden larger than the value. - Lineage is persisted, not implied. Approved venues carry
external_source,external_source_id, andexternal_source_attribution(migration000040). OSM is ODbL, so attribution has to survive into the database and out to any surface that displays the venue. - External capacity is never authoritative. It is carried as an advisory hint for a human to confirm, because OSM capacity figures usually describe a seated or sports configuration rather than a concert one.
- The failover cannot break search. An upstream outage degrades to an explicit
externalStatus: "unavailable"alongside intact internal results.
Off by default (VENUE_EXTERNAL_DIRECTORY_ENABLED). Contract detail lives in the
Venue API page.
Promoter ↔ Venue event linkage
Section titled “Promoter ↔ Venue event linkage”Canonical event truth lives in the Promoter module. Venue stores only a reference row in venue_events so finance-status, operations checklists, and reporting can hang off something local without forcing every event consumer to round-trip through Promoter.
Every venue_events row is exactly one of:
- Real promoter reference —
promoter_event_idis set,is_placeholder = false. The Venue row mirrors a canonical promoter event. - Placeholder for off-platform promoter —
is_placeholder = true,promoter_event_idis null, plusplaceholder_promoter_name(and optionallyplaceholder_promoter_email,placeholder_notes). The promoter has not joined Kisum yet; this row holds the venue-side operational state until reconciled.
Three creation paths populate venue_events:
- Explicit
POST /api/v1/events— strict: must carry exactly one ofpromoterEventIdorplaceholder=trueplus placeholder promoter context. - Booking confirmation sync (
SyncBookingEvent) — resolves in orderbooking.eventId(existing linked row) →booking.promoterEventId(upsert by(company_id, promoter_event_id)) → placeholder shadow keyed onbooking_id. The resolved id is back-linked onto the booking row. - Placeholder reconciliation (
POST /api/v1/events/{id}/link-promoter) — promotes a placeholder to a canonical promoter reference once the previously off-platform promoter joins Kisum, mirroring the sleeping-venue takeover pattern.
Rules:
PATCH /api/v1/events/{id}may update venue-side operational fields (title, description, status, start/end, spaceIds, placeholder context on placeholder rows) but never mutatespromoter_event_idoris_placeholder. Promotion only happens vialink-promoter.- The
(company_id, promoter_event_id)pair is unique whenpromoter_event_idis non-null — a real promoter event can be referenced by at most one Venue row per company. - Promoters exposes
GET /internal/events/:eventId(machine key) since 2026-05-27 — acceptspublicEventIdUUID or Mongo_id. Venue Phase 3 will call it before persist; until then Venue trusts caller format forpromoterEventId.
Platform dependency split
Section titled “Platform dependency split”Auth owns
Section titled “Auth owns”- identity
- sessions
- JWTs
- company memberships
- business-unit memberships
- effective module / permission resolution
Core owns
Section titled “Core owns”- companies
- business units
- commercial catalog
- subscriptions
- entitlements
Artists owns
Section titled “Artists owns”- canonical counterparties
- canonical contacts / people
- country master data
- city master data
Finance owns
Section titled “Finance owns”- obligations
- invoices
- payments
- settlements
- accounting integration truth
Practical rule:
- Venue orchestrates venue workflows
- Finance persists finance truth
Admin integration
Section titled “Admin integration”Admin venue management must flow through:
Frontend-Kisum-AdminBackend-Kisum-AdminBackend-Kisum-Venues
Rules:
Backend-Kisum-Adminstays the browser-facing BFFBackend-Kisum-Venuesstays the runtime source of truth- Admin must not read Venue Postgres directly
- the integer
venues.idis the canonical venue identity everywhere (it wasvenue_idbefore migration000042) - sleeping venue takeover approval, audit, reconciliation, and activation are Venue-owned workflow state surfaced through Admin
Shared party model
Section titled “Shared party model”Venue must not duplicate party master data.
Recommended and current target rule:
- bookings, contracts, and overlays reference Market company/person ids when possible
- Venue stores only local overlay data such as:
- notes
- preferred terms
- risk
- relationship history
- venue-specific performance context
- local operational contacts
Finance boundary
Section titled “Finance boundary”Venue depends operationally on Finance and should be treated as finance-linked.
At the product level:
- a company using Venue Management should also have Finance enabled
- venue-management users should get the Finance visibility required for deposit and settlement workflows
At the persistence level:
- Venue may own deposit expectations and operational status
- Finance owns invoice/payment/settlement/accounting truth
- Current runtime detail: Venue deposit create/update now calls Finance
POST|PATCH /api/incomein machinesource=venue_depositmode, stores the returned Finance id locally, and should read paid-state back from Finance
Contract and signature boundary
Section titled “Contract and signature boundary”Venue owns the canonical venue-rental contract record and its operational lifecycle:
- draft creation
- template selection
- placeholder resolution
- signer assignment
- send for signature
- signature progress
- signed / declined / expired state
- attachments and addenda
Adobe Sign is backend-only.
The frontend must never hold Adobe credentials or call Adobe directly.
Access model
Section titled “Access model”Tenant-scoped requests require:
Authorization: Bearer <JWT>x-org: <company_id>
Internal service-to-service routes may use:
X-Internal-API-Key: <VENUE_INTERNAL_API_KEY>
Required fail-closed direction:
- JWT validation
- company context enforcement
venuemodule gate- granular
venue.*permission checks - scoped visibility for space-level operations
Current runtime shape
Section titled “Current runtime shape”Current stack:
- Go
- Fiber
- PostgreSQL
pgxpool- migration-driven schema
Current route namespaces:
/api/v1/*for tenant/user routes/internal/*for service-to-service reads
Current implemented route families:
- venues
- spaces
- booking slots (per-space fixed windows + per-slot price; promoter bookings use
bookingDate+slotIds) - availability blocks
- bookings (with optional
eventId/promoterEventIdlinkage; slot-based create derivesstartAt/endAt/totalPriceCents;PUT /api/v1/bookings/:idfull operational edit;GET /api/v1/bookings/:id/counterpartyArtists-enriched counterparty) - customer overlays
- deals, contracts (with local signature adapter), deposits (with Finance income sync), event finance-status
- events (incl.
POST /api/v1/events/:id/link-promoterfor placeholder reconciliation) - operations checklists + reusable templates (incl. event/space assignment and readiness state)
- reports (utilization / revenue / profitability / customers — Venue aggregations + optional Finance HTTP enrichment via
GET /api/incomeandGET /api/bills) - internal venue list/detail/inquiry
- internal Admin venue CRUD, venue-types, sleeping-inventory, takeover-approvals/audit/reconciliation, activation
Current pending route families:
- (none for v1; pending work is upstream — real Adobe Sign, deeper Finance handoff, optional Finance-side aggregation endpoints)
Legacy sleeping venue import
Section titled “Legacy sleeping venue import”The current migration direction includes a hard import path from legacy Mongo.
Imported collections:
kisum_data.venues_typekisum_data.venues
Imported venue-type rules
Section titled “Imported venue-type rules”Legacy venue types import into the singular Postgres table:
venue_type
Key model:
venue_type.venue_type_id- numeric type id from Mongo
- primary key
Venue rows link through:
venues.venue_type_id- FK to
venue_type(venue_type_id)
- FK to
Imported venue rules
Section titled “Imported venue rules”Imported venue inventory lands under the platform tombstone company (00000000-0000-0000-0000-000000000001 / [Deleted organization]), not a real tenant company.
Imported records are marked:
ownership_status = sleepingis_sleeping = trueis_claimable = true
Compatibility keys preserved on venues:
- import source name ->
external_source - the source record’s own id ->
external_source_id - Mongo
unique_id->unique_idwhen present - the pre-migration TEXT uuid ->
uuid
Current DB uniqueness rules:
idis the primary key (integer, sequence-backed byvenues_id_seq)uuidis enforced unique, so references written before migration000042still resolve- (
external_source,external_source_id) is enforced unique where both are set unique_idis enforced unique on live rows (partial index,deleted_at IS NULL)
Migration 000042 renamed venue_id to id, renamed the old uuid id to uuid, merged
source_system/source_mongo_object_id into the external_* pair, and dropped
source_collection.
Migration 000043 made country_id, venue_type_id and normalized_name NOT NULL, dropped
name_normalized / normalized_city / normalized_country, and rebuilt the column order
(id, uuid, name, unique_id, slug, venue_type_id, country_id, city_id, status, company_id, business_unit_id, then the rest). Geo ids are resolved at write time from Artists over HTTP —
there are deliberately no local countries/cities tables; the city/country/timezone text
stays on the row for display.
Geo import rules
Section titled “Geo import rules”Imported venue rows should keep raw compatibility values:
countrycitycountry_iso2
and also resolve canonical Market references when possible:
country_idcity_id
Important rule:
cityandcity_idmay remainNULL- unresolved geo ids must remain
NULLrather than guessed
Import phase scope
Section titled “Import phase scope”This phase is import-only.
Takeover approval and activation are later workflow phases and are not part of the import command itself.
Redis GET response cache (2026-06-28)
Section titled “Redis GET response cache (2026-06-28)”Backend-Kisum-Venues optionally caches successful GET JSON responses in Redis (REDIS_URL, default TTL 7 days). Keys are scoped by path, query hash, and x-org.
Write invalidation: Every POST/PATCH/PUT/DELETE purges Redis before and after the handler (after only on 2xx). When the path includes a venue id, delete all keys matching venues:*{venueId}* plus venue list routes (/api/v1/venues, /internal/admin/venues, /internal/venues, /internal/catalog/venues). No write-through — the next GET loads from Postgres. Other writes still clear x-org / anon buckets and path-specific keys. Admin BFF mirrors the same venue-id purge on admin:*{venueId}*.
Empty lists are not cached: Responses with "data":[] (or null) are never stored, so a first “no bookings” read cannot stick after a later create.
Bypass (debug / support): Send header x-redis: bypass or query ?live=true on GET to skip read cache. Promoters BFF forwards x-redis when the browser sends it.
Product note: Promoter marketplace Calendar tab shows operator availability blocks (rose) plus the promoter’s own booking inquiries for the selected space (violet). Venue operator blocks do not include promoter inquiries until the venue accepts and materializes holds. Full inbox: Promoters My venue bookings; operator queue: venue Bookings + resource Calendar.
Booking slots + commercial packages (2026-06-29)
Section titled “Booking slots + commercial packages (2026-06-29)”Venue operators define fixed bookable windows per space (e.g. Slot 1 08:00–13:00, Slot 2 14:00–22:00). Pricing lives on commercial packages per slot, not flat price_cents on the slot row (removed in migration 000031). Each slot has ≥1 package; exactly one default; packages may be public (marketplace) or internal (quote-only). Reusable templates clone onto slots.
Promoters do not pick arbitrary start/end times — they send bookingDate (YYYY-MM-DD) + slotIds + slotPackageSelections ([{ slotId, packageId }], one public package per slot) on POST /api/v1/bookings (Promoters BFF: POST /api/venues/bookings). The Venues service resolves slot templates in the venue timezone, snapshots package terms on the booking (slot_package_selections JSONB + termsVersionHash), sets indicative_price_cents (fixed rent only at inquiry), stores start_at/end_at for calendar/conflict checks, and sets total_price_cents = indicative_price_cents for backward compat. Confirmed bookings never mutate snapshots; GET booking may flag drift if live package terms changed.
Post-event settlement (manual sales inputs v1) calculates final rent from snapshotted terms + versus groups; POST /api/v1/bookings/:id/settlement/submit calls FinanceHandoff.SubmitSettlement (noop v1 — documented payload contract for Finance Phase 7).
| Surface | Route / UI |
|---|---|
| Venue admin — slots | GET/POST/PATCH/DELETE /api/v1/booking-slots — Settings → Spaces → clock icon (time/status only) |
| Venue admin — packages | GET/POST /api/v1/booking-slots/:slotId/packages, PATCH/DELETE /api/v1/booking-slot-packages/:id, POST …/from-template — /settings/spaces/[spaceId]/slots/[slotId]/packages |
| Venue admin — templates | GET/POST/PATCH/DELETE /api/v1/commercial-package-templates — /settings/commercial/templates |
| Venue admin — settlement | `GET |
| Promoter read | GET /api/venues/booking-slots?spaceId=&venueCompanyId=&visibility=public (public packages embedded per slot) |
| Promoter book | POST with { bookingDate, slotIds, slotPackageSelections, spaceIds, venueId, … } |
Migrations: 000020_venue_booking_slots.sql (slots + booking window columns); 000021_venues_currency.sql (canonical venues.currency); 000031_slot_commercial_packages.sql (templates, slot packages, booking snapshots, settlements; backfill Standard package; drop slot price_cents).
Venue currency. Commercial pricing currency is not operator-editable. Each venue row stores canonical venues.currency (ISO 4217). All package term amounts and settlement calculations use venue currency.
Booking surface rebuild (2026-06-28)
Section titled “Booking surface rebuild (2026-06-28)”The booking surface gained five hold tiers, a full operational edit endpoint, and an Artists-enriched counterparty read. See the API page for the exact request/response shapes.
Five hold tiers. Booking status now supports hold_option_1 through hold_option_5 (added hold_option_3/4/5). The venue_bookings.status column is free-text TEXT with no DB CHECK, so no migration was needed. Holds 3–5 occupy a window the same way holds 1–2 do: the occupancy status IN (...) lists in HasAvailabilityConflict (internal/venue/repositories/postgres.go) and the reporting aggregations (reports_postgres.go) include all five hold tiers.
Full booking edit — PUT /api/v1/bookings/:id (guard venue.booking.manage). Edits operational fields only: marketCompanyId, marketPersonId, title, spaceIds, startAt, endAt. Deliberately excludes status (owned by PATCH), finance/event linkage, slots, and pricing. Re-runs the conflict check ignoring the booking’s own row. Terminal bookings (cancelled / closed) are frozen → 409.
Market counterparty lookup — GET /api/v1/bookings/:id/counterparty (guard venue.booking.view). Returns a Counterparty = the local venue-private customer overlay (name, partyType, riskLevel, notes) merged with the canonical Artists/Market party detail (company + optional person: legalName, country, city, email, phone, website, contactName, raw attributes). The new MarketPartyResolver.GetPartyByID calls Market GET /companies/{id} or /people/{id} and is best-effort: when Market is unconfigured or returns 404 it yields nil and the response sets marketLinked:false — the read never fails. Wires Market enrichment into the booking read path. No migration.
Practical engineering rules
Section titled “Practical engineering rules”When working in Backend-Kisum-Venues:
- do not duplicate canonical Artists party records
- do not redefine Finance truth in Venue
- do not weaken auth or
x-orgrules - keep
/internal/*internal-only - treat sleeping-venue import compatibility as real product behavior, not throwaway migration code
- keep docs explicit about what is live versus still target-state
Promoter booking scope metadata
Section titled “Promoter booking scope metadata”Promoter-originated venue bookings carry promoter event, BU, acting-user, and scope metadata. Venue stores it for Promoters BFF filtering; Venue operator authorization remains separate.