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
- •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(),
})
- •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
- •Push the schema:
npm run db:push
Database Providers
| Provider | Connection String Format |
|---|---|
| Neon (recommended) | postgresql://user:pass@ep-xxx.region.aws.neon.tech/dbname?sslmode=require |
| Supabase | postgresql://postgres:pass@db.xxx.supabase.co:5432/postgres |
| PlanetScale | mysql://user:pass@host/dbname?ssl={"rejectUnauthorized":true} |
| Local PostgreSQL | postgresql://user:pass@localhost:5432/dbname |