Event Schemas#

Register your custom event types and attach JSON Schemas so every producer writes the same payload shape into the shared event ledger.

Why register schemas?#

Events in Outeract share one ledger: messages, system events, and your custom events all land in the same stream and flow out through the same webhook subscriptions. A payload contract keeps that stream trustworthy:

  • Producers can’t drift. An order service that forgets tracking_number gets an error, not a silent bad row.
  • Consumers can rely on shape. Webhook handlers and dashboards read payload.order_id without defensive checks.
  • Types are self-documenting. The schema shows up in the console next to the event type it governs.

Schemas are optional; an event type without one accepts any valid JSON.

The EventSchema record#

Each event type is an EventSchema record:

FieldDescription
idUUID; events reference their type by ID, so renames are instant
nameType name, e.g. order.shipped (lowercase letters, digits, dots, underscores)
jsonSchemaOptional JSON Schema (Draft-07) validated against payload
enforceValidationtrue (default): invalid payloads are rejected; false: warn but accept
appIdSet for your app’s types; null for built-in global types (message.*, etc.)

Creating an event with an unregistered type auto-creates the type for your app, with no schema attached. Registering explicitly is how you add the contract.

Reserved prefixes#

Built-in system types use reserved prefixes that your custom types can’t start with: file.*, link_code.*, message.*, user.*

Anything else is yours: order.shipped, payment.stripe.succeeded, analytics.page_view.

Register a type with a schema#

Event schemas are managed in the console, under each application’s Event Types section. Create a type named order.shipped, turn on enforced validation, and give it this JSON Schema:

{
  "type": "object",
  "required": ["order_id", "tracking_number"],
  "properties": {
    "order_id": { "type": "string", "pattern": "^order_[a-zA-Z0-9]+$" },
    "tracking_number": { "type": "string" },
    "carrier": { "type": "string", "enum": ["ups", "fedex", "dhl", "royal_mail"] },
    "shipped_at": { "type": "string", "format": "date-time" }
  }
}

To register a type as a plain identifier (no validation yet), create it without a schema.

Inspect registered types#

The Event Types list shows every type available to the application (the ones you registered plus the global built-ins) with each type’s schema, enforcement setting, creation date and per-type stats. Names resolve against your app first, then the global built-ins.

What happens on validation failure#

When you create an event, Outeract resolves its type and validates the payload:

  1. No registered type → the type is auto-created and the event is accepted (any JSON).
  2. Type registered, no jsonSchema → accepted.
  3. Schema exists, enforceValidation: true → invalid payloads are rejected: the mutation returns a GraphQL error and no event is stored.
  4. Schema exists, enforceValidation: false → validation errors are logged, but the event is stored anyway (warn-but-accept).

A rejected createEvent fails like this:

mutation {
  createEvent(
    userId: "USER_ID"
    eventType: "order.shipped"
    payload: { order_id: "order_12345" }   # missing tracking_number
  ) {
    id
  }
}
{
  "errors": [
    {
      "message": "Invalid payload for order.shipped: tracking_number: 'tracking_number' is a required property"
    }
  ]
}
Validation applies at event creation. Changing a schema later does not re-validate or reject events already in the ledger.

Evolving a schema#

A type’s detail page has a schema builder for editing the JSON Schema, toggling enforcement, renaming and deleting. To add a new optional field, extend the properties:

{
  "type": "object",
  "required": ["order_id", "tracking_number"],
  "properties": {
    "order_id": { "type": "string" },
    "tracking_number": { "type": "string" },
    "carrier": { "type": "string" },
    "shipped_at": { "type": "string", "format": "date-time" },
    "estimated_delivery": { "type": "string", "format": "date" }
  }
}

Clearing the schema leaves the type in place as a plain identifier. Renaming is instant, because events reference their type by ID rather than by name.

Guidance#

  • Add optional fields freely. New optional properties never break existing producers.
  • Tighten in two steps. To add a required field, ship the producer change first, then update the schema, or turn enforcement off during the migration and watch the logs before re-enabling.
  • Rename types, don’t fork them. Renaming keeps history intact; creating a parallel type splits your ledger. Remember to update webhook subscription patterns that matched the old name.
  • Don’t loosen contracts silently. Consumers depend on the schema, so treat removals of required fields as breaking changes for your webhook handlers.

See Also#