Skip to main content

Building Database Apps

Cubby automatically provisions Postgres databases for apps that use Prisma. This guide covers everything you need to know about database-backed apps.

Automatic Database Provisioning

When Cubby detects Prisma in your project (via prisma/schema.prisma), it automatically:
  1. Provisions a Postgres 17 container alongside your app
  2. Creates a named volume for data persistence
  3. Injects DATABASE_URL into your environment
  4. Runs migrations via prisma db push on each deploy
You don’t need to configure anything - it just works.

Prisma Setup

The cubby init template includes Prisma pre-configured:
my-app/
├── prisma/
│   └── schema.prisma     # Define your models here
├── lib/
│   └── db.ts             # Prisma client singleton
└── ...

The Prisma Client Singleton

lib/db.ts provides a singleton Prisma client that prevents connection exhaustion during development:
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}
Import it in your code:
import { prisma } from '@/lib/db'

Defining Models

Edit prisma/schema.prisma to define your data models:
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String   @id @default(cuid())
  name      String
  email     String   @unique
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
}

Common Field Types

Prisma TypePostgreSQL TypeExample
StringTEXTname String
IntINTEGERcount Int
FloatDOUBLE PRECISIONprice Float
BooleanBOOLEANactive Boolean
DateTimeTIMESTAMPcreatedAt DateTime
JsonJSONBmetadata Json

Default Values

model Example {
  id        String   @id @default(cuid())    // Auto-generated ID
  count     Int      @default(0)              // Default number
  active    Boolean  @default(true)           // Default boolean
  createdAt DateTime @default(now())          // Auto timestamp
}

Relations

// One-to-many: User has many Posts
model User {
  id    String @id @default(cuid())
  posts Post[]
}

model Post {
  id       String @id @default(cuid())
  author   User   @relation(fields: [authorId], references: [id])
  authorId String
}

// Many-to-many: Posts have many Tags
model Post {
  id   String @id @default(cuid())
  tags Tag[]
}

model Tag {
  id    String @id @default(cuid())
  posts Post[]
}

CRUD Operations

Create

// Create one
const user = await prisma.user.create({
  data: {
    name: 'Alice',
    email: '[email protected]'
  }
})

// Create with relation
const post = await prisma.post.create({
  data: {
    title: 'Hello World',
    author: { connect: { id: userId } }
  }
})

// Create many
await prisma.user.createMany({
  data: [
    { name: 'Bob', email: '[email protected]' },
    { name: 'Charlie', email: '[email protected]' }
  ]
})

Read

// Find one by ID
const user = await prisma.user.findUnique({
  where: { id: userId }
})

// Find one by unique field
const user = await prisma.user.findUnique({
  where: { email: '[email protected]' }
})

// Find many with filter
const posts = await prisma.post.findMany({
  where: { published: true },
  orderBy: { createdAt: 'desc' }
})

// Include relations
const userWithPosts = await prisma.user.findUnique({
  where: { id: userId },
  include: { posts: true }
})

Update

// Update one
const updated = await prisma.user.update({
  where: { id: userId },
  data: { name: 'Alice Smith' }
})

// Update many
await prisma.post.updateMany({
  where: { authorId: userId },
  data: { published: false }
})

// Upsert (update or create)
const user = await prisma.user.upsert({
  where: { email: '[email protected]' },
  update: { name: 'Alice Updated' },
  create: { name: 'Alice', email: '[email protected]' }
})

Delete

// Delete one
await prisma.user.delete({
  where: { id: userId }
})

// Delete many
await prisma.post.deleteMany({
  where: { authorId: userId }
})

Local Development

cubby dev handles everything:
  1. Starts a local Postgres container
  2. Syncs your schema with prisma db push
  3. Makes the database available at localhost:5432
cubby dev
Output:
  App:      http://localhost:3000
  Database: postgresql://cubby:cubby@localhost:5432/cubby

Resetting Local Data

To start with a fresh database:
cubby dev --reset
This deletes the Docker volume and creates a new database.

Manual Schema Sync

If you need to sync manually:
npx prisma db push
And regenerate the client:
npx prisma generate

Deployment

When you run cubby deploy:
  1. Cubby detects prisma/schema.prisma
  2. Provisions (or reuses) a Postgres container
  3. Runs prisma db push to sync the schema
  4. Injects DATABASE_URL into your app
Data persists across deploys in a named Docker volume.

User-Scoped Data

Most Cubby apps store user-specific data. Use the userId from headers:
// Store userId with your data
model Todo {
  id     String @id @default(cuid())
  title  String
  userId String // Store the owner
}

// Always filter by userId
const todos = await prisma.todo.findMany({
  where: { userId: h.get('X-Cubby-User-Id')! }
})
See the Authentication guide for details.

Best Practices

await prisma.$transaction([
  prisma.post.deleteMany({ where: { authorId: userId } }),
  prisma.user.delete({ where: { id: userId } })
])

Index Frequently Queried Fields

model Todo {
  id     String @id @default(cuid())
  userId String

  @@index([userId]) // Speeds up queries filtered by userId
}

Handle Not Found

const todo = await prisma.todo.findUnique({
  where: { id: todoId }
})

if (!todo) {
  return NextResponse.json({ error: 'Not found' }, { status: 404 })
}