Agent skill
Neon Database
Serverless PostgreSQL database patterns for LivestockAI using Neon and Kysely ORM
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/neon-database
SKILL.md
Neon Database
LivestockAI uses Neon as its serverless PostgreSQL database, accessed through the Kysely query builder for type-safe SQL.
Connection Pattern
The database connection is managed through app/lib/db/index.ts with two access patterns:
Server Functions (Cloudflare Workers)
ALWAYS use getDb() in server functions - this is critical for Cloudflare Workers compatibility:
export const myServerFn = createServerFn({ method: 'GET' }).handler(
async () => {
const { getDb } = await import('~/lib/db')
const db = await getDb()
return db.selectFrom('batches').execute()
},
)
CLI Scripts (Node.js/Bun)
For seeders, migrations, and CLI scripts, use the direct export:
import { db } from '~/lib/db'
await db.insertInto('users').values({...}).execute()
Why getDb()?
Cloudflare Workers doesn't support process.env. Environment variables come from the env binding via cloudflare:workers, which is only available during request handling. The getDb() function:
- Tries
process.env.DATABASE_URLfirst (Node.js/Bun) - Falls back to
cloudflare:workersenv binding - Creates a singleton Kysely instance
Common Mistakes
// ❌ WRONG - breaks on Cloudflare Workers
import { db } from '~/lib/db'
export const fn = createServerFn().handler(async () => {
return db.selectFrom('users').execute()
})
// ❌ WRONG - old pattern, doesn't work
const { db } = await import('~/lib/db')
// ✅ CORRECT
const { getDb } = await import('~/lib/db')
const db = await getDb()
Environment Variables
# Local development (.env or .dev.vars)
DATABASE_URL=postgres://user:pass@ep-xxx.region.neon.tech/livestockai?sslmode=require
# Production (Cloudflare secrets)
wrangler secret put DATABASE_URL
# Test database
DATABASE_URL_TEST=postgres://user:pass@ep-xxx.region.neon.tech/livestockai_test?sslmode=require
Database Schema
The schema is defined in app/lib/db/types.ts with 23+ tables including:
| Table | Purpose |
|---|---|
users |
User accounts (Better Auth) |
user_settings |
Preferences (currency, units, language) |
farms |
Farm entities |
farm_modules |
Enabled livestock types per farm |
batches |
Livestock batches (all 6 types) |
mortality_records |
Death tracking |
feed_records |
Feed consumption |
weight_samples |
Growth tracking |
sales |
Revenue records |
expenses |
Cost tracking |
invoices |
Customer invoices |
Migrations
Migrations are in app/lib/db/migrations/:
# Run migrations
bun run db:migrate
# Rollback
bun run db:rollback
# Reset (drop all + migrate)
bun run db:reset
Migration naming: YYYY-MM-DD-NNN-description.ts
Kysely Query Examples
// Select with joins
const batches = await db
.selectFrom('batches')
.leftJoin('farms', 'farms.id', 'batches.farmId')
.select(['batches.id', 'batches.species', 'farms.name as farmName'])
.where('batches.status', '=', 'active')
.execute()
// Insert with returning
const result = await db
.insertInto('batches')
.values({ farmId, species, initialQuantity })
.returning('id')
.executeTakeFirstOrThrow()
// Update
await db
.updateTable('batches')
.set({ status: 'depleted' })
.where('id', '=', batchId)
.execute()
// Aggregate
const stats = await db
.selectFrom('sales')
.select(sql<number>`sum(quantity)`.as('totalSold'))
.where('batchId', '=', batchId)
.executeTakeFirst()
Related Skills
kysely-orm- Detailed Kysely patternscloudflare-workers- Deployment environmentthree-layer-architecture- Repository layer patterns
Didn't find tool you were looking for?