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:
| Type | Endpoint | Table | Scope |
|---|---|---|---|
| Match event | POST /api/game/events | game_app_event_records | Tied to a player within a match session |
| Player event | POST /api/game/player-events | game_app_player_events | Tied 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
| Column | Type | Notes |
|---|---|---|
tenant_id | uuid | Composite PK |
id | uuid | Composite PK |
match_player_id | uuid | FK → match player record |
match_session_id | uuid | FK → match session |
login_session_id | uuid? | Linked at write time; not re-validated |
event_definitions_id | uuid | FK → resolved event definition |
idempotency_key | text | Required; used for deduplication |
value | bigint? | Optional numeric payload |
occurred_at | timestamptz | Domain event time |
created_at_gl | timestamptz | Server receive time |
row_hash | text | Application-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.
| Limit | Value |
|---|---|
| Max events per match event request | 10,000 |
| Max events per player event request | 10,000 |
Per-Event Fields
Match event (EventRecordRequest):
| Field | Required | Notes |
|---|---|---|
matchSessionId | Yes | Must reference an existing match session |
matchPlayerId | Yes | Must exist in the match roster for this session |
loginSessionId | No | Linked at write time; not re-validated |
eventDefinitionId | No* | Definition lookup by ID |
eventKey | No* | Definition lookup by key/value |
eventValue | No | Used with eventKey for key/value resolution |
idempotencyKey | Yes | Required; max 64 chars |
value | No | Long integer payload |
occurredAt | Yes | Domain 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:
-
By ID — if
eventDefinitionIdis 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
eventKeyis also provided; if fallback succeeds, the event is accepted with a warning
-
By key/value — if only
eventKey(and optionallyeventValue) 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 scope | Match events | Player events |
|---|---|---|
match | Allowed | Rejected |
player | Rejected | Allowed |
both | Allowed | Allowed |
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 exceeded →
429 Too Many RequestswithRetry-Afterheader - Quota hard stop exceeded →
429 Too Many Requestswith quota context
Rate limits are per-minute and per-hour counters enforced atomically in Redis, per game auth key.
Field Limits
| Field | Limit |
|---|---|
| Events per request | 10,000 |
idempotencyKey | 64 chars |
eventKey | 32 chars |
eventValue | 32 chars |
value | long.MinValue to long.MaxValue |
Error Codes
| Code | Meaning |
|---|---|
| 201 | Created — at least one event inserted |
| 400 | Invalid request |
| 401 | Missing or invalid game key |
| 404 | Event definition ID not found (and no key/value fallback) |
| 409 | Event definition is inactive or wrong scope |
| 422 | No events inserted (all rejected) |
| 429 | Rate 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_requestandmax_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
| Field | Required on create | Mutable | Notes |
|---|---|---|---|
eventKey | Yes | No | Short identifier; max 32 chars |
eventValue | No | No | Optional qualifier on the key; max 32 chars |
eventScope | No | Yes | match, player, or both; defaults to match |
eventCategory | No | Yes | Grouping label; max 32 chars |
eventDescription | No | Yes | Human-readable description; max 512 chars |
isActive | No | Yes | Defaults 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
| Value | Where it can be used |
|---|---|
match | Match event writes only |
player | Player event writes only |
both | Either 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-definitionsPass ?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 DateTimePaged response: PagedEventDefinitionsResponse
items[] EventDefinitionResponse[]
limit int
offset int
totalCount intCreate Definition
POST /api/bus_tenants/{tenantId}/event-definitionsRequest: CreateEventDefinitionRequest
| Field | Required | Notes |
|---|---|---|
eventKey | Yes | Max 32 chars |
eventValue | No | Max 32 chars |
eventScope | No | match, player, or both; defaults to match |
eventCategory | No | Max 32 chars |
eventDescription | No | Max 512 chars |
isActive | No | Defaults 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: UpdateEventDefinitionRequest — eventScope, eventCategory, eventDescription,
isActive (all optional).
Returns updated EventDefinitionResponse. Audit logged with before/after diff.
Export Definitions (CSV)
GET /api/bus_tenants/{tenantId}/event-definitions/exportDownloads 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-dataImport 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:
overwriteExisting | Row has id? | Result |
|---|---|---|
false | Yes / No | Create-only import. id is ignored. Existing duplicates return row errors. |
true | No | Row error — overwrite mode requires id on every non-empty row |
true | Yes | Updates 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 |
true | Yes but missing | Error — 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
| Operation | Trigger |
|---|---|
| Definition created | POST — manual creation |
| Definition updated | PATCH — any field change; includes before/after diff |
| Definition auto-created | Ingest — 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/settingsResponse: EventDefinitionSettingsResponse
autoCreate boolUpdate Settings
PATCH /api/bus_tenants/{tenantId}/event-definitions/settingsRequest: UpdateEventDefinitionSettingsRequest
autoCreate boolThe 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
| Field | Value |
|---|---|
eventKey | Normalized from the event batch entry |
eventValue | Normalized from the event batch entry (may be null) |
eventScope | Inferred from the event type (match or player) |
eventCategory | Not set |
eventDescription | Not set |
isActive | true |
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.csGetAutoCreateSettingAsync— reads setting with cacheRecordAutoCreatedDefinitionAuditAsync— writes audit record on auto-create
- DTOs:
GamersLabRestAPI/Game/Events/DTOs/EventDefinitionDTOs.csEventDefinitionSettingsResponse,UpdateEventDefinitionSettingsRequest