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#
| Type | Generated (TypeScript) | On the wire |
|---|---|---|
String | string | JSON string |
Int | number | JSON number |
Float | number | JSON number |
Boolean | boolean | JSON boolean |
DateTime | string | ISO-8601, millisecond precision, UTC |
Json | JSON value | Passed through as-is |
| Another model | Its interface | Nested object, same rules |
| An enum | Its type | The 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.