Claude-skill-registry db-sqlite
SQLite database management with Prisma ORM, type-safe queries, and Railway deployment with Litestream backup. This skill should be used when creating database schemas, writing migrations, managing SQLite on Railway volumes, or troubleshooting database issues.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/db-sqlite" ~/.claude/skills/majiayu000-claude-skill-registry-db-sqlite && rm -rf "$T"
skills/data/db-sqlite/SKILL.mdSQLite Database Skill
Comprehensive patterns for SQLite database management in Node.js/TypeScript projects using Prisma ORM, including schema-first development and Railway deployment with Litestream backup.
When to Use This Skill
- Setting up SQLite in a new project
- Defining database schemas with Prisma
- Running migrations with
prisma migrate - Deploying SQLite to Railway with persistent volumes
- Backing up production databases with Litestream
- Troubleshooting database issues
Core Concepts
SQLite vs Network Databases
SQLite is appropriate when:
- Single server/container deployment
- Read-heavy workloads (or moderate writes)
- Simplicity is valued over horizontal scaling
- Local-first or embedded scenarios
- Cost-sensitive deployments
Consider PostgreSQL when:
- Multiple servers need database access
- Remote database inspection is required
- High write concurrency is expected
- Team needs direct database access for debugging
Railway Deployment Constraints
SQLite on Railway requires understanding these constraints:
- Volume mounting - Database file must live on a Railway volume (not container filesystem)
- No remote access - Cannot connect database GUI tools directly to production
- Single container - Only one instance can write to the database
- Backup strategy - Use Litestream for continuous backup to Railway Bucket
Critical: Railway Volume Path vs Container Path
This is the #1 cause of data loss on Railway SQLite deployments.
Railway volumes mount at a specific path (e.g.,
/data). But your app runs in /app/ by default. If your code writes to ./prisma/app.db, it creates the file at /app/prisma/app.db — which is NOT on the volume and gets destroyed on every deploy.
Solution: Set
DATABASE_URL to use the volume path in production.
# Wrong (data lost on each deploy): file:/app/prisma/app.db ← Container filesystem, not persistent # Correct (data persists): file:/data/app.db ← Railway volume, persistent + backed up
Database Setup Pattern
Package Installation
npm install @prisma/client npm install -D prisma
Directory Structure
prisma/ ├── schema.prisma # Database schema (source of truth) └── migrations/ # Generated SQL migrations src/lib/db/ └── index.ts # Prisma client singleton
Environment Configuration
# .env.local (development) DATABASE_URL="file:./prisma/dev.db" # Railway (production) — REQUIRED # Must point to volume mount path DATABASE_URL="file:/data/app.db"
Prisma Schema Setup
Create
prisma/schema.prisma:
generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } // Define your models here model User { id String @id @default(cuid()) email String @unique name String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
Prisma Client Singleton
Create
src/lib/db/index.ts:
import { PrismaClient } from '@prisma/client' const globalForPrisma = globalThis as unknown as { prisma: PrismaClient | undefined } export const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], }) if (process.env.NODE_ENV !== 'production') { globalForPrisma.prisma = prisma } export default prisma
Package.json Scripts
{ "scripts": { "db:generate": "prisma generate", "db:migrate": "prisma migrate dev", "db:migrate:prod": "prisma migrate deploy", "db:push": "prisma db push", "db:studio": "prisma studio", "db:reset": "prisma migrate reset" } }
Schema Patterns
Primary Keys
Use CUID or UUID for user-facing entities:
model User { id String @id @default(cuid()) // or: id String @id @default(uuid()) }
Timestamps
Always include created/updated timestamps:
model Post { id String @id @default(cuid()) title String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
Relations
One-to-many:
model User { id String @id @default(cuid()) posts Post[] } model Post { id String @id @default(cuid()) authorId String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) }
Many-to-many (explicit junction table):
model Post { id String @id @default(cuid()) tags PostTag[] } model Tag { id String @id @default(cuid()) name String @unique posts PostTag[] } model PostTag { postId String tagId String post Post @relation(fields: [postId], references: [id], onDelete: Cascade) tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade) @@id([postId, tagId]) }
Enums
enum Role { ADMIN USER GUEST } model User { id String @id @default(cuid()) role Role @default(USER) }
Indexes
model Post { id String @id @default(cuid()) authorId String published Boolean @default(false) createdAt DateTime @default(now()) @@index([authorId]) @@index([published, createdAt]) }
Unique Constraints
model BookCollaborator { id String @id @default(cuid()) bookId String userId String @@unique([bookId, userId]) }
Migration Workflow
Development
# Make schema changes in prisma/schema.prisma # Create and apply migration npm run db:migrate # Prompts for migration name, e.g., "add_posts_table" # Quick iteration (no migration file, just sync) npm run db:push
Production
# Apply pending migrations (run in CI/CD or startup) npm run db:migrate:prod
Migration Best Practices
- Never edit applied migrations - They may have run in production
- Name migrations descriptively -
,add_user_avatarcreate_posts_table - Review generated SQL - Check
before deployingprisma/migrations/ - Use db:push for prototyping - Switch to migrations when schema stabilizes
- Commit migrations - They're part of your codebase
Query Patterns
Basic CRUD
import { prisma } from '@/lib/db' // Create const user = await prisma.user.create({ data: { email: 'user@example.com', name: 'John Doe', }, }) // Read one const user = await prisma.user.findUnique({ where: { email: 'user@example.com' }, }) // Read many with filters const users = await prisma.user.findMany({ where: { role: 'ADMIN', createdAt: { gte: new Date('2024-01-01') }, }, orderBy: { createdAt: 'desc' }, take: 10, }) // Update const updated = await prisma.user.update({ where: { id: userId }, data: { name: 'Jane Doe' }, }) // Delete await prisma.user.delete({ where: { id: userId }, })
With Relations
// Include related data const userWithPosts = await prisma.user.findUnique({ where: { id: userId }, include: { posts: { where: { published: true }, orderBy: { createdAt: 'desc' }, }, }, }) // Select specific fields const userEmail = await prisma.user.findUnique({ where: { id: userId }, select: { email: true, posts: { select: { title: true }, }, }, }) // Nested create const userWithPost = await prisma.user.create({ data: { email: 'author@example.com', posts: { create: { title: 'My First Post', }, }, }, include: { posts: true }, })
Transactions
// Sequential operations const [user, post] = await prisma.$transaction([ prisma.user.create({ data: { email: 'user@example.com' } }), prisma.post.create({ data: { title: 'Hello', authorId: 'temp' } }), ]) // Interactive transaction await prisma.$transaction(async (tx) => { const user = await tx.user.create({ data: { email: 'user@example.com' }, }) await tx.post.create({ data: { title: 'My Post', authorId: user.id, }, }) })
Upsert
const user = await prisma.user.upsert({ where: { email: 'user@example.com' }, update: { name: 'Updated Name' }, create: { email: 'user@example.com', name: 'New User' }, })
Railway Operations
Environment Variables
Required Railway configuration:
DATABASE_URL=file:/data/app.db
Ensure volume is mounted at
/data.
Shell Access
# Open interactive shell in Railway container railway shell # Inside container - use Prisma Studio (opens web UI) npx prisma studio # Or use sqlite3 directly sqlite3 /data/app.db .tables .schema User SELECT * FROM User LIMIT 5; .quit
Check Migration Status
railway shell npx prisma migrate status
Continuous Backup with Litestream
Litestream provides real-time SQLite replication to S3-compatible storage. Combined with Railway Buckets, this gives you continuous backups without external providers.
See
for complete setup guide.references/litestream.md
Quick Overview
- Litestream monitors SQLite WAL changes
- Streams changes to Railway Bucket every ~10 seconds
- On container restart, restores from bucket if local DB is missing
- ~10 second recovery point objective (RPO)
Minimal Setup
# 1. Create Railway Bucket railway add --service bucket # 2. Add litestream.yml to project root # 3. Update nixpacks.toml to install litestream # 4. Update railway.toml start command # 5. Add restore script for empty volumes
Troubleshooting
"Cannot find module '@prisma/client'"
Generate the client after schema changes:
npm run db:generate
"Migration failed"
Check migration status and pending migrations:
npx prisma migrate status
For stuck migrations, you may need to mark as applied or reset:
# Mark a migration as applied (use with caution) npx prisma migrate resolve --applied <migration_name> # Reset database (development only) npx prisma migrate reset
"Database is locked"
SQLite allows only one writer at a time. Solutions:
- Keep transactions short
- Avoid long-running queries during writes
- Use connection pooling sparingly (usually singleton is fine)
Production Database Issues
- Check Litestream backup status
- Restore from backup if needed
- Review
for restore proceduresreferences/litestream.md
References
- Complete Prisma setup codereferences/boilerplate.md
- Migration patterns and examplesreferences/migrations.md
- Continuous backup setup with Railway Bucketsreferences/litestream.md- Prisma Docs - Official documentation
- Railway SQLite Guide - Railway-specific patterns