Database

indiegoon uses Drizzle ORM with PostgreSQL (via Neon's serverless driver). It's type-safe, fast, and requires zero runtime dependencies beyond a connection string.

Setup

Using the CLI

goon setup db

Paste your connection string and the CLI validates it.

Manual Setup

DATABASE_URL="postgresql://user:password@host.neon.tech/dbname?sslmode=require"

# Optional: unpooled URL for migrations (Neon/Supabase)
DATABASE_URL_UNPOOLED="postgresql://user:password@host.neon.tech/dbname?sslmode=require"

Database Connection

The connection is in src/lib/db/index.ts:

import { neon } from "@neondatabase/serverless"
import { drizzle } from "drizzle-orm/neon-http"
import * as schema from "./schema"

const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql, { schema })

Uses Neon's HTTP driver — perfect for serverless (no persistent connections, no connection pooling issues).

Schema

All tables are defined in src/lib/db/schema/:

Users & Auth (schema/auth.ts)

export const users = pgTable("users", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  emailVerified: boolean("email_verified").default(false),
  image: text("image"),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
})

Also defines: sessions, accounts, verifications.

Subscriptions (schema/subscriptions.ts)

export const subscriptions = pgTable("subscriptions", {
  id: text("id").primaryKey(),
  userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
  customerId: text("customer_id").unique(),
  subscriptionId: text("subscription_id").unique(),
  priceId: text("price_id"),
  status: text("status").default("inactive"),
  currentPeriodEnd: timestamp("current_period_end"),
  cancelAtPeriodEnd: boolean("cancel_at_period_end").default(false),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
})

Teams (schema/teams.ts)

export const teamRoleEnum = pgEnum("team_role", ["owner", "admin", "member"])

export const teams = pgTable("teams", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  slug: text("slug").notNull().unique(),
  logo: text("logo"),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
})

export const teamMembers = pgTable("team_members", { ... })
export const teamInvitations = pgTable("team_invitations", { ... })

Leads (schema/leads.ts)

export const leads = pgTable("leads", {
  id: text("id").primaryKey(),
  email: text("email").notNull().unique(),
  source: text("source"),
  createdAt: timestamp("created_at").defaultNow(),
})

Migrations

Push Schema (Development)

Fast iteration — pushes schema directly without migration files:

npm run db:push

Generate Migration (Production)

Creates a SQL migration file:

npm run db:generate

Run Migrations

Applies pending migrations:

npm run db:migrate

Open Drizzle Studio

Visual database browser:

npm run db:studio

Querying Data

Drizzle provides a type-safe query builder:

Select

import { db } from "@/lib/db"
import { users } from "@/lib/db/schema"
import { eq } from "drizzle-orm"

// Find user by email
const user = await db.query.users.findFirst({
  where: eq(users.email, "hello@example.com"),
})

// List all active subscriptions
const activeSubs = await db.query.subscriptions.findMany({
  where: eq(subscriptions.status, "active"),
  with: { user: true },
})

Insert

import { db } from "@/lib/db"
import { leads } from "@/lib/db/schema"

await db.insert(leads).values({
  id: crypto.randomUUID(),
  email: "new@user.com",
  source: "landing-page",
})

Update

await db
  .update(users)
  .set({ name: "New Name" })
  .where(eq(users.id, userId))

Delete

await db.delete(users).where(eq(users.id, userId))

Adding New Tables

  1. Create a new schema file in src/lib/db/schema/:
// src/lib/db/schema/projects.ts
import { pgTable, text, timestamp } from "drizzle-orm/pg-core"
import { users } from "./auth"

export const projects = pgTable("projects", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
  createdAt: timestamp("created_at").defaultNow(),
})
  1. Export it from the barrel file:
// src/lib/db/schema/index.ts
export * from "./auth"
export * from "./subscriptions"
export * from "./teams"
export * from "./leads"
export * from "./projects"  // Add this
  1. Push the schema:
npm run db:push

Database Providers

ProviderConnection String Format
Neon (recommended)postgresql://user:pass@ep-xxx.region.aws.neon.tech/dbname?sslmode=require
Supabasepostgresql://postgres:pass@db.xxx.supabase.co:5432/postgres
PlanetScalemysql://user:pass@host/dbname?ssl={"rejectUnauthorized":true}
Local PostgreSQLpostgresql://user:pass@localhost:5432/dbname

Next Steps

  • Email — Send emails using your provider of choice
  • Teams — Multi-tenant data architecture