Prisma IDB FaviconPrisma IDB

Client Contracts

How one schema produces a browser contract and a server contract, and what exclusion does to relations

A synced app has two databases with overlapping but different contents. The server holds everything; the browser holds only what the user may see. Rather than keeping two schemas in step by hand, you write one schema.prisma and mark what stays on the server. This page explains how that one schema becomes two contracts, and the rules that keep them consistent. For setup, follow the Sync tutorial.

Two contracts from one schema

@idb.exclude on a field, or @@idb.exclude on a model, marks it as server-only:

model User {
  id       String    @id
  name     String
  password String    @idb.exclude
  sessions Session[]
}

model Session {
  id     String @id
  userId String
  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@idb.exclude
}

Each side reads the schema differently:

  • The browser reads it through the IndexedDB family, with prismaIdbContract(path, { projection: "client" }). The "client" projection leaves out everything marked. Without projection, nothing is left out.
  • The server can't use the IndexedDB family: a Postgres contract comes from the SQL family, and its parser rejects the unknown idb attributes. So defineConfig from @prisma-idb/sync-server/postgres removes the attributes from the schema text, adds a Changelog model, and passes the result to the SQL family, all in memory. Nothing extra is written to disk.

The two sides are separate projects that happen to share a schema. Each has its own contract, its own migrations folder and its own migration history. The browser's client never sees the server's contract.

For a server on a database other than Postgres, see Server.

Relations to an excluded model

When a model that reaches the browser has a relation to an excluded model, the relation is dropped from the browser's contract. The model itself stays, and so do its foreign-key fields:

model Todo {
  id        String  @id
  sessionId String
  session   Session @relation(fields: [sessionId], references: [id])
}

model Session {
  id String @id
  @@idb.exclude
}

In the browser, Todo.sessionId is still there, with the same type, but Todo.session isn't. The browser can't follow it or check it. This applies to every kind of relation, required or optional. contract emit logs a warning each time:

[prisma-idb] Dropped relation "Todo.session" from the client contract: target model "Session" is excluded. The relation's scalar fields are kept.

Two alternatives were ruled out:

  • Dropping the foreign-key field too would change the shape of records that clients have already synced.
  • Excluding Todo as well would make exclusion spread: excluding one model could silently exclude others that point at it, and those could exclude more. A model is only ever excluded because you marked it.

Rules

contract emit rejects a schema that breaks any of these:

  • A model's @id field can't be excluded.
  • An index can't include an excluded field.
  • An excluded field can't be @unique.
  • A relation field, or a field that a relation uses as a key, can't be excluded on its own. Exclude the whole model on the other side instead.

How sync uses the browser contract

The browser contract defines what syncs. createSyncServer takes both contracts: it reads relations from the server contract and syncs only the models in the browser contract. Every synced model needs a path of many-to-one relations up to the root model, usually User, so the server can tell who owns each record. If a synced model has no such path, or the root model is excluded, createSyncServer throws at startup.

Apps split into several packages

If the frontend and backend are separate packages, put the schema in a shared package, such as @myapp/schema, and build both contracts there. Export them from two entry points, ./client and ./server, so both always come from the same version of the schema. Don't make the frontend depend on the backend package, or the reverse, just to reach the schema.

On this page