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

Location: ORM > Next > Contract authoring > Author in TypeScript

TypeScript authoring defines the Prisma Next [data contract](/orm/next/contract-authoring/the-data-contract) in code. Instead of a `.prisma` file, you write `prisma/contract.ts` with the `defineContract` builder, and [`prisma-next contract emit`](/cli/next/contract-emit) produces exactly the same `contract.json` and `contract.d.ts` a PSL schema would.

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

[PSL](/orm/next/contract-authoring/psl-syntax) is the preferred authoring surface, and since both modes emit identical artifacts, you give up nothing by staying with it. The TypeScript builder is an escape hatch for the cases PSL does not cover. Reach for it when:

* model definitions must be split, composed, or reused across ordinary TypeScript modules or packages
* parts of the schema are assembled programmatically from other static definitions (the [purity rules](#keep-the-contract-file-pure) still apply)

If neither applies, write PSL: it is more compact, and it is what `create-prisma` scaffolds and [`prisma-next contract infer`](/cli/next/contract-infer) writes.

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

The config's `contract` path names the source of truth. A `.ts` extension selects TypeScript authoring:

  

#### PostgreSQL

```typescript title="prisma-next.config.ts" 
import { defineConfig } from "@prisma-next/postgres/config";

export default defineConfig({
  contract: "./prisma/contract.ts",
});
```

#### MongoDB

```typescript title="prisma-next.config.ts" 
import { defineConfig } from "@prisma-next/mongo/config";

export default defineConfig({
  contract: "./prisma/contract.ts",
});
```

A complete contract [#a-complete-contract]

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

  

#### PostgreSQL

```typescript title="prisma/contract.ts" 
import pgvector from "@prisma-next/extension-pgvector/pack";
import { defineContract, enumType, member, rel } from "@prisma-next/postgres/contract-builder";

const pgText = { codecId: "pg/text@1", nativeType: "text" } as const;

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

export const contract = defineContract(
  {
    extensionPacks: { pgvector },
  },
  ({ field, model, type }) => {
    const types = {
      Embedding1536: type.pgvector.Vector(1536),
    } as const;

    const User = model("User", {
      fields: {
        id: field.id.uuidv4String(),
        email: field.text(),
        createdAt: field.temporal.createdAt(),
        updatedAt: field.temporal.updatedAt(),
        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(),
        embedding: field.namedType(types.Embedding1536).optional(),
      },
    });

    return {
      enums: { Priority },
      types,
      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(({ cols, constraints }) => ({
          table: "post",
          foreignKeys: [
            constraints.foreignKey(cols.userId, User.refs.id, {
              name: "post_userId_fkey",
            }),
          ],
        })),
      },
    };
  },
);
```

#### MongoDB

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

const User = model("User", {
  collection: "users",
  fields: {
    _id: field.objectId(),
    name: field.string(),
    email: field.string(),
    bio: field.string().optional(),
  },
  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,
  },
});
```

Run `npx prisma-next contract emit` after any change to refresh the artifacts.

The two builders share the same shape but differ where the databases do. On PostgreSQL, fields pick column types and models chain `.sql({ table })` to map storage; on MongoDB, the ID is a `field.objectId()` named `_id`, scalar helpers are `field.string()`, `field.int32()`, `field.double()`, `field.bool()`, and `field.date()`, and the collection is an inline `collection` option on the model rather than a chained call.

How defineContract works [#how-definecontract-works]

`defineContract` takes two arguments: an options object and a factory function.

The options object declares what the contract is built from, most importantly `extensionPacks`. The target and database family are already bound by the import: `@prisma-next/postgres/contract-builder` produces PostgreSQL contracts, so you never name them yourself.

The factory receives authoring helpers composed from the target and every extension pack you declared: `field` for field definitions, `model` for models, and `type` for pack-provided types (which is why `type.pgvector` exists in the example, and only when `pgvector` is in `extensionPacks`). The factory returns the contract's content: `models`, plus optional `enums` and `types`.

The MongoDB builder is simpler: it exports `field`, `model`, and `rel` directly, and its `defineContract` takes a single definition object with the `models`, as in the MongoDB tab above. The sections below use the PostgreSQL builder.

Fields [#fields]

`field` provides typed constructors for common field shapes:

* `field.text()`, `field.uuidString()`, `field.json()` for scalar fields
* `field.id.uuidv4String()` for a UUID primary key with a client-generated default
* `field.temporal.createdAt()` and `field.temporal.updatedAt()` for managed timestamps
* `field.namedType(x)` for enums and declared named types

The exact helper set comes from the target and the composed extension packs. Every field builder supports chained modifiers:

* `.optional()` makes the field nullable.
* `.default(value)` sets a literal default; `.defaultSql(expression)` sets a database function default.
* `.unique()` adds a unique constraint; `.id()` marks the primary key.
* `.column("column_name")` sets the physical column name when it differs from the field name.

Enums [#enums]

`enumType` declares an enum with an explicit storage codec and members:

```typescript
const Priority = enumType(
  "Priority",
  { codecId: "pg/text@1", nativeType: "text" } as const,
  member("Low", "low"),
  member("High", "high"),
);
```

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 factory's returned `enums` map so it is emitted.

Relations [#relations]

Relations are declared on the model builder with `.relations(...)` and the `rel` helpers:

```typescript
User.relations({
  posts: rel.hasMany(Post, { by: "userId" }),
});

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

`rel.hasMany(Model, { by })` names the foreign key field on the other model; `rel.belongsTo(Model, { from, to })` maps the local foreign key field to the referenced field. `rel.hasOne` and `rel.manyToMany` cover the remaining shapes. A typo in a relation target is a compile error either way: the PostgreSQL builder takes model objects, and the MongoDB builder takes model names that are type-checked against the declared models.

Storage mapping [#storage-mapping]

`.sql(...)` maps a model to its table. The object form covers the common case:

```typescript
User.relations({ ... }).sql({ table: "user" });
```

The callback form additionally exposes the model's columns and constraint builders, for foreign keys with explicit names:

```typescript
Post.relations({ ... }).sql(({ cols, constraints }) => ({
  table: "post",
  foreignKeys: [
    constraints.foreignKey(cols.userId, User.refs.id, { name: "post_userId_fkey" }),
  ],
}));
```

`Model.refs` provides typed references to another model's fields, so `User.refs.id` is checked against the actual `User` definition.

Extension types [#extension-types]

Declare packs in `defineContract`'s options and the factory's `type` helper exposes their constructors:

```typescript
import pgvector from "@prisma-next/extension-pgvector/pack";

export const contract = defineContract(
  { extensionPacks: { pgvector } },
  ({ field, model, type }) => {
    const types = { Embedding1536: type.pgvector.Vector(1536) } as const;
    // ... use field.namedType(types.Embedding1536) in a model
    return { types, models: { /* ... */ } };
  },
);
```

The same pack must also be composed in `prisma-next.config.ts` (as `extensions: [pgvector]`, using the pack's `/control` export) so the CLI and runtime agree with the contract.

Keep the contract file pure [#keep-the-contract-file-pure]

The contract file describes structure; emission canonicalizes it to JSON and hashes it, and the same source must always produce the same bytes. That works only if the file is pure data:

* Do not read `process.env`, the current time, or random values into the contract. A contract that changes per machine or per run breaks hashing and verification.
* 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. Emission evaluates it to obtain the contract object and nothing else.

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

Parity with PSL [#parity-with-psl]

TypeScript and [PSL](/orm/next/contract-authoring/psl-syntax) authoring emit the same canonical artifact for an equivalent schema. You can move between the modes without any downstream change, but a project declares exactly one source of truth: the file named in the config. Keep the other form out of the project so the two can never disagree.

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

Projects scaffolded with `create-prisma@next` install [Prisma Next skills](/ai/tools/skills#available-skills-for-prisma-next) for your coding agent; the `prisma-next-contract` skill covers this page. Ask your agent to:

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

Next steps [#next-steps]

* Emit and inspect [the artifacts](/orm/next/contract-authoring/the-contract-artifact) the contract produces.
* Apply the contract to a database with [`prisma-next db init`](/cli/next/db-init) or plan changes with [`prisma-next migration plan`](/cli/next/migration-plan).

## Related pages

- [`Author in PSL`](https://www.prisma.io/docs/orm/next/contract-authoring/psl-syntax): Write the Prisma Next contract as a Prisma schema file, using the schema language you already know plus the Prisma Next additions.
- [`Capabilities`](https://www.prisma.io/docs/orm/next/contract-authoring/capabilities): Capabilities record what your database stack supports, so Prisma Next can reject unsupported features early with a clear error.
- [`The data contract`](https://www.prisma.io/docs/orm/next/contract-authoring/the-data-contract): The data contract is the single description of your data model and its storage layout. Everything in Prisma Next is typed, planned, and verified against it.
- [`The emitted artifacts`](https://www.prisma.io/docs/orm/next/contract-authoring/the-contract-artifact): contract.json and contract.d.ts are the deterministic artifacts every other part of Prisma Next consumes. Here is what is inside them.