Gamers Lab Docs

2.4 Events

Section 2 — Games

2.4 Events

2.4.1 Event Ingestion

What Events Are

Events are timestamped observations emitted by the game server during or after gameplay. Each event references a definition (the schema of what the event means) and optionally carries a numeric value. Events are append-only — once written, never updated or deleted.

Two event types, each with a dedicated endpoint and a separate ledger table:

TypeEndpointTableScope
Match eventPOST /api/game/eventsgame_app_event_recordsTied to a player within a match session
Player eventPOST /api/game/player-eventsgame_app_player_eventsTied to a player outside of any match

Match events require the player to have a match player record (matchPlayerId) in the referenced match session. Player events have no match requirement.

Both endpoints are authenticated via X-Game-Key and a player bearer JWT. The authenticated player must match the event actor: matchPlayerId must belong to the bearer player for match events, and playerId must match the bearer player for player events.

Ledger Tables

game_app_event_records — match events

ColumnTypeNotes
tenant_iduuidComposite PK
iduuidComposite PK
match_player_iduuidFK → match player record
match_session_iduuidFK → match session
login_session_iduuid?Linked at write time; not re-validated
event_definitions_iduuidFK → resolved event definition
idempotency_keytextRequired; used for deduplication
valuebigint?Optional numeric payload
occurred_attimestamptzDomain event time
created_at_gltimestamptzServer receive time
row_hashtextApplication-computed integrity hash

game_app_player_events — player events

Same shape as above except match_player_id and match_session_id are replaced with player_id.

Batch Submission

Both endpoints accept a batch of events in a single request. Each event is processed independently — a failure on one event does not block others.

LimitValue
Max events per match event request10,000
Max events per player event request10,000

Per-Event Fields

Match event (EventRecordRequest):

FieldRequiredNotes
matchSessionIdYesMust reference an existing match session
matchPlayerIdYesMust exist in the match roster for this session
loginSessionIdNoLinked at write time; not re-validated
eventDefinitionIdNo*Definition lookup by ID
eventKeyNo*Definition lookup by key/value
eventValueNoUsed with eventKey for key/value resolution
idempotencyKeyYesRequired; max 64 chars
valueNoLong integer payload
occurredAtYesDomain time the event occurred

* At least one of eventDefinitionId or eventKey must be provided.

Player event (PlayerEventRecordRequest): Same fields, except matchSessionId and matchPlayerId are replaced by playerId.

Definition Resolution

Each event must resolve to an active event definition. Resolution follows this priority order:

  1. By ID — if eventDefinitionId is provided:

    • Found and active and correct scope → accepted
    • Found but inactive → 409 Conflict
    • Found but wrong scope → 409 Conflict
    • Not found → attempt fallback to key/value if eventKey is also provided; if fallback succeeds, the event is accepted with a warning
  2. By key/value — if only eventKey (and optionally eventValue) is provided:

    • Existing definition found and active and correct scope → accepted
    • Existing definition found but inactive → 409 Conflict
    • Not found + auto-create enabled → new definition created automatically (see §2.4.3)
    • Not found + auto-create disabled → 422 Unprocessable Entity

Scope enforcement:

Definition scopeMatch eventsPlayer events
matchAllowedRejected
playerRejectedAllowed
bothAllowedAllowed

Partial Success Response

records[]               — events accepted and inserted
  index                 int
  eventRecordId         Guid
  eventDefinitionId     Guid
  matchPlayerId         Guid      — (match events only)
  matchSessionId        Guid      — (match events only)
  playerId              Guid      — (player events only)

warnings[]              — events inserted with a non-fatal notice
  index                 int
  message               string

errors[]                — events rejected
  index                 int
  statusCode            int
  error                 string
  eventDefinitionId     Guid?
  eventKey              string?
  eventValue            string?

Returns 201 Created if at least one event was inserted. Returns 422 if zero were inserted.

Idempotency

Event idempotency is database-only. The combination of (tenant_id, occurred_at, idempotency_key) has a unique index. Duplicates are silently dropped via ON CONFLICT DO NOTHING. No Redis-backed response caching (unlike match lifecycle writes). See §2.1.2 for a full comparison.

Rate Limiting and Quota

Both endpoints enforce per-key rate limits configured on the game auth key:

  • Rate limit exceeded429 Too Many Requests with Retry-After header
  • Quota hard stop exceeded429 Too Many Requests with quota context

Rate limits are per-minute and per-hour counters enforced atomically in Redis, per game auth key.

Field Limits

FieldLimit
Events per request10,000
idempotencyKey64 chars
eventKey32 chars
eventValue32 chars
valuelong.MinValue to long.MaxValue

Error Codes

CodeMeaning
201Created — at least one event inserted
400Invalid request
401Missing or invalid game key
404Event definition ID not found (and no key/value fallback)
409Event definition is inactive or wrong scope
422No events inserted (all rejected)
429Rate limit or quota hard stop exceeded

Code References

  • Match event controller: GamersLabRestAPI/Game/Events/Controllers/GameEventRecordsController.cs
  • Player event controller: GamersLabRestAPI/Game/Events/Controllers/GamePlayerEventRecordsController.cs
  • Service interface: GamersLabRestAPI/Game/Events/Services/IEventService.cs
  • Service: GamersLabRestAPI/Game/Events/Services/EventService.cs
  • DTOs: GamersLabRestAPI.Contracts/Game/Events/DTOs/EventRecordDTOs.cs
  • Models: GamersLabRestAPI/Game/Events/Models/EventRecord.cs
  • Batch size caps are plan-driven via tenant-resolved settings: max_match_event_records_per_request and max_player_event_records_per_request
  • Structural limits remain in GamersLabRestAPI/Game/Constants/GameWriteLimits.cs

2.4.2 Event Definitions

What an Event Definition Is

An event definition is the registered schema for an event type. Every event record written during gameplay must reference a definition. Definitions are tenant-scoped and stored in the mutable table game_event_definitions. Unlike event records, definitions can be updated and deactivated.

Definition Fields

FieldRequired on createMutableNotes
eventKeyYesNoShort identifier; max 32 chars
eventValueNoNoOptional qualifier on the key; max 32 chars
eventScopeNoYesmatch, player, or both; defaults to match
eventCategoryNoYesGrouping label; max 32 chars
eventDescriptionNoYesHuman-readable description; max 512 chars
isActiveNoYesDefaults to true; inactive definitions reject new events

eventKey and eventValue are fixed after creation. They form the logical identity of the definition. To change them, deactivate the existing definition and create a new one.

Uniqueness

The combination of (tenantId, eventKey, eventValue) must be unique. A duplicate create returns 409 Conflict.

Scope Values

ValueWhere it can be used
matchMatch event writes only
playerPlayer event writes only
bothEither type

Active vs Inactive

Setting isActive = false deactivates the definition. Ingestion attempts against an inactive definition return 409 Conflict. Inactive definitions are hidden from list queries by default but can be included with ?includeInactive=true.

Authentication

All definition management endpoints require a SaaS JWT with TenantOwnerOrAdmin policy. Game keys (X-Game-Key) are not accepted.

Endpoints

List Definitions

GET /api/bus_tenants/{tenantId}/event-definitions

Pass ?includeInactive=true to include deactivated definitions. Pass ?paged=true&limit=200&offset=0 to return a paged response envelope instead of the legacy array response.

Response: List<EventDefinitionResponse>

id                  Guid
eventKey            string
eventValue          string?
eventScope          string
eventCategory       string?
eventDescription    string?
isActive            bool
createdAt           DateTime
updatedAt           DateTime

Paged response: PagedEventDefinitionsResponse

items[]             EventDefinitionResponse[]
limit               int
offset              int
totalCount          int

Create Definition

POST /api/bus_tenants/{tenantId}/event-definitions

Request: CreateEventDefinitionRequest

FieldRequiredNotes
eventKeyYesMax 32 chars
eventValueNoMax 32 chars
eventScopeNomatch, player, or both; defaults to match
eventCategoryNoMax 32 chars
eventDescriptionNoMax 512 chars
isActiveNoDefaults to true

Returns 201 Created with EventDefinitionResponse. Returns 409 on duplicate key/value. Audit logged on success.

Update Definition

PATCH /api/bus_tenants/{tenantId}/event-definitions/{definitionId}

Only mutable fields can be updated. eventKey and eventValue are silently ignored.

Request: UpdateEventDefinitionRequesteventScope, eventCategory, eventDescription, isActive (all optional).

Returns updated EventDefinitionResponse. Audit logged with before/after diff.

Export Definitions (CSV)

GET /api/bus_tenants/{tenantId}/event-definitions/export

Downloads all definitions as event-definitions.csv. Pass ?includeInactive=true to include deactivated definitions.

CSV columns: id, event_key, event_value, event_scope, event_category, event_description, is_active

Import Definitions (CSV)

POST /api/bus_tenants/{tenantId}/event-definitions/import
Content-Type: multipart/form-data

Import limits: max file size 2 MB; max parsed rows 5,000.

Required CSV columns: event_key, event_value. Optional: id (or event_definition_id), event_scope, event_category, event_description, is_active.

Column names are case-insensitive. UTF-8 BOM stripped automatically. Any non-empty unknown header causes the entire upload to fail. Duplicate recognized headers are rejected. If overwriteExisting=true, the CSV header must also include id (or event_definition_id). Form field: overwriteExisting (true / false).

Row behavior:

overwriteExistingRow has id?Result
falseYes / NoCreate-only import. id is ignored. Existing duplicates return row errors.
trueNoRow error — overwrite mode requires id on every non-empty row
trueYesUpdates mutable fields when the ID matches an existing definition. If the row's eventKey + eventValue matches a different existing definition, the row is rejected as a duplicate
trueYes but missingError — ID not found for this tenant

Response: EventDefinitionCsvImportResponse

totalRows       int
created         int
updated         int
failed          int
errors[]
  rowNumber     int
  message       string
  definitionId  string?
  eventKey      string?
  eventValue    string?
  eventScope    string?
  eventCategory string?
  eventDescription string?
  isActive      bool?

Returns 200 OK regardless of individual row errors. Returns 400 Bad Request if the CSV is structurally invalid (empty headers, unknown headers, missing required headers, missing id header in overwrite mode, duplicate recognized headers, malformed quoting, empty file, file larger than 2 MB, or more than 5,000 parsed rows).

Audit Logging

OperationTrigger
Definition createdPOST — manual creation
Definition updatedPATCH — any field change; includes before/after diff
Definition auto-createdIngest — when auto-create is enabled

Auto-created definitions are distinguished by source: "auto_create" in the audit record.

Code References

  • Controller: GamersLabRestAPI/Game/Events/Controllers/EventDefinitionsController.cs
  • Settings controller: GamersLabRestAPI/Game/Events/Controllers/EventDefinitionSettingsController.cs
  • Service: GamersLabRestAPI/Game/Events/Services/EventService.cs
  • DTOs: GamersLabRestAPI/Game/Events/DTOs/EventDefinitionDTOs.cs
  • Model: GamersLabRestAPI/Game/Events/Models/EventDefinition.cs
  • CSV parser: GamersLabRestAPI/Game/Events/Helpers/EventDefinitionCsvParser.cs
  • CSV writer: GamersLabRestAPI/Game/Events/Helpers/EventDefinitionCsvWriter.cs

2.4.3 Auto-Create & Definition Settings

What Auto-Create Does

By default, every event submitted during ingestion must reference an existing definition. If no matching definition is found, the event is rejected with 422.

When auto-create is enabled, the platform will automatically create a new definition the first time an unknown eventKey/eventValue pair is seen in an event batch. The event is then accepted as if the definition had always existed.

This is an opt-in, per-tenant setting stored under event_definitions.auto_create. It defaults to false.

Endpoints

Both endpoints require a SaaS JWT with TenantOwnerOrAdmin policy.

Get Settings

GET /api/bus_tenants/{tenantId}/event-definitions/settings

Response: EventDefinitionSettingsResponse

autoCreate      bool

Update Settings

PATCH /api/bus_tenants/{tenantId}/event-definitions/settings

Request: UpdateEventDefinitionSettingsRequest

autoCreate      bool

The setting takes effect immediately. Due to a 5-minute cache TTL, newly enabled or disabled settings may take up to 5 minutes to be seen by active event ingestion workers.

Auto-Created Definition Properties

FieldValue
eventKeyNormalized from the event batch entry
eventValueNormalized from the event batch entry (may be null)
eventScopeInferred from the event type (match or player)
eventCategoryNot set
eventDescriptionNot set
isActivetrue

Auto-created definitions have no category or description. They can be updated manually via PATCH after creation.

Race Condition Handling: If two concurrent batches both reference the same new key/value and both trigger auto-create simultaneously, the database unique constraint catches the second insert. The second request re-fetches the definition created by the first. No duplicate definitions are produced and no error is returned.

Audit Logging: Every auto-created definition is recorded in the tenant audit log with source: "auto_create".

Trade-Offs

Pros:

  • Fast prototyping — emit new event types without pre-registering them.
  • Reduced friction during early development where the event schema is still evolving.

Cons:

  • Reduced security. A leaked game key with auto-create enabled allows the holder to inject arbitrary new event definitions into the tenant's schema — not just event records. With auto-create disabled, a leaked key's damage is limited to event data.
  • No schema governance. No review or approval step for auto-created definitions. Typos, misformatted keys, or accidental events from a buggy client silently produce new definitions.
  • Analytics noise. Auto-created definitions have no category or description, making dashboards harder to interpret.
  • Hard to distinguish planned vs accidental. The audit log helps, but the schema can become cluttered.

Recommendation: Enable auto-create during active development. Disable before a production game launch and manage definitions explicitly via the API or CSV import/export.

Caching

The auto-create setting is cached per tenant with a 5-minute TTL (event-def:auto-create:v2:{tenantId}).

Code References

  • Settings controller: GamersLabRestAPI/Game/Events/Controllers/EventDefinitionSettingsController.cs
  • Service: GamersLabRestAPI/Game/Events/Services/EventService.cs
    • GetAutoCreateSettingAsync — reads setting with cache
    • RecordAutoCreatedDefinitionAuditAsync — writes audit record on auto-create
  • DTOs: GamersLabRestAPI/Game/Events/DTOs/EventDefinitionDTOs.cs
    • EventDefinitionSettingsResponse, UpdateEventDefinitionSettingsRequest

On this page