Skip to main content

Schema location

prisma/schema.prisma

Applying schema changes

pnpm install --silent && pnpm run db:push && pnpm run db:generate
pnpm install --silent is required first because prisma.config.ts imports from the prisma package.
Do not add a url property to the datasource block in schema.prisma. Database URLs are configured in prisma.config.ts.

Built-in models

User (do not modify fields)

model User {
  id            String   @id
  externalId    String?  @unique
  email         String?
  emailVerified Boolean  @default(false)
  name          String?
  picture       String?
  lastSeenAt    DateTime @default(now()) @updatedAt
}
You may add relations to User from other models. Use externalId to map to internal user IDs.
To store additional user attributes, create a separate table with a relation to User (e.g., a UserProfile model) rather than modifying the User model directly.

Organization

For multi-tenant apps, use the built-in organization tables:
model Organization {
  id         String               @id @default(cuid())
  name       String
  externalId String?              @unique
  createdAt  DateTime             @default(now())
  members    OrganizationMember[]
}

model OrganizationMember {
  orgId      String
  userId     String
  org        Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
  user       User         @relation(fields: [userId], references: [id], onDelete: Cascade)
  assignedAt DateTime     @default(now())

  @@id([orgId, userId])
  @@index([userId])
}
Filter queries by ctx.currentOrgId for organization-scoped data:
getSummaries: scopedProcedure([])
  .meta({ description: 'Get summaries for the current organization' })
  .query(async ({ ctx }) => {
    if (!ctx.currentOrgId) {
      throw new TRPCError({ code: 'BAD_REQUEST', message: 'No organization selected' });
    }
    return ctx.db.summary.findMany({
      where: { orgId: ctx.currentOrgId },
    });
  }),

Additional built-in models

Beyond User and Organization, the scaffolded schema includes framework-managed models for:
CategoryModels
ServicesAppService, AppServiceSetting, AppServiceCustomAuthSetting, AppServiceSystemCredential, AppServiceUserCredential, AppServiceOAuthSystemCredential, AppServiceOAuthUserCredential, AppServiceOAuthState
OAuthOAuthClient, OAuthAuthorizationCode, OAuthRefreshToken, OAuthConsent
Identity providersIdentityProvider, IdentityProviderLink, IdpAuthSession
WorkflowsWfJob, WfSchedule, WfStepLog, WfDraft, WfWorker
These models are managed by the framework and their RLS policies are configured in scopes.json. You do not need to modify them.

Database access

Always use ctx.db in procedure handlers. It injects RLS context automatically.

Indexing requirements

Prisma does not automatically create indexes on foreign key columns. Add @@index explicitly for:
Column typeReason
Foreign key columnsEfficient joins
WHERE clause columnsEfficient filtering
ORDER BY columnsEfficient sorting
Membership columnsRLS group membership lookups

Relations and data fetching

Use Prisma include to fetch related data in a single query:
getTasksWithAuthors: scopedProcedure([])
  .meta({ description: 'Get tasks with author details' })
  .query(async ({ ctx }) => {
    return ctx.db.task.findMany({
      where: { userId: ctx.userId },
      include: { author: { select: { id: true, name: true, email: true, picture: true } } },
      orderBy: { createdAt: 'desc' },
    });
  }),

Transactions

Use Prisma transactions for operations that must succeed or fail together:
transferTask: scopedProcedure(['tasks:editAll'])
  .meta({ description: 'Transfer a task to another user' })
  .input(z.object({ taskId: z.string(), toUserId: z.string() }))
  .mutation(async ({ ctx, input }) => {
    return ctx.db.$transaction([
      ctx.db.task.update({
        where: { id: input.taskId },
        data: { userId: input.toUserId },
      }),
      ctx.db.taskHistory.create({
        data: {
          taskId: input.taskId,
          action: 'transferred',
          performedBy: ctx.userId,
        },
      }),
    ]);
  }),

Environment differences

EnvironmentDatabaseRLS enforcement
DevelopmentSQLite (LibSQL)Application-level filtering
ProductionPostgreSQLDatabase-level RLS policies
The framework handles the difference transparently.

User identity

ContextHow to get user ID
Server (procedures)ctx.userId
Client (components)useAuth().user.id
Never accept user identity as tRPC input — it cannot be considered authentic, and may break due to procedure-level or RLS-level scoping. Always use ctx.userId on the backend.