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.
Nothing extra to install
Release builds of
repost embed the engine for your platform and extract it on first use. Installing the CLI is all you need.Version-pinned
The CLI runs only the engine build it ships with. A mismatched engine is refused rather than run.
Local and CI-friendly
Every command runs locally against files in your repo.
validate and fmt --check return a non-zero exit code for CI gates.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.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.
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.
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).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..repost files in place. Formatting is idempotent; pass --check to fail instead of writing when a file is not already formatted.
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.
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:
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
.envthatinitscaffolded (REPOST_SEND_API_KEY="rp_..."). Nothing loads.envautomatically — pass--env-fileto Node (20.6+) or use your framework’s loader.createRepostClient()readsREPOST_SEND_API_KEYat send time (an explicitapiKeyoption overrides it). - Running a TypeScript file. Plain
node index.tsdoes not work on current Node. Usenpx 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"inpackage.json(or use.mts/.mjsextensions) so Node doesn’t warn about, and reparse, every typeless file.
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.Custom output for monorepos
Setoutput 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.
Other SDK languages
The same schema generates typed senders for Go, Python, Java, and Kotlin — set the generator’slanguage (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).
go get github.com/repost-sh/repost-go or pip install repost-client — then repost schema generate and send:
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. SetpackageName (the Java/Kotlin package), clientName (the generated client class), and both a source root (output) and a resource root (resourceOutput):
.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:
repost schema generate run, including the same language twice with different outputs:
examples/go, examples/python, examples/java, and examples/kotlin.
Generated client and your package manager
The default output lives innode_modules, which your package manager owns. Three things to know:
- Regenerate after install. A fresh clone or a CI job that restores
node_modulesfrom cache does not have your generated client. Runrepost schema generatein your build (or apostinstallscript) — the same practice Prisma projects use. Until the first generate, importing@repost/clientthrows@repost/client did not initialize yet. Please run "repost schema generate". - The repost CLI is a prerequisite. Generation is done by the
repostCLI (the engine ships inside it), so machines that build your app need it installed. - No
node_modulesat all? Yarn PnP and similar layouts have nowhere for the default output to go — setoutputand 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.
--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.
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.
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.
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.
--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 nogit 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.
Import from Svix
Convert a Svix environment export into arepost/ 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.
Command summary
Each command has its own reference page:Next steps
Reference
See every
repost schema command and flag in the complete CLI reference.Authentication
Install the CLI binary — the schema engine ships bundled inside it.