> ## Documentation Index
> Fetch the complete documentation index at: https://repost.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Define Webhook Schemas with the Repost CLI

> Use repost schema to scaffold, validate, format, version, and generate a type-safe TypeScript SDK and JSON Schema for your webhook events.

export const RepostHighlight = () => {
  useEffect(() => {
    const colors = {
      comment: ['#116329', '#6A9955'],
      constant: ['#0550AE', '#9CDCFE'],
      keyword: ['#CF222E', '#569CD6'],
      name: ['#953800', '#4EC9B0'],
      string: ['#0A3069', '#CE9178'],
      type: ['#0550AE', '#4EC9B0'],
      variable: ['#1F2328', '#9CDCFE']
    };
    const tokenPattern = /"[^"\n]*"|@[A-Za-z_]\w*|\b(?:String|Int|Float|Boolean|DateTime|Json)\b|\b(?:true|false)\b|\b\d+(?:\.\d+)?\b|\b(?:now|uuid|cuid)(?=\s*\()|\b[A-Z]\w*\b|\b[A-Za-z_]\w*\b/g;
    const append = (line, text, color) => {
      if (!text) return;
      if (!color) {
        line.append(text);
        return;
      }
      const span = document.createElement('span');
      span.textContent = text;
      span.style.color = color[0];
      span.style.setProperty('--shiki-dark', color[1]);
      line.append(span);
    };
    const commentStart = text => {
      let quoted = false;
      for (let index = 0; index < text.length - 1; index += 1) {
        if (text[index] === '"' && text[index - 1] !== '\\') quoted = !quoted;
        if (!quoted && text[index] === '/' && text[index + 1] === '/') return index;
      }
      return -1;
    };
    const highlightCode = code => {
      if (code.dataset.repostHighlighted !== undefined || code.querySelector('span[style*="--shiki-dark"]')) {
        return;
      }
      code.dataset.repostHighlighted = '';
      const source = code.textContent ?? '';
      const fragment = document.createDocumentFragment();
      for (const text of source.split('\n')) {
        const line = document.createElement('span');
        line.className = 'line';
        const declaration = text.match(/^(\s*)(generator|model|enum|type|event)(\s+)([A-Za-z_]\w*)(.*)$/);
        if (declaration) {
          append(line, declaration[1]);
          append(line, declaration[2], colors.keyword);
          append(line, declaration[3]);
          append(line, declaration[4], colors.name);
          append(line, declaration[5]);
        } else {
          const start = commentStart(text);
          const codeText = start === -1 ? text : text.slice(0, start);
          const leading = codeText.match(/^(\s*)([A-Za-z_]\w*)(?=\s+(?:@|[A-Z]))/);
          const bare = codeText.match(/^(\s*)([A-Za-z_]\w*)(\s*)$/);
          let offset = 0;
          for (const match of codeText.matchAll(tokenPattern)) {
            append(line, codeText.slice(offset, match.index));
            const token = match[0];
            let color = colors.constant;
            if (token.startsWith('"')) color = colors.string; else if (token.startsWith('@') || (/^(now|uuid|cuid)$/).test(token)) color = colors.name; else if ((/^(String|Int|Float|Boolean|DateTime|Json)$/).test(token)) color = colors.type; else if (leading && match.index === leading[1].length) color = colors.variable; else if (bare && match.index === bare[1].length) color = colors.constant; else if ((/^[A-Z]/).test(token)) color = colors.name; else if ((/^[A-Za-z_]/).test(token)) color = colors.variable;
            append(line, token, color);
            offset = match.index + token.length;
          }
          append(line, codeText.slice(offset));
          if (start !== -1) append(line, text.slice(start), colors.comment);
        }
        fragment.append(line, '\n');
      }
      code.replaceChildren(fragment);
    };
    const highlightRepostBlocks = () => {
      for (const block of document.querySelectorAll('.code-block')) {
        const code = block.querySelector('pre code');
        const filename = block.querySelector('[data-component-part="code-block-header-filename"] [title$=".repost"]');
        const looksLikeRepost = (/^\s*(generator|model|enum|type|event)\s+[A-Za-z_]\w*\s*\{/m).test(code?.textContent ?? '');
        if (code && (filename || looksLikeRepost)) highlightCode(code);
      }
    };
    let frame;
    const schedule = () => {
      if (frame !== undefined) return;
      frame = requestAnimationFrame(() => {
        frame = undefined;
        highlightRepostBlocks();
      });
    };
    const observer = new MutationObserver(schedule);
    observer.observe(document.documentElement, {
      childList: true,
      subtree: true
    });
    schedule();
    return () => {
      observer.disconnect();
      if (frame !== undefined) cancelAnimationFrame(frame);
    };
  }, []);
  return null;
};

<RepostHighlight />

The `repost schema` commands define your outbound webhook events in a Prisma-derived schema language, version them like database migrations, and generate a type-safe TypeScript SDK and Standard Webhooks-compatible JSON Schema. If you know the Prisma schema and its `init` → `migrate dev` → `generate` flow, you already know this.

## How it works

`repost schema` is a thin front end. Each subcommand delegates to a bundled schema engine (`repost-schema`): your arguments pass through verbatim, and the engine's output and exit code are preserved.

<CardGroup cols={3}>
  <Card title="Nothing extra to install" icon="package">
    Release builds of `repost` embed the engine for your platform and extract it on first use. Installing the CLI is all you need.
  </Card>

  <Card title="Version-pinned" icon="lock">
    The CLI runs only the engine build it ships with. A mismatched engine is refused rather than run.
  </Card>

  <Card title="Local and CI-friendly" icon="terminal">
    Every command runs locally against files in your repo. `validate` and `fmt --check` return a non-zero exit code for CI gates.
  </Card>
</CardGroup>

<Note>
  These are human-facing commands. Unlike the agentic commands, they do not emit the JSON envelope, and the `--json` and `--output` flags do not apply — arguments are forwarded to the engine as-is.
</Note>

## Scaffold a workspace

`repost schema init` creates a `repost/` directory with a working starter schema, a `.env` with the two placeholders you'll fill (`REPOST_SEND_API_KEY=""` for the generated client's sends, `REPOST_TOKEN=""` for deploys), and a `.gitignore` entry for `.env`. For TypeScript the generated client lives in `node_modules`, which any Node project already ignores.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
# Interactive on a terminal: prompts for the SDK language and output directory.
repost schema init

# Headless / scripted: pass the flags directly.
repost schema init --language typescript
repost schema init --language typescript --output ./generated/webhooks
```

Run bare on a terminal, `init` asks which SDK language the generator should emit (the list comes from `repost schema languages`) and where generated code should go; with any argument, or outside a TTY, it runs headless. Languages other than TypeScript have no default output — pass `--output` (their generated code is committed to your repo, not gitignored). It refuses to run if a `repost/` directory already exists, then prints the language's next steps: edit `repost/schema.repost`, install the runtime, run `repost schema migrate dev --name init`, and import the client.

<Note>
  **Pick one layout.** Your schema is *either* a single `repost/schema.repost` *or* a `repost/schema/` directory of `.repost` files — never both. If you outgrow one file, move **all** of them into `repost/schema/`; leaving extra `.repost` files next to `schema.repost`, or keeping both layouts, is a hard error naming the offending files (rather than silently ignoring them and writing a removal migration for the events they held).
</Note>

## Validate and format

Check that your schema parses and satisfies every rule without writing any output. The exit code reflects the diagnostics, so this fits directly into CI.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema validate
```

Format `.repost` files in place. Formatting is idempotent; pass `--check` to fail instead of writing when a file is not already formatted.

<CodeGroup>
  ```bash Format theme={"languages":{"custom":["/languages/repost.json"]}}
  repost schema fmt
  ```

  ```bash Check only theme={"languages":{"custom":["/languages/repost.json"]}}
  repost schema fmt --check
  ```
</CodeGroup>

## Generate the client

`repost schema generate` validates the schema, then emits a complete, precompiled client package — `package.json`, CommonJS and ESM builds, one typed `index.d.ts`, the Svix-compatible `schemas.json`, and a copy of your schema. It does not write a migration.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema generate
```

With no `output` in your `generator` block (the default), the package is written to `node_modules/.repost/client`, and the installed `@repost/client` package re-exports it — exactly how Prisma's `@prisma/client` works:

```ts theme={"languages":{"custom":["/languages/repost.json"]}}
import { createRepostClient, type User } from "@repost/client";

const repost = createRepostClient();
await repost.webhooks.user.created({ customerId: "cus_1", data: user });
```

Everything is typed from your schema: model interfaces, enums (value object + type), and the `webhooks.<catalog>.<member>()` method tree.

### Run your first send

Three practicalities a fresh project hits:

* **An API key.** Create an Environment in the dashboard — it generates the environment's publish API key and shows it once. Paste it into the `.env` that `init` scaffolded (`REPOST_SEND_API_KEY="rp_..."`). Nothing loads `.env` automatically — pass `--env-file` to Node (20.6+) or use your framework's loader. `createRepostClient()` reads `REPOST_SEND_API_KEY` at send time (an explicit `apiKey` option overrides it).
* **Running a TypeScript file.** Plain `node index.ts` does not work on current Node. Use `npx tsx index.ts`, or Node's built-in stripping (`node --experimental-strip-types index.ts`, Node 22.6+), or write the entry as plain `.mjs`.
* **Module type.** Give your project a `"type": "module"` in `package.json` (or use `.mts`/`.mjs` extensions) so Node doesn't warn about, and reparse, every typeless file.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
node --experimental-strip-types --env-file=.env index.ts
```

<Note>
  Sends go to Repost's publish API and are delivered to your customers' subscribed endpoints. To test sends without network calls, pass the exported `stubTransport` to `createRepostClient({ transport })` — it validates, serializes, and returns a typed result locally.
</Note>

### Custom output for monorepos

Set `output` to write the identical package to a path instead (resolved relative to the schema directory). Import it by path or re-export it from a workspace package; `@repost/client` then serves only as the runtime dependency.

```repost theme={"languages":{"custom":["/languages/repost.json"]}}
generator sdk {
  language = "typescript"
  output   = "../src/generated/repost"
}
```

Custom output refuses to overwrite a directory it does not recognize as a previously generated client, so a mistyped path cannot destroy your code.

## Other SDK languages

The same schema generates typed senders for Go, Python, Java, and Kotlin — set the generator's `language` (Ctrl+Space in your editor completes the supported values). Unlike TypeScript, these languages have no `node_modules` to hide output in: `output` is required, and the generated package is committed to your repo like any other codegen (protoc/sqlc convention).

<CodeGroup>
  ```repost Go theme={"languages":{"custom":["/languages/repost.json"]}}
  generator sdk {
    language = "go"
    output   = "./internal/repostclient"
  }
  ```

  ```repost Python theme={"languages":{"custom":["/languages/repost.json"]}}
  generator sdk {
    language = "python"
    output   = "./repost_sdk"
  }
  ```
</CodeGroup>

Install the language's runtime once — `go get github.com/repost-sh/repost-go` or `pip install repost-client` — then `repost schema generate` and send:

<CodeGroup>
  ```go Go theme={"languages":{"custom":["/languages/repost.json"]}}
  client, err := repostclient.NewClient(repost.ClientOptions{})
  if err != nil { log.Fatal(err) }
  result, err := client.Webhooks.Book.Created(ctx, repostclient.BookCreatedInput{
      CustomerID: "cus_1",
      Data:       book,
  })
  ```

  ```python Python theme={"languages":{"custom":["/languages/repost.json"]}}
  client = RepostClient()
  result = client.webhooks.book.created(customer_id="cus_1", data=book)
  ```
</CodeGroup>

Auth and configuration behave identically in every language: `REPOST_SEND_API_KEY` read at send time, `REPOST_API_URL` to override the API, a no-network stub transport for tests, and the same retry/idempotency semantics — the runtimes share one wire format, pinned by a cross-language conformance suite. A generated client and its runtime library verify a descriptor-format handshake at construction, so a version mismatch fails immediately with the fix (regenerate, or upgrade the runtime) instead of corrupting sends.

### Java and Kotlin

The JVM languages generate the same way but need two extra generator settings, because a JVM client is more than a source tree: it also emits a discovery **resource**. Set `packageName` (the Java/Kotlin package), `clientName` (the generated client class), and both a source root (`output`) and a resource root (`resourceOutput`):

<CodeGroup>
  ```repost Java theme={"languages":{"custom":["/languages/repost.json"]}}
  generator javaSdk {
    language       = "java"
    output         = "../target/generated-sources/repost"
    resourceOutput = "../target/generated-resources/repost"
    packageName    = "com.example.repost"
    clientName     = "RepostClient"
  }
  ```

  ```repost Kotlin theme={"languages":{"custom":["/languages/repost.json"]}}
  generator kotlinSdk {
    language       = "kotlin"
    output         = "../build/generated/sources/repost/kotlinSdk/kotlin"
    resourceOutput = "../build/generated/resources/repost/kotlinSdk"
    packageName    = "com.example.repost"
    clientName     = "RepostClient"
  }
  ```
</CodeGroup>

Repost owns both roots: it writes the generated `.java`/`.kt` files under `output` and one `client.json` registry per client under `resourceOutput`, and replaces both together in one transaction. Point them at build-output directories — they are regenerated, not committed.

Most JVM projects don't call the CLI directly. A Maven or Gradle plugin runs the same engine as part of the build, and both offer a check goal that regenerates into an isolated directory and fails on drift without mutating your tree:

<CodeGroup>
  ```bash CLI theme={"languages":{"custom":["/languages/repost.json"]}}
  repost schema generate
  ```

  ```bash Maven theme={"languages":{"custom":["/languages/repost.json"]}}
  ./mvnw repost:generate
  ./mvnw repost:check
  ```

  ```bash Gradle theme={"languages":{"custom":["/languages/repost.json"]}}
  ./gradlew repostGenerate
  ./gradlew repostGenerateCheck
  ```
</CodeGroup>

The [Java](/docs/send/java/quickstart) and [Kotlin](/docs/send/kotlin/quickstart) guides cover the plugin, BOM, sends, framework injection, and support baseline end to end.

One schema can carry **multiple generators** — a multi-service monorepo generates every service's client in one `repost schema generate` run, including the same language twice with different outputs:

```repost theme={"languages":{"custom":["/languages/repost.json"]}}
generator api {
  language = "typescript"
}

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

generator analytics {
  language = "python"
  output   = "../services/analytics/repost_sdk"
}
```

Working examples live in the repo under `examples/go`, `examples/python`, `examples/java`, and `examples/kotlin`.

<Warning>
  **Upgrading from the loose-TS output?** Earlier builds emitted `models.ts`/`webhooks.ts`/`client.ts` source files. Delete those generated directories once — `generate` refuses to overwrite them because it cannot prove they are generated — and update imports to `@repost/client` (default) or your `output` path.
</Warning>

## Generated client and your package manager

The default output lives in `node_modules`, which your package manager owns. Three things to know:

* **Regenerate after install.** A fresh clone or a CI job that restores `node_modules` from cache does not have your generated client. Run `repost schema generate` in your build (or a `postinstall` script) — the same practice Prisma projects use. Until the first generate, importing `@repost/client` throws `@repost/client did not initialize yet. Please run "repost schema generate"`.
* **The repost CLI is a prerequisite.** Generation is done by the `repost` CLI (the engine ships inside it), so machines that build your app need it installed.
* **No `node_modules` at all?** Yarn PnP and similar layouts have nowhere for the default output to go — set `output` and import by path instead.

`@repost/client` is not on npm yet (publishing is pending license confirmation). Until it is, install it locally with `pnpm pack` and an install from the tarball — see the runtime package's `README.md` (`sdk/typescript`) for the full local-development steps. Once published, it becomes a plain `npm install @repost/client`.

## Version changes with migrations

`repost schema migrate dev` diffs the current output against your last migration. When something changed, it writes a new `repost/migrations/<timestamp>_<name>/` snapshot — the directory is prefixed with a UTC `YYYYMMDDHHMMSS` timestamp — bumps the version of each affected event, and regenerates. When nothing changed, it is a no-op.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema migrate dev --name add_subtitle
```

`--name` is optional. When you omit it and a migration is being written, the command prompts you for a name (press Enter with no input for a timestamp-only directory). With no name and no terminal — such as in CI — it errors instead, so a migration is never written unnamed by accident. A no-op run needs no name at all.

The migration directory and the version bumps are committed to your repo, so every schema change is reviewable in `git diff`.

<Warning>
  **Removing event types is confirmed.** When a migration would drop event types from the catalog, `migrate dev` lists them and asks for an explicit `y` (default No). In CI or any non-interactive shell it errors instead — pass `--accept-removals` to remove deliberately.
</Warning>

### Merging branches with migrations

Migration directories carry a UTC timestamp prefix, so two branches almost never mint the same directory name and git merges them cleanly; `migration_lock.toml` holds no history, so it never conflicts either. Each migration also records its `parent` (the previous migration) and a `checksum` of its snapshot, so the CLI can reconstruct the lineage.

When two branches each ran `migrate dev`, merging both leaves the lineage with two heads — a **fork**. Every command that reads migrations detects it: `generate` and `migrate status` fail loudly and name both directories rather than silently emitting reconciled versions. Run `repost schema migrate dev` once on the merged schema to resolve it: it diffs the merge against **each** side, prints a per-parent summary of what each branch contributed, resolves any contested version number to the next free one, and writes a single reconciling migration recording `parents: [a, b]`. Commit that migration.

For CI, run `repost schema validate` and then `repost schema migrate status`. `status` exits non-zero on a fork, on unmigrated changes (the schema is ahead of the recorded history), or on a hand-edited snapshot (checksum mismatch), and exits `0` when the lineage is clean.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema migrate status
```

## Deploy to your environment

`repost schema migrate deploy` applies your committed migrations to a live Repost environment: it sends the migration head, the full lineage, and the accumulated `schemas.json`, and the server materializes the event-type registry from it. It requires a clean local status — a fork, unmigrated changes, or a checksum mismatch is a hard error, so what you deploy is always what your repo records.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema migrate deploy
```

Authentication comes from `REPOST_TOKEN` — an environment-scoped token with the `schema:deploy` scope — and `REPOST_API_URL` when you target something other than the default API. Both are read from `repost/.env` (scaffolded by `repost schema init`) before the process environment. On success the CLI prints the applied diff (`created`, `updated`, `archived` event types); re-deploying the same head is a no-op. When the server's deployed head is not an ancestor of yours, the deploy is refused with a reason. If your checkout is merely **behind** the deployed history, pull the latest migrations from your repo (`git pull`) and redeploy. If the histories have **diverged**, merge the deployed migrations in git and run `repost schema migrate dev` to reconcile — or replace the deployed history with `--force` (below).

For development and staging loops, `repost schema push` sends the current schema without writing a migration artifact — the equivalent of Prisma's `db push`. It is refused on environments that already have migration history, so a push can never fork a production lineage. Repeat pushes of an unchanged schema are no-ops.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema push
```

Pass `--dry-run` to either command to print the deployment payload instead of sending it.

### Forcing past a diverged history

Some histories the ancestry gate can never accept on their own: you squashed or re-initialized your migration directories, or restored the environment from another repo. There is no ancestor to fast-forward from and no `git pull` that closes the gap. `repost schema migrate deploy --force` replaces the deployed history outright.

Force is deliberate and shows its work. The CLI first fetches the deployed head and prints exactly what will be replaced — checksum, kind, migration name, and deploy time — then asks for an explicit `y` (default No). In CI or any non-interactive shell it errors instead; pass `--accept-replace` to force deliberately.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema migrate deploy --force
```

<Warning>
  **Force is leased, not blind.** The head the CLI just showed you is echoed back to the server as the expected head; if another deploy moved it between your look and your force, the server refuses rather than clobber a head nobody reviewed. Forcing to an older, previously deployed checksum is the supported **rollback** path — it rewinds the registry to that earlier state, and the CLI prints an extra warning when it detects one. A wrong force is recoverable: archived event types still accept publishes and still deliver, so nothing is dropped, and redeploying the correct head heals the registry.
</Warning>

## Import from Svix

Convert a Svix environment export into a `repost/` workspace — one catalog member and event binding per event type, a payload model per schema, and an initial migration that preserves the source's full version history.

```bash theme={"languages":{"custom":["/languages/repost.json"]}}
repost schema import svix export.json --out app
```

Names are kept verbatim. Any Svix event-type name that cannot be represented is reported as an error listing every offender, rather than being silently changed.

## Command summary

Each command has its own reference page:

| Command                                                      | What it does                                              |
| ------------------------------------------------------------ | --------------------------------------------------------- |
| [`repost schema init`](/docs/cli/schema/init)                     | Scaffold `repost/` with a starter schema.                 |
| [`repost schema languages`](/docs/cli/schema/languages)           | List the supported SDK languages.                         |
| [`repost schema validate`](/docs/cli/schema/validate)             | Parse and validate; writes nothing.                       |
| [`repost schema fmt`](/docs/cli/schema/fmt)                       | Format `.repost` files, with a `--check` CI mode.         |
| [`repost schema generate`](/docs/cli/schema/generate)             | Emit every declared client, with a `--check` drift gate.  |
| [`repost schema migrate dev`](/docs/cli/schema/migrate-dev)       | Record schema changes as a migration and regenerate.      |
| [`repost schema migrate status`](/docs/cli/schema/migrate-status) | Report migration health without writing.                  |
| [`repost schema migrate deploy`](/docs/cli/schema/migrate-deploy) | Apply committed migrations to a live environment.         |
| [`repost schema push`](/docs/cli/schema/push)                     | Send the schema to a dev environment without a migration. |
| [`repost schema import svix`](/docs/cli/schema/import)            | Convert a Svix export into a `repost/` workspace.         |

<Tip>
  Contributors and CI that build a dev CLI do not embed the engine. `make -C repost-go install-dev-cli` installs the engine onto your `PATH` alongside the `devrepost` binary. Set `REPOST_SCHEMA_ENGINE` to point at a specific engine build if you need to override resolution.
</Tip>

## Next steps

<CardGroup cols={2}>
  <Card title="Reference" icon="list-tree" href="/docs/cli/reference">
    See every `repost schema` command and flag in the complete CLI reference.
  </Card>

  <Card title="Authentication" icon="key-round" href="/docs/cli/authentication">
    Install the CLI binary — the schema engine ships bundled inside it.
  </Card>
</CardGroup>
