Defining events

Describe your events in a Prisma-style schema: payload models, event types, defaults and wire names. SDKs, payloads and event docs are generated from it.

Events are defined in a .repost schema file, the single source everything else is generated from. It describes the shape of each payload, the name of each event, and which SDKs to generate. The generated client, the payloads on the wire, and the event docs your customers read all come from this file, so they stay in sync without any effort on your part.

The syntax follows Prisma's schema language. A complete schema looks like this:

generator sdk {
  language = "typescript"
}
 
enum Currency {
  USD
  EUR
}
 
type Book {
  /// A book was created
  created
  updated
  deleted
}
 
model Author {
  name  String
  email String?
}
 
model Book {
  title    String
  authors  Author[]
  price    Float
  currency Currency
  subtitle String?
}
 
model DeletedBook {
  id String
}
 
event BookCreated {
  type      @type(Book.created)
  data      Book
  timestamp DateTime
}
 
event BookUpdated {
  type      @type(Book.updated)
  data      Book
  timestamp DateTime
}
 
event BookDeleted {
  type      @type(Book.deleted)
  data      DeletedBook
  timestamp DateTime
}

There are four kinds of block. A model describes a payload, a type lists your event names, an event ties a name to a payload, and a generator selects an SDK to emit.

Where a schema line ends up

Consider the line price Float. After generation it appears in three places.

In your code, it becomes a typed field. This is the actual generated TypeScript:

export interface Book {
  title: string;
  authors: Author[];
  price: number;
  currency: Currency;
  subtitle?: string | null;
}
 
export interface Webhooks {
  book: {
    /** A book was created */
    created(input: { customerId: string; data: Book; idempotencyKey?: string }): Promise<SendResult>;
    updated(input: { customerId: string; data: Book; idempotencyKey?: string }): Promise<SendResult>;
    deleted(input: { customerId: string; data: DeletedBook; idempotencyKey?: string }): Promise<SendResult>;
  };
}

Each catalog member became a method, so Book.created is called as webhooks.book.created(...), and the /// comment above the member became the method's documentation. Passing a string as price is a compile error.

On the wire, it becomes a payload field:

{
  "type": "book.created",
  "timestamp": "2026-07-17T12:00:00.000Z",
  "data": {
    "title": "The Pragmatic Programmer",
    "authors": [{ "name": "Andy Hunt" }],
    "price": 42.9,
    "currency": "USD"
  }
}

Fields serialize in declaration order in every language. The wire name of the event is the lowercased catalog name, a dot, and the member: book.created.

In your customers' hands, it becomes documentation. Deploying the schema publishes book.created to your environment with a JSON Schema of the payload and the doc comment as its description. The portal's event catalog and your public event docs render that registry directly.

Models

A model is a list of typed fields:

model Order {
  id        String    @default(cuid())
  reference String    @map("order_reference")
  amount    Float
  express   Boolean   @default(false)
  lines     Line[]
  coupon    String?
  metadata  Json?
  createdAt DateTime  @default(now())
}
 
model Line {
  sku String
  qty Int    @default(1)
}

Field types

TypeGenerated (TypeScript)On the wire
StringstringJSON string
IntnumberJSON number
FloatnumberJSON number
BooleanbooleanJSON boolean
DateTimestringISO-8601, millisecond precision, UTC
JsonJSON valuePassed through as-is
Another modelIts interfaceNested object, same rules
An enumIts typeThe member's wire value

Modifiers compose. String? is optional, Line[] is a list, and String[]? is an optional list.

Absent is not null

Optional fields have three states, and the SDKs keep them separate. A field that is set serializes its value. A field that is absent is omitted from the payload, unless it declares a @default, in which case the default is filled in. An explicit null serializes as null. Consumers can rely on the difference between a missing field and a null one.

Defaults

@default fills a field in when you don't provide a value. It accepts literals (@default("fallback"), @default(1), @default(false), @default(USD)), the send time (@default(now())), and generated ids (@default(uuid()), @default(cuid())).

Wire names

Schema names are for your code. @map sets the name used in the payload:

model Order {
  reference String @map("order_reference")
}

Your code reads order.reference; the payload contains order_reference. Enum members can be renamed the same way: RED @map("scarlet").

Enums

enum Currency {
  USD
  EUR
}

An enum generates a value object and a type (Currency.USD in TypeScript) and serializes as the member name, or the @map value if the member was renamed.

Event types

The type block names your events. Each event block gives one of them a payload:

type Book {
  /// A book was created
  created
  deleted
}
 
event BookCreated {
  type      @type(Book.created)
  data      Book
  timestamp DateTime
}
 
event BookDeleted {
  type      @type(Book.deleted)
  data      DeletedBook
  timestamp DateTime
}

Events in the same catalog can carry different payloads. A deletion usually only needs an id, so it gets a smaller model.

The /// comment on a member is worth writing. It becomes the generated method's documentation, the event's description in your environment, and the description shown in the portal catalog and your public event docs.

A schema can hold any number of catalogs. type Order, type Invoice, and type ApiKey each become their own branch of the method tree, such as apiKey.rotated.

Generators

A generator block emits one SDK. A schema can carry several, so a monorepo can generate every service's client from the same file:

generator api {
  language = "typescript"
}
 
generator payments {
  language = "go"
  output   = "../services/payments/internal/repostclient"
}

Supported languages are typescript, go, python, java, and kotlin. TypeScript writes into node_modules by default. The other languages commit their generated package to the repo and require an output path. Java and Kotlin take a few extra settings for their build plugins, covered in the Java and Kotlin guides.

Validating and formatting

repost schema validate checks the schema without writing anything. repost schema fmt formats it in place. Both exit non-zero on problems, so they work as CI checks. The schema lives in repost/schema.repost, or in a repost/schema/ directory of .repost files once it outgrows a single file; use one layout or the other, not both.

Changing the schema is a versioned operation. The next page covers how changes are recorded and deployed.

Continue