Skip to main content

Overview

GTM Feedback uses Drizzle ORM with PostgreSQL (recommended: Neon) for all database operations.
Critical: This project uses Drizzle Relational Query Builder (RQB) syntax exclusively. Never use db.select() patterns.

Database Schema

The schema is located in packages/database/src/schema.ts.

Core Tables

users

User accounts with authentication and admin flags

requests

Feature requests with areas, status, and links

feedback

Customer-specific feedback linked to accounts/opportunities

areas

Product areas for categorization

sfdcAccounts

Salesforce account data (ARR, enterprise status)

sfdcOpportunities

Salesforce opportunities with ARR and stages

Schema Example

export const users = pgTable(
  "users",
  {
    id: uuid().defaultRandom().primaryKey().notNull(),
    name: text("name").notNull(),
    email: text("email").unique().notNull(),
    image: text("image"),
    avatar: text("avatar"),
    emailVerified: timestamp("emailVerified", { mode: "date" }),
    isAdmin: boolean("is_admin").default(false).notNull(),
  },
  (table) => [unique("users_email_key").on(table.email)],
);

Relational Query Builder (RQB)

Critical: Always use Drizzle Relational Query Builder (RQB) syntax. Never use db.select() patterns.

Correct Pattern: Using Query Engine

import { db } from "@feedback/db";

// find many with relations
const feedback = await db.query.feedback.findMany({
  with: {
    user: true,
    entries: {
      with: { user: true },
      orderBy: (entries, { desc }) => [desc(entries.createdAt)]
    },
    comments: {
      orderBy: (comments, { desc }) => [desc(comments.createdAt)],
      limit: 10
    }
  },
  where: (feedback, { eq, and, isNotNull }) => and(
    eq(feedback.status, 'open'),
    isNotNull(feedback.creator)
  ),
  orderBy: (feedback, { desc }) => [desc(feedback.updatedAt)]
});

Incorrect Pattern: Never Use This

import { db } from "@feedback/db";
import { eq } from "drizzle-orm";
import { Feedback } from "@feedback/db/schema";

// NEVER use db.select() syntax
const result = await db.select().from(Feedback).where(eq(Feedback.id, id));

Database Relations

Understanding the relationship patterns:

One-to-Many Relationships

One user creates many feedback items:
const user = await db.query.users.findFirst({
  where: (users, { eq }) => eq(users.id, userId),
  with: {
    feedback: {
      orderBy: (feedback, { desc }) => [desc(feedback.createdAt)],
      limit: 10
    }
  }
});

Migrations

Configuration

Drizzle is configured in packages/database/drizzle.config.ts:
import "dotenv/config";
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: "postgresql",
  schema: "./src/schema.ts",
  out: "./drizzle/migrations",
  strict: true,
  verbose: true,
  dbCredentials: {
    url: process.env.DATABASE_URL ?? "",
  },
});

Applying Schema Changes

1

Push schema changes

For development, use db:push to sync schema without migrations:
pnpm db:push
This is the recommended approach for local development.
2

Generate migrations (production)

For production deployments, generate migration files:
cd packages/database
pnpm drizzle-kit generate
3

Apply migrations

Apply generated migrations:
pnpm drizzle-kit migrate
The db:push command is filtered to run in the www workspace: pnpm --filter www db:push

Seeding Data

The seed script populates demo data for local development:
pnpm db:seed
This script:
  1. Creates demo users
  2. Creates product areas
  3. Populates feature requests
  4. Generates sample feedback
  5. Creates embeddings (if AI configured)
  6. Links accounts and opportunities
Seeding is optional but helpful for testing the full application flow.

Query Patterns from CLAUDE.md

Follow these established patterns from the codebase:

Pattern 1: Simple Queries

// fetch single record by ID
const request = await db.query.requests.findFirst({
  where: (requests, { eq }) => eq(requests.id, requestId)
});

// fetch with basic filtering
const openRequests = await db.query.requests.findMany({
  where: (requests, { eq }) => eq(requests.status, 'open'),
  orderBy: (requests, { desc }) => [desc(requests.updatedAt)]
});

Pattern 2: Eager Loading Relations

// load related data in a single query
const feedback = await db.query.feedback.findFirst({
  where: (feedback, { eq }) => eq(feedback.id, feedbackId),
  with: {
    user: true,  // load creator
    request: {   // load request with its creator
      with: { user: true }
    },
    comments: {  // load comments with pagination
      with: { user: true },
      orderBy: (comments, { desc }) => [desc(comments.createdAt)],
      limit: 10
    }
  }
});

Pattern 3: Complex Filtering

// combine multiple conditions
const results = await db.query.feedback.findMany({
  where: (feedback, { eq, and, or, gte, inArray }) => and(
    eq(feedback.severity, 'high'),
    or(
      inArray(feedback.accountId, accountIds),
      gte(feedback.createdAt, thirtyDaysAgo)
    )
  ),
  with: { user: true, request: true }
});

Pattern 4: Ordering and Limiting

// sort and paginate results
const recentFeedback = await db.query.feedback.findMany({
  where: (feedback, { eq }) => eq(feedback.requestId, requestId),
  with: { user: true },
  orderBy: (feedback, { desc }) => [desc(feedback.createdAt)],
  limit: 20,
  offset: page * 20
});

Common Operations

import { db } from "@feedback/db";
import { feedback } from "@feedback/db/schema";

const [newFeedback] = await db.insert(feedback)
  .values({
    requestId,
    accountId,
    severity: 'high',
    description: 'Customer needs this feature',
    creator: userId,
  })
  .returning();
import { db } from "@feedback/db";
import { requests } from "@feedback/db/schema";
import { eq } from "drizzle-orm";

await db.update(requests)
  .set({ 
    status: 'shipped',
    updatedAt: new Date().toISOString()
  })
  .where(eq(requests.id, requestId));
import { db } from "@feedback/db";
import { feedback } from "@feedback/db/schema";
import { eq } from "drizzle-orm";

await db.delete(feedback)
  .where(eq(feedback.id, feedbackId));
import { db } from "@feedback/db";
import { requests } from "@feedback/db/schema";
import { eq, count } from "drizzle-orm";

const [result] = await db
  .select({ count: count() })
  .from(requests)
  .where(eq(requests.status, 'open'));

Best Practices

Use RQB Syntax

Always use db.query.tableName patterns, never db.select()

Eager Load Relations

Use with clause to avoid N+1 queries

Type Safety

Let TypeScript infer types from Drizzle queries

Indexes

Use indexes for frequently queried fields (defined in schema)

Troubleshooting

Verify your DATABASE_URL environment variable:
echo $DATABASE_URL
Test the connection:
psql $DATABASE_URL
If migrations are out of sync:
# Reset local database (dev only!)
pnpm db:push
pnpm db:seed
Ensure you’re using the callback syntax for where and orderBy:
// correct
where: (table, { eq }) => eq(table.id, id)

// incorrect
where: eq(table.id, id)

Next Steps

Workflows

Learn about background workflows

Contributing

Read contribution guidelines

Build docs developers (and LLMs) love