Unified Schema Reference

All data synced from connected providers is stored in a normalised PostgreSQL schema. This document covers the entity relationships, table definitions, and field-level documentation.


Entity-Relationship Diagram

erDiagram
    tenants {
        uuid tenant_id PK
        text name
        text plan
        timestamptz created_at
    }

    api_keys {
        uuid key_id PK
        text key_hash
        uuid tenant_id FK
        text name
        text[] scopes
        timestamptz created_at
        timestamptz last_used_at
        timestamptz revoked_at
    }

    provider_connections {
        uuid connection_id PK
        uuid tenant_id FK
        text provider
        text status
        timestamptz last_synced_at
        jsonb credentials
        int events_count
    }

    events {
        uuid event_id PK
        uuid tenant_id FK
        text provider
        text provider_event_id
        text name
        text description
        text venue
        timestamptz start_date
        timestamptz end_date
        int capacity
        text status
        timestamptz created_at
    }

    ticket_types {
        uuid ticket_type_id PK
        uuid event_id FK
        uuid tenant_id FK
        text name
        int price_cents
        text currency
        int capacity
    }

    ticket_sales {
        uuid sale_id PK
        uuid tenant_id FK
        uuid event_id FK
        uuid ticket_type_id FK
        text provider
        int quantity
        int gross_cents
        int net_cents
        int fees_cents
        text currency
        timestamptz sold_at
        text purchase_channel
    }

    attendees {
        uuid attendee_id PK
        uuid tenant_id FK
        uuid event_id FK
        uuid ticket_type_id FK
        text country_code
        text purchase_channel
        timestamptz checked_in_at
        timestamptz registered_at
    }

    tenants ||--o{ api_keys : "owns"
    tenants ||--o{ provider_connections : "has"
    tenants ||--o{ events : "has"
    events ||--o{ ticket_types : "has"
    events ||--o{ ticket_sales : "has"
    events ||--o{ attendees : "has"
    ticket_types ||--o{ ticket_sales : "typed by"
    ticket_types ||--o{ attendees : "typed by"

(ASCII fallback below for environments without Mermaid support)

tenants
  ├── api_keys          (1:N)
  ├── provider_connections (1:N)
  └── events            (1:N)
        ├── ticket_types (1:N)
        ├── ticket_sales (1:N)  ──── ticket_type_id → ticket_types
        └── attendees    (1:N)  ──── ticket_type_id → ticket_types

Table definitions

tenants

Top-level account isolation. Every row in every other table carries a tenant_id that scopes queries.

ColumnTypeNullableExampleDescription
tenant_iduuidNof47ac10b-...Primary key, generated by gen_random_uuid()
nametextNoLumière Events SARLDisplay name of the organisation
plantextNoproOne of free, pro, business
created_attimestamptzNo2025-03-01T09:00:00ZAccount creation timestamp

api_keys

API keys used by the MCP server and REST adapter for authentication.

ColumnTypeNullableExampleDescription
key_iduuidNoa3f1...Primary key
key_hashtextNosha256:abc...SHA-256 of the raw key — raw key is never stored
tenant_iduuidNof47ac10b-...Owner tenant
nametextYesClaude Desktop keyHuman-readable label
scopestext[]No{read}Permission scopes (currently read only)
created_attimestamptzNo2025-06-01T10:00:00ZCreation timestamp
last_used_attimestamptzYes2025-08-01T15:42:00ZUpdated on every successful auth
revoked_attimestamptzYesnullSet when key is revoked; null = active

Index: api_keys_hash_idx on (key_hash) WHERE revoked_at IS NULL — fast auth lookup.


provider_connections

One row per (tenant, provider) pair. Stores connection status and encrypted credentials.

ColumnTypeNullableExampleDescription
connection_iduuidNob1c2...Primary key
tenant_iduuidNof47ac10b-...Owner tenant
providertextNoeventbriteProvider identifier (see Connector Reference)
statustextNoconnectedOne of connected, syncing, error
last_synced_attimestamptzYes2025-08-01T10:30:00ZTimestamp of last successful sync
credentialsjsonbNo{"access_token": "..."}Encrypted provider credentials (AES-256-CBC) — never readable in plaintext from the DB
events_countintegerNo42Cached count of synced events

Constraint: UNIQUE (tenant_id, provider) — one connection per provider per tenant.


sync_logs

One row per sync run. Stores coarse run status plus structured field-coverage details for provider-specific enrichments.

ColumnTypeNullableExampleDescription
log_iduuidNoab12...Primary key
tenant_iduuidNof47ac10b-...Owner tenant
provider_idtextNoweezeventProvider identifier
connection_iduuidNob1c2...FK-like reference to the synced connection
started_attimestamptzNo2026-05-31T10:00:00ZSync start timestamp
completed_attimestamptzYes2026-05-31T10:01:42ZSync finish timestamp
records_syncedintegerYes487Total rows upserted during the run
statustextNosuccessOne of running, success, error
errortextYesnullTop-level sync failure message when the run errors
detailsjsonbNo{"field_coverage":{"payment_method":{"populated":42,"skipped":3}}}Structured per-field counts, stage outcomes, incremental cursor, and provider-gap notes

events

Normalised event records, one row per provider event per tenant.

ColumnTypeNullableExampleDescription
event_iduuidNoc3d4...Spekta-internal primary key
tenant_iduuidNof47ac10b-...Owner tenant
providertextNoeventbriteSource provider
provider_event_idtextNo123456789Provider's own event ID
nametextNoSummer Beats FestivalEvent name
descriptiontextYesAnnual music festival...Event description (may be long)
venuetextYesParc de la Villette, ParisVenue name or address string
start_datetimestamptzNo2025-08-15T18:00:00ZEvent start (stored in UTC)
end_datetimestamptzYes2025-08-16T02:00:00ZEvent end (nullable for open-ended)
capacityintegerYes5000Total capacity (null if not set by provider)
statustextNoupcomingOne of upcoming, live, past, cancelled
created_attimestamptzNo2025-03-01T09:00:00ZRecord insertion time

Constraint: UNIQUE (tenant_id, provider, provider_event_id) — deduplication on upsert.
Indexes:

  • events_tenant_start on (tenant_id, start_date) — range queries
  • events_tenant_status on (tenant_id, status) — status filter

Extended fields (migration 002_extend_pivot_schema.sql)

ColumnTypeNullableDescription
citytextYesEvent city, usually sourced from provider venue metadata
country_codetextYesProvider country / country code (default FR)
categorytextYesNormalised event category when the provider exposes one
expected_capacityintegerYesOrganiser target capacity, distinct from hard capacity
online_eventbooleanNoWhether the event is virtual / online-only

ticket_types

Ticket categories within an event (e.g. "Early Bird", "VIP", "General Admission").

ColumnTypeNullableExampleDescription
ticket_type_iduuidNod5e6...Primary key
event_iduuidNoc3d4...Parent event
tenant_iduuidNof47ac10b-...Owner tenant (denormalised for fast tenant-scoped queries)
nametextNoVIPTicket type name
price_centsintegerNo8500Face value in minor currency units (e.g. 8500 = €85.00)
currencytextNoEURISO 4217 currency code
capacityintegerYes200Allocation for this tier (null = unlimited)
sale_starttimestamptzYes2025-05-01T09:00:00ZWhen this ticket tier goes on sale
sale_endtimestamptzYes2025-08-14T23:59:59ZWhen this ticket tier stops selling
is_early_birdbooleanNotrueFlag for early-bird tiers
is_vipbooleanNotrueFlag for VIP / premium tiers
sort_orderintegerNo20Provider ordering / display order within the event

Indexes:

  • ticket_types_event on (event_id).
  • ticket_types_event_sort_idx on (event_id, sort_order).

ticket_sales

Individual sale records. One row = one transaction line (may cover multiple tickets via quantity).

ColumnTypeNullableExampleDescription
sale_iduuidNoe7f8...Primary key
tenant_iduuidNof47ac10b-...Owner tenant
event_iduuidNoc3d4...Parent event
ticket_type_iduuidYesd5e6...Ticket type (null if provider does not return type info)
providertextNoweezeventSource provider
quantityintegerNo2Number of tickets in this transaction
gross_centsintegerNo17000Gross revenue in minor units (includes fees)
net_centsintegerNo15300Net revenue (after provider fees)
fees_centsintegerNo1700Provider fee component
currencytextNoEURISO 4217 currency code
sold_attimestamptzNo2025-07-01T14:22:00ZSale timestamp (UTC)
purchase_channeltextYeswebe.g. web, app, box_office
refund_statustextNopartialOne of none, partial, full
refunded_attimestamptzYes2025-07-02T10:15:00ZWhen the provider reports the refund happened
refund_amount_centsintegerNo8500Refunded amount in minor units
promo_code_iduuidYesf1a2...FK to promo_codes when a code was applied
utm_sourcetextYesinstagramMarketing source tag
utm_mediumtextYespaid_socialMarketing medium tag
utm_campaigntextYessummer_launchMarketing campaign tag
payment_methodtextYescardOne of card, bank_transfer, cash, free, other

Indexes:

  • ticket_sales_tenant_event on (tenant_id, event_id, sold_at) — per-event time-series
  • ticket_sales_tenant_date on (tenant_id, sold_at) — cross-event date range
  • ticket_sales_event_date on (event_id, sold_at) — event-level analytics
  • ticket_sales_promo_code_idx on (promo_code_id) — joins to promo-code analytics
  • ticket_sales_refund_status_idx on (tenant_id, event_id, refund_status, sold_at) — refund analysis

attendees

Per-attendee records. No PII is stored — names and emails are never persisted. Demographic data is limited to anonymised fields.

ColumnTypeNullableExampleDescription
attendee_iduuidNog9h0...Primary key
tenant_iduuidNof47ac10b-...Owner tenant
event_iduuidNoc3d4...Parent event
ticket_type_iduuidYesd5e6...Ticket type
country_codetextYesFRISO 3166-1 alpha-2 country code
purchase_channeltextYesappPurchase channel
checked_in_attimestamptzYes2025-08-15T19:04:00ZCheck-in time (null = not checked in)
registered_attimestamptzYes2025-06-15T11:30:00ZRegistration timestamp

promo_codes

Discount and promo-code definitions, scoped by tenant and optionally by event.

ColumnTypeNullableDescription
promo_code_iduuidNoPrimary key
tenant_iduuidNoOwner tenant
event_iduuidYesEvent scope; null means all events
codetextNoThe promo code entered by the buyer
discount_typetextNopercent or fixed_cents
discount_valueintegerNoPercent value or cents value, depending on discount_type
max_usesintegerYesUsage cap (null = unlimited)
used_countintegerNoObserved / synced uses
valid_fromtimestamptzYesFirst valid timestamp
valid_untiltimestamptzYesExpiry timestamp
created_attimestamptzNoRow creation time

Indexes:

  • promo_codes_tenant_event_idx on (tenant_id, event_id).
  • promo_codes_event_idx on (event_id).

check_in_logs

Granular scan events. Unlike attendees.checked_in_at, this table keeps each synced scan row.

ColumnTypeNullableDescription
check_in_log_iduuidNoPrimary key
tenant_iduuidNoOwner tenant
event_iduuidNoParent event
attendee_iduuidYesLinked attendee when one can be resolved
ticket_type_iduuidYesLinked ticket type when one can be resolved
scanned_attimestamptzNoScan timestamp
gatetextYesGate / checkpoint label
scan_resulttextNoOne of valid, already_used, invalid, not_found
device_idtextYesScanner / device identifier

Indexes:

  • check_in_logs_tenant_event_scanned_idx on (tenant_id, event_id, scanned_at).
  • check_in_logs_tenant_event_scan_result_idx on (tenant_id, event_id, scan_result).
  • check_in_logs_attendee_idx on (attendee_id).
  • check_in_logs_ticket_type_idx on (ticket_type_id).

waitlist

Demand capture for sold-out or gated ticket tiers.

ColumnTypeNullableDescription
waitlist_iduuidNoPrimary key
tenant_iduuidNoOwner tenant
event_iduuidNoParent event
ticket_type_iduuidYesTicket tier the visitor joined for
joined_attimestamptzNoWaitlist join timestamp
convertedbooleanNoWhether the waitlisted buyer eventually converted
converted_attimestamptzYesConversion timestamp

Indexes:

  • waitlist_tenant_event_idx on (tenant_id, event_id).
  • waitlist_ticket_type_idx on (ticket_type_id).

mv_daily_sales

Materialized daily sales aggregate used for fast rollups by date, event, and ticket type.

ColumnTypeDescription
tenant_iduuidOwner tenant
event_iduuidParent event
sale_datedateUTC sale date derived from ticket_sales.sold_at
ticket_type_iduuidTicket type
ordersbigintCount of synced sale rows
tickets_soldbigintSum of quantity
gross_centsbigintSum of gross revenue
net_centsbigintSum of net revenue
refunded_qtybigintQuantity associated with refunded sales
refunded_centsbigintSum of refunded minor units

Index: mv_daily_sales_tenant_event_date_ticket_idx on (tenant_id, event_id, sale_date, ticket_type_id).


Provider Field Coverage

Field familyWeezeventWelkomNotes
events.city, events.country_code⚠️ PartialWeezevent event listing does not expose structured venue fields in the current connector; Welkom does.
events.categoryLeft null until a provider exposes a stable category field in the synced endpoints.
events.expected_capacityNot exposed by the currently synced provider endpoints.
events.online_eventDefaults to false; no provider signal is mapped yet.
ticket_types.sale_start, ticket_types.sale_endWeezevent /tickets exposes start_sale / end_sale; the engine persists them when present on the ticket payload.
ticket_types.is_early_bird, ticket_types.is_vip✅ Heuristic✅ HeuristicDerived from ticket names when the provider does not send explicit flags.
ticket_types.sort_orderWelkom preserves sales-breakdown order.
ticket_sales.refund_*✅ PartialWeezevent maps sale-level refund status plus timestamps/amounts when the participant payload exposes them; missing provider values are logged in sync_logs.details.
ticket_sales.promo_code_id + promo_codes✅ PartialWeezevent now prefers a dedicated promo-code endpoint and falls back to participant-derived usage when that endpoint is unavailable.
ticket_sales.utm_*Not exposed by the currently integrated provider endpoints.
ticket_sales.payment_method⚠️ PartialWeezevent stores explicit payment-method fields when available and otherwise falls back to origin-based heuristics.
check_in_logs✅ PartialWeezevent now prefers a dedicated scan-log endpoint and falls back to participant control-status scan data when that endpoint is unavailable.
waitlistTable exists, but no current connector populates it.

Weezevent Mapping

Weezevent field / endpointSpekta targetNotes
GET /participant/liststatus = refundedticket_sales.refund_status = 'full'Also applies when refund markers resolve to a full refund.
GET /participant/liststatus = partial_refundticket_sales.refund_status = 'partial'Falls back to amount-based heuristics when only partial refund amounts are exposed.
GET /participant/list → other statuses / missing refund markersticket_sales.refund_status = 'none'Default for non-refunded sales.
GET /participant/listrefund_date / last_updateticket_sales.refunded_atUses the provider refund timestamp when present; otherwise the field remains NULL.
GET /participant/listrefund_amount / numeric refundticket_sales.refund_amount_centsAmount is converted to minor units in the sync engine.
GET /events/{event_id}/promo-codes or GET /event/{event_id}/promo-codespromo_codes tableMaps code, discount type/value, usage caps, usage counts, and validity windows.
GET /participant/listpromo_codeticket_sales.promo_code_idUsed both as a direct sale link and as fallback promo-code discovery when the dedicated endpoint is unavailable.
GET /events/{event_id}/scannings or GET /event/{event_id}/scanningscheck_in_logs tableMaps scan timestamp, gate, scan result, and device/operator identifiers.
GET /participant/listcontrol_status.scan_date / date_check_incheck_in_logs fallback and attendees.checked_in[_at]Used when no dedicated scan-log endpoint is available.
GET /participant/listpayment_method / payment_type / originticket_sales.payment_methodExplicit values win; origin-based heuristics backfill card, cash, bank_transfer, free, or other.
GET /participant/listutm_source / utm_medium / utm_campaignticket_sales.utm_source, utm_medium, utm_campaignLeft NULL when Weezevent does not expose these fields.
GET /ticketsstart_sale, end_saleticket_types.sale_start, ticket_types.sale_endTicket payload is flattened recursively across categories/sub-categories.
GET /ticketsnameticket_types.is_early_bird, ticket_types.is_vipHeuristic inference for early bird / prévente / tarif anticipé and vip / premium / gold / platinum.

Provider Gaps

  • Weezevent’s public docs enumerate /events, /tickets, /participant/list, and scan-related endpoints, but do not document UTM fields; ticket_sales.utm_* remains NULL unless those keys appear in the payload.
  • Refund timestamps and amounts are not guaranteed on every participant payload; missing values are logged as skipped in sync_logs.details.
  • Promo-code and scan-log coverage can fall back to participant-derived observations if the dedicated endpoint is unavailable for the connected account.

Notes on data types

Money / amounts

All monetary amounts are stored as integer minor units (cents, pence, etc.). The currency column on the same row indicates the ISO 4217 code. When querying via MCP tools, amounts are returned in major units (e.g. 85.00) in the currency you specify (default EUR).

Timestamps

All timestamps are stored as timestamptz in UTC. The sync engine converts provider-native representations (UNIX seconds, UNIX milliseconds, ISO 8601 strings, local timestamps) to UTC before persisting.

Credentials encryption

provider_connections.credentials is an AES-256-CBC encrypted JSONB blob. The encryption key (ENCRYPTION_KEY env var) is never stored in the database. Without the key, the JSONB is unreadable ciphertext.