Protokit

Database

Configure DrizzleORM with PostgreSQL and manage your database schema

Database

Datyze uses DrizzleORM with PostgreSQL for type-safe database operations. The schema is defined in the @lucashochart/database package.

Configuration

packages/database/
├── src/
│   ├── schema.ts     # Database schema definitions
│   └── index.ts      # Database client and exports
├── drizzle/          # Migration files
└── drizzle.config.ts # Drizzle configuration

Connection

Set your database URL in .env:

DATABASE_URL="postgresql://user:password@localhost:5432/datyze"

Schema Overview

Authentication Tables

// User table
export const user = pgTable("user", {
  id: uuid("id").defaultRandom().primaryKey(),
  name: text("name"),
  email: text("email").unique().notNull(),
  emailVerified: timestamp("emailVerified"),
  image: text("image"),
  isAdmin: boolean("isAdmin").default(false),
  createdAt: timestamp("createdAt").defaultNow().notNull(),
  updatedAt: timestamp("updatedAt").defaultNow().notNull(),
});
 
// Account (OAuth connections)
export const account = pgTable("account", {
  userId: uuid("userId")
    .notNull()
    .references(() => user.id),
  type: text("type").notNull(),
  provider: text("provider").notNull(),
  providerAccountId: text("providerAccountId").notNull(),
  // ... OAuth tokens
});
 
// Session
export const session = pgTable("session", {
  sessionToken: text("sessionToken").primaryKey(),
  userId: uuid("userId")
    .notNull()
    .references(() => user.id),
  expires: timestamp("expires").notNull(),
});

Multi-tenancy Tables

// Team
export const team = pgTable("team", {
  id: uuid("id").defaultRandom().primaryKey(),
  name: text("name").notNull(),
  slug: text("slug").unique().notNull(),
  createdAt: timestamp("createdAt").defaultNow().notNull(),
  updatedAt: timestamp("updatedAt").defaultNow().notNull(),
});
 
// Team Member
export const teamMember = pgTable("team_member", {
  id: uuid("id").defaultRandom().primaryKey(),
  teamId: uuid("teamId")
    .notNull()
    .references(() => team.id),
  userId: uuid("userId")
    .notNull()
    .references(() => user.id),
  role: teamRoleEnum("role").default("member").notNull(),
  createdAt: timestamp("createdAt").defaultNow().notNull(),
});
 
// Team Roles
export const teamRoleEnum = pgEnum("team_role", ["owner", "admin", "member"]);

Subscription Tables

export const subscription = pgTable("subscription", {
  id: uuid("id").defaultRandom().primaryKey(),
  teamId: uuid("teamId")
    .notNull()
    .references(() => team.id),
 
  // Stripe fields
  stripeCustomerId: text("stripeCustomerId"),
  stripeSubscriptionId: text("stripeSubscriptionId"),
 
  // Lemon Squeezy fields
  lemonSqueezyCustomerId: text("lemonSqueezyCustomerId"),
  lemonSqueezySubscriptionId: text("lemonSqueezySubscriptionId"),
 
  // Common fields
  status: subscriptionStatusEnum("status").default("inactive"),
  plan: text("plan"),
  currentPeriodEnd: timestamp("currentPeriodEnd"),
});

Invitation Table

export const invitation = pgTable("invitation", {
  id: uuid("id").defaultRandom().primaryKey(),
  teamId: uuid("teamId")
    .notNull()
    .references(() => team.id),
  email: text("email").notNull(),
  role: teamRoleEnum("role").default("member").notNull(),
  token: text("token").unique().notNull(),
  invitedBy: uuid("invitedBy")
    .notNull()
    .references(() => user.id),
  expiresAt: timestamp("expiresAt").notNull(),
  acceptedAt: timestamp("acceptedAt"),
  createdAt: timestamp("createdAt").defaultNow().notNull(),
});

Database Operations

Querying Data

import { db } from "@lucashochart/database";
import { user, team, teamMember } from "@lucashochart/database/schema";
import { eq } from "drizzle-orm";
 
// Find user by email
const foundUser = await db.query.user.findFirst({
  where: eq(user.email, "user@example.com"),
});
 
// Find user's teams
const userTeams = await db.query.teamMember.findMany({
  where: eq(teamMember.userId, userId),
  with: {
    team: true,
  },
});

Inserting Data

import { db } from "@lucashochart/database";
import { team } from "@lucashochart/database/schema";
 
const newTeam = await db
  .insert(team)
  .values({
    name: "My Team",
    slug: "my-team",
  })
  .returning();

Updating Data

import { db } from "@lucashochart/database";
import { user } from "@lucashochart/database/schema";
import { eq } from "drizzle-orm";
 
await db.update(user).set({ name: "New Name" }).where(eq(user.id, userId));

Deleting Data

import { db } from "@lucashochart/database";
import { invitation } from "@lucashochart/database/schema";
import { eq } from "drizzle-orm";
 
await db.delete(invitation).where(eq(invitation.id, invitationId));

Database Commands

CommandDescription
pnpm db:pushPush schema changes to database (dev)
pnpm db:generateGenerate migration files
pnpm db:migrateRun migrations (production)
pnpm db:studioOpen Drizzle Studio GUI

Adding New Tables

  1. Define the table in packages/database/src/schema.ts:
export const myTable = pgTable("my_table", {
  id: uuid("id").defaultRandom().primaryKey(),
  name: text("name").notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});
  1. Export from the schema file

  2. Run pnpm db:push to apply changes

In production, use pnpm db:generate followed by pnpm db:migrate for safer migrations.

Relations

Define relations for Drizzle's query builder:

export const teamRelations = relations(team, ({ many, one }) => ({
  members: many(teamMember),
  subscription: one(subscription, {
    fields: [team.id],
    references: [subscription.teamId],
  }),
  invitations: many(invitation),
}));

Indexes

Add indexes for frequently queried columns:

export const invitation = pgTable(
  "invitation",
  {
    // ... columns
  },
  (table) => ({
    emailIdx: index("invitation_email_idx").on(table.email),
    teamIdIdx: index("invitation_team_id_idx").on(table.teamId),
  })
);

Next Steps

On this page