# Author in TypeScript (/docs/orm/contract-authoring/typescript-schema-builder)

> For the complete Prisma documentation index, see [llms.txt](https://www.prisma.io/docs/llms.txt). A markdown version of any docs page is available by appending `.md` to its URL.

Define the Prisma ORM contract with a typed builder in TypeScript instead of a schema file. Same models, same `contract.json` and `contract.d.ts`, no separate language.

Location: ORM > Contract authoring > Author in TypeScript

In Prisma ORM 8, `schema.prisma` is replaced by a file called [the contract](https://www.prisma.io/docs/orm/contract-authoring/the-data-contract), which holds the model definitions you used to write in `schema.prisma`. You can write it in two forms: one is a `.prisma` file written in PSL, short for Prisma Schema Language, and the other is TypeScript, in `src/prisma/contract.ts`, built with the `defineContract` builder. Both forms produce the same two files: [`npx prisma contract emit`](https://www.prisma.io/docs/cli/contract-emit) writes `contract.json` and `contract.d.ts` into the folder that holds your contract file.

## When to choose TypeScript over PSL [#when-to-choose-typescript-over-psl]

[PSL](https://www.prisma.io/docs/orm/contract-authoring/psl-syntax) is the preferred way to write the contract, and both forms produce the same two files, so you give up nothing by staying with PSL. Use the TypeScript builder for the cases PSL does not cover, and reach for it when:

* model definitions must be split, composed, or reused across ordinary TypeScript modules or packages
* you want to build models in a loop from data you already keep in TypeScript, such as one model per entry in a list of table names

If neither applies, write PSL, which is more compact and is what [`contract infer`](https://www.prisma.io/docs/cli/contract-infer) writes.

## Point the config at the contract file [#point-the-config-at-the-contract-file]

For a new project, run [`npx prisma orm init`](https://www.prisma.io/docs/cli/orm-init), which asks how you want to write your schema, and choosing TypeScript creates the contract file and the config together.

The config's `contract` path names the one file Prisma ORM reads, and a `.ts` extension selects TypeScript authoring:

  

#### PostgreSQL

```typescript title="prisma.config.ts" 
import { definePrismaConfig } from "prisma/config";
import { defineConfig as ormConfig } from "@prisma/orm-postgres/config";

export default definePrismaConfig({
  orm: ormConfig({
    contract: "./src/prisma/contract.ts",
  }),
});
```

#### MongoDB

```typescript title="prisma.config.ts" 
import { definePrismaConfig } from "prisma/config";
import { defineConfig as ormConfig } from "@prisma/orm-mongo/config";

export default definePrismaConfig({
  orm: ormConfig({
    contract: "./src/prisma/contract.ts",
  }),
});
```

## A complete contract [#a-complete-contract]

The builder comes from your database's package: `@prisma/orm-postgres/contract-builder` for PostgreSQL, `@prisma/orm-mongo/contract-builder` for MongoDB.

Export the contract under the name `contract`, as below, or as the file's default export, because those are the only two names `prisma contract emit` looks for. Run `npx prisma contract emit` again after every edit to the contract, and commit the contract file together with [`contract.json` and `contract.d.ts`](https://www.prisma.io/docs/orm/contract-authoring/the-contract-artifact).

  

#### PostgreSQL

```typescript title="src/prisma/contract.ts" 
import { defineContract, enumType, member } from "@prisma/orm-postgres/contract-builder";

// nativeType is the column type the database gets. codecId picks how Prisma ORM converts the value between TypeScript and that column, and its @1 is the version of that conversion.
const pgText = { codecId: "pg/text@1", nativeType: "text" } as const;

const Priority = enumType("Priority", pgText, member("Low", "low"), member("High", "high"));

export const contract = defineContract({}, ({ field, model, rel }) => {
  const User = model("User", {
    fields: {
      id: field.id.uuidv4String(),
      email: field.text(),
      createdAt: field.temporal.createdAt(),
      address: field.json().optional(),
    },
  });

  const Post = model("Post", {
    fields: {
      id: field.id.uuidv4String(),
      title: field.text(),
      userId: field.uuidString(),
      priority: field.namedType(Priority).default(Priority.members.Low),
      createdAt: field.temporal.createdAt(),
      updatedAt: field.temporal.updatedAt(),
    },
  });

  return {
    enums: { Priority },
    models: {
      User: User.relations({ posts: rel.hasMany(Post, { by: "userId" }) }).sql({ table: "user" }),
      Post: Post.relations({
        user: rel.belongsTo(User, { from: "userId", to: "id" }).sql({ fk: { name: "post_userId_fkey" } }),
      }).sql({ table: "post" }),
    },
  };
});
```

#### MongoDB

```typescript title="src/prisma/contract.ts" 
import { defineContract, field, model, rel } from "@prisma/orm-mongo/contract-builder";

const User = model("User", {
  collection: "users",
  fields: {
    _id: field.objectId(),
    email: field.string(),
  },
  relations: {
    posts: rel.hasMany("Post", { from: "_id", to: "authorId" }),
  },
});

const Post = model("Post", {
  collection: "posts",
  fields: {
    _id: field.objectId(),
    authorId: field.objectId(),
    title: field.string(),
    publishedAt: field.date().optional(),
  },
  relations: {
    author: rel.belongsTo(User, { from: "authorId", to: User.ref("_id") }),
  },
});

export const contract = defineContract({ models: { User, Post } });
```

## How `defineContract` works [#how-definecontract-works]

On PostgreSQL, `defineContract` takes an options object and then a function. You do not set a `provider` anywhere: importing `@prisma/orm-postgres/contract-builder` is what selects PostgreSQL. The options object lists the extension packs you use, and you write `{}` when you use none. The function returns the contract's content: `models`, plus `enums` and `types` if you have them.

Take `field`, `model`, `rel`, and `type` from the function's one argument, and import `defineContract`, `enumType`, and `member` from the package. The package also exports `model` and `rel` for use outside the function, plus a `field` that has only `column`, `generated`, and `namedType`.

On MongoDB, `defineContract` also accepts a single object holding `models`, as above, and you import `field`, `model`, and `rel` from the package instead. The MongoDB builder differs from the PostgreSQL one in five ways:

* Every model declares an `_id` field built with `field.objectId()`. That field is always the primary key, so you never mark it with `.id()` and there is no `.attributes(...)` on MongoDB.
* The scalar helpers are `field.objectId()`, `field.string()`, `field.int32()`, `field.double()`, `field.bool()`, and `field.date()`.
* The collection name is an inline option on the model rather than a chained call, and relations are inline as well.
* Relations name the field on each side. `rel.hasMany` takes `{ from, to }` on MongoDB and `{ by }` on PostgreSQL. `to` accepts either the field name as a string or a typed reference such as `User.ref("_id")`, and both forms mean the same thing. PostgreSQL writes that reference as `User.refs.id`.
* Indexes are an `indexes` option on the model, built with the `index` helper the same package exports, as in `indexes: [index({ email: 1 }, { unique: true })]`. The `1` is ascending, MongoDB's own index syntax.

Enums and extension packs work on MongoDB too: `enumType` and `member` come from `@prisma/orm-mongo/contract-builder`. Pass the enums in the same object as the models, as `defineContract({ models, enums })`, and extension packs go in the same `extensions` option.

The examples below use the PostgreSQL builder.

## Fields [#fields]

`field` has a helper for each column type. On PostgreSQL:

| Column                                  | Helper               |
| --------------------------------------- | -------------------- |
| text                                    | `field.text()`       |
| integer                                 | `field.int()`        |
| big integer                             | `field.bigint()`     |
| float                                   | `field.float()`      |
| decimal                                 | `field.decimal()`    |
| boolean                                 | `field.boolean()`    |
| date and time                           | `field.dateTime()`   |
| bytes                                   | `field.bytes()`      |
| JSON                                    | `field.json()`       |
| UUID stored in a `character(36)` column | `field.uuidString()` |
| UUID stored in a `uuid` column          | `field.uuidNative()` |

There is no helper for a date without a time, or a time without a date. For a date-only column, name the type yourself: `field.column({ codecId: "pg/date-temporal@1", nativeType: "date" } as const)`. `field.column(...)` and the chained `.column(...)` below are different calls: `field.column(descriptor)` builds a field from a type description, and `.column(name)` on an existing field sets its column name. Extension packs add more helpers of their own.

For a primary key whose value your app generates, use `field.id.uuidv4String()`, `field.id.uuidv7String()`, `field.id.ulid()`, `field.id.nanoid()`, `field.id.cuid2()`, or `field.id.ksuid()`. They all generate the ID in your app, and `uuidv4String` is the common choice. Each marks the field as the primary key for you, and `field.id.uuidv4Native()` and `field.id.uuidv7Native()` do the same in a `uuid` column. There is no auto-increment helper, so use one of those `field.id.*` helpers. For a key the database generates instead, write `field.uuidNative().defaultSql("gen_random_uuid()").id()`.

`field.temporal.createdAt()` fills the column with `now()` when the row is created, while `field.temporal.updatedAt()` fills it on every create and every update, and Prisma ORM sets it in the client, not with a database trigger. `field.namedType(x)` takes an enum or a type from an extension pack.

Every field builder supports chained modifiers:

* `.optional()` makes the field nullable.
* `.default(value)` sets a literal default. `.defaultSql(expression)` sets a default the database computes, and the argument is a SQL expression as a string: `.defaultSql("now()")`.
* `.unique()` adds a unique constraint. `.id()` marks the primary key, for a key that is not generated, such as an integer you set yourself.
* `.column("column_name")` sets the column name in the database when it differs from the field name.

## Enums [#enums]

`enumType` declares an enum, the type its values are stored as, and its members, as `Priority` does in the full example above.

The second argument says how each member is stored, as the same `codecId` and `nativeType` pair the full example above explains. Write `as const` after the object, so TypeScript keeps the exact strings. To store the members as integers, pass `{ codecId: "pg/int4@1", nativeType: "int4" } as const`. More type ids are listed with the [raw query `param` helper](https://www.prisma.io/docs/orm/reference/raw-queries#binding-a-bare-value-with-param), which names types the same way.

Each `member(name, storedValue)` pairs the TypeScript-visible name with the value stored in the column. Fields reference the enum with `field.namedType(Priority)`, and defaults reference a member as `Priority.members.Low`. Include the enum in the returned `enums` map so it reaches the two files.

Members are stored in whatever column type you name here, `text` above. For a real PostgreSQL `enum` type, declare it with `nativeEnum` and type the field with `pg.enum`, both exported by `@prisma/orm-postgres/contract-builder`. The type is created in the database under the name you give it:

```typescript
import { defineContract, nativeEnum, pg } from "@prisma/orm-postgres/contract-builder";

const Role = nativeEnum("Role", "user", "admin");

export const contract = defineContract({}, ({ field, model }) => {
  const Account = model("Account", { fields: { role: field.column(pg.enum(Role)) } });
  return { models: { Account: Account.sql({ table: "account" }) } };
});
```

`Role` does not go in the returned `enums` map: using it on a field is enough to create the type in the database.

## Relations [#relations]

Relations are declared on the model builder with `.relations(...)` and the `rel` helpers, as the full example above shows.

`rel.hasMany(Model, { by })` names the foreign key field on the other model, and `rel.hasOne(Model, { by })` is the same with at most one row on the other side. `rel.belongsTo(Model, { from, to })` maps the local foreign key field to the field it points at.

`rel.manyToMany(Model, { through, from, to })` goes through a join table: you declare the model for the join table yourself and pass it as `through`. Below, `PostTag` is that model, holding the two foreign key fields `postId` and `tagId`:

```typescript
Post.relations({
  tags: rel.manyToMany(Tag, { through: PostTag, from: "postId", to: "tagId" }),
});
```

`rel.belongsTo` on its own does not create a foreign key constraint in the database, so ask for one by chaining `.sql(...)` on the relation itself, inside the `.relations({ ... })` object, as the full example above does:

```typescript
Post.relations({
  user: rel.belongsTo(User, { from: "userId", to: "id" }).sql({ fk: { name: "post_userId_fkey" } }),
});
```

`fk` takes `name`, `onDelete`, and `onUpdate`. `onDelete` and `onUpdate` each take `'noAction'`, `'restrict'`, `'cascade'`, `'setNull'`, or `'setDefault'`.

Prisma ORM does not check that the column types on the two sides of a relation match, so choose a field helper that produces the same column type as the key you point at.

`rel.hasMany`, `rel.hasOne`, `rel.belongsTo`, and `rel.manyToMany` also accept the model name as a string. Pass the model object, as above, and a typo is a compile error, but pass a string and the typo is reported when `prisma contract emit` builds the contract.

## Storage mapping [#storage-mapping]

`.sql(...)` maps a model to its table, and the object form covers the common case: `User.sql({ table: "user" })`.

You can chain `.relations(...)`, `.attributes(...)`, and `.sql(...)` in any order, and you can skip any of them, so a model with no relations calls `.sql({ table })` straight after `model(...)`.

The callback form gives you the model's columns as `cols` and the constraint builders as `constraints`. Use it for indexes:

```typescript
Post.relations({ ... }).sql(({ cols, constraints }) => ({
  table: "post",
  indexes: [
    constraints.index([cols.userId]),
    constraints.index([cols.userId, cols.createdAt], { name: "post_user_created_idx" }),
  ],
}));
```

`constraints.index` always takes a list of columns, even when the list has one entry. Add `{ unique: true }` to make it a unique index.

`Model.refs` provides typed references to another model's fields, for the constraint builders inside `.sql(...)`. Write `constraints.foreignKey(cols.userId, User.refs.id)` and TypeScript checks `id` against the actual `User` definition.

For a primary key made of two fields, use `.attributes(...)` instead of `.sql(...)`, and build it from the field references:

```typescript
PostTag.attributes(({ fields, constraints }) => ({
  id: constraints.id([fields.postId, fields.tagId]),
}));
```

That object accepts exactly two keys, `id` and `uniques`. `id` takes one constraint, and `uniques` takes a list, so `uniques: [constraints.unique([fields.postId, fields.tagId])]` makes a unique constraint across two fields. For a key made of one field, `.id()` on the field is enough, and the `field.id.*` helpers already do it.

## Extension types [#extension-types]

An extension pack is an npm package that adds column types to the builder. List packs in `defineContract`'s options object, and the `type` helper exposes their constructors:

```typescript title="src/prisma/contract.ts"
import pgvector from "@prisma/orm-extension-pgvector/pack";
import { defineContract } from "@prisma/orm-postgres/contract-builder";

export const contract = defineContract({ extensions: { pgvector } }, ({ field, model, type }) => {
  const types = { Embedding1536: type.pgvector.Vector(1536) } as const;
  const Post = model("Post", {
    fields: { id: field.id.uuidv4String(), embedding: field.namedType(types.Embedding1536) },
  });
  return { types, models: { Post: Post.sql({ table: "post" }) } };
});
```

The key on `type.pgvector` is the pack's own name, not the name you gave the import. The name is in the pack's documentation, and pgvector's is `pgvector`. `Embedding1536` is a name you choose, and it appears under that name in `contract.d.ts`. Return the `types` map so the name reaches the two files.

Add the same pack to `prisma.config.ts`: it goes inside `ormConfig({ ... })`, beside `contract`, and the config imports the pack's `/control` export while the contract file imports its `/pack` export:

```typescript title="prisma.config.ts"
import { definePrismaConfig } from "prisma/config";
import pgvector from "@prisma/orm-extension-pgvector/control";
import { defineConfig as ormConfig } from "@prisma/orm-postgres/config";

export default definePrismaConfig({
  orm: ormConfig({
    contract: "./src/prisma/contract.ts",
    extensions: [pgvector],
  }),
});
```

## Keep the contract file free of changing values [#keep-the-contract-file-pure]

The contract file describes structure, and these rules keep it usable:

* Do not read `process.env`, the current time, or random values into the contract. A contract built from those values makes `npx prisma contract emit` produce a different `contract.json` on each run.
* Keep field values plain: strings, numbers, booleans, and the builder's own objects. Functions, class instances, and `Date` objects do not serialize.
* Keep the file free of side effects. `npx prisma contract emit` loads the file with Node.js, so anything it does while loading, such as writing a file or calling a service, happens every time you run the command. The file is ordinary TypeScript, so `import` model definitions from other files as usual.
* `npx prisma contract emit` runs the file but does not type-check it. Type errors show in your editor and when you run `tsc`.

Configuration that legitimately varies per environment, such as the database URL, belongs in `prisma.config.ts`, not in the contract.

## Parity with PSL [#parity-with-psl]

TypeScript and [PSL](https://www.prisma.io/docs/orm/contract-authoring/psl-syntax) authoring produce the same `contract.json` and `contract.d.ts` for an equivalent contract, so you can move between the two forms without changing anything downstream. A project names exactly one contract file in its config.

No command converts a `.prisma` contract into TypeScript, so to move an existing project, write the TypeScript file by hand, point `contract` in `prisma.config.ts` at it, and delete the `.prisma` file so the two can never disagree.

## Prompt your coding agent [#prompt-your-coding-agent]

Projects created with `npm create prisma@latest` include the [Prisma ORM skills](https://www.prisma.io/docs/ai/tools/skills#available-skills-for-prisma-8) for your coding agent. In an existing project, run `npx prisma skills sync` to add them. The `prisma-8` skill covers TypeScript authoring, so ask your agent to:

* "Convert this contract.prisma to the TypeScript schema builder."
* "Using the prisma-8 skill, add a unique constraint to the email field in our TypeScript schema."

## Next steps [#next-steps]

* Inspect [`contract.json` and `contract.d.ts`](https://www.prisma.io/docs/orm/contract-authoring/the-contract-artifact), the two files the contract produces.
* Apply the contract to a database with [`db init`](https://www.prisma.io/docs/cli/db-init) or plan changes with [`migration plan`](https://www.prisma.io/docs/cli/migration-plan).

## Related pages

- [`Author in PSL`](https://www.prisma.io/docs/orm/contract-authoring/psl-syntax): Write the Prisma ORM contract in the Prisma schema language you already know, plus the Prisma ORM 8 additions.
- [`contract.json and contract.d.ts`](https://www.prisma.io/docs/orm/contract-authoring/the-contract-artifact): contract.json and contract.d.ts are the two files every other part of Prisma ORM reads. Here is what is inside them.
- [`Editor support`](https://www.prisma.io/docs/orm/contract-authoring/editor-support): What the Prisma VS Code extension does for a Prisma ORM contract, and what to do when it stops accepting the file.
- [`Supported database features`](https://www.prisma.io/docs/orm/contract-authoring/capabilities): The contract records which database features your packages support, so Prisma ORM can reject an unsupported one early with a clear error.
- [`The data contract`](https://www.prisma.io/docs/orm/contract-authoring/the-data-contract): The data contract is the one description of your data model and how it is stored. Prisma ORM types your queries, plans your migrations, and checks your database against it.