Skip to content

Database

TheTerms uses Prisma 6 with PostgreSQL. The schema is defined in packages/db/prisma/schema.prisma. All database access goes through the Prisma client — no raw SQL.

13 models. There is no separate Membership model — User has a direct org_id and role (OWNER/ADMIN/EDITOR/VIEWER). There is no separate Clause model either — clauses are a JSON array (content: {clauses: [...]}) on DocumentVersion. Signer decisions land in SigningRequest.assent_map, a {clause_id: boolean} JSON map — there’s no SigningResponse model.

Organisation ──┬── User (org_id + role directly on User, no join table)
├── Container
│ └── Document
│ └── DocumentVersion (content JSON holds clauses)
│ └── SigningRequest (assent_map JSON holds decisions)
│ └── SignatureEvent (audit trail: VIEWED/SUBMITTED/VOIDED/EXPIRED)
├── Invitation (team invites)
├── ApiKey
├── Webhook
│ └── WebhookDelivery
├── BatchJob (bulk CSV signing invites)
└── OrgMonthlyUsage (usage tracking for tier limits)

For development (schema changes you’ll commit)

Section titled “For development (schema changes you’ll commit)”
Terminal window
pnpm db:migrate --name describe-your-change

This generates a SQL migration file in packages/db/prisma/migrations/. Always commit migration files with your PR.

Terminal window
pnpm db:push

This applies the schema directly to your local database. Suitable for first-time setup.

Migrations run automatically on container startup via the docker-entrypoint.sh script, which calls:

Terminal window
prisma migrate deploy --schema=./packages/db/prisma/schema.prisma

To visually browse your local database:

Terminal window
cd packages/db
npx prisma studio

This opens a web interface at http://localhost:5555.

Prisma’s Json type doesn’t overlap with specific TypeScript interfaces. When reading JSON fields, use a double-cast:

const content = document.content as unknown as DocumentContent;

Prefer findUnique + explicit TRPCError over findUniqueOrThrow for better error messages:

const container = await prisma.container.findUnique({ where: { id } });
if (!container) throw new TRPCError({ code: "NOT_FOUND", message: "Container not found" });