Documentation Index
Fetch the complete documentation index at: https://docs.cubby.pro/llms.txt
Use this file to discover all available pages before exploring further.
Building Your First App
Let’s build a complete todo list app from scratch and deploy it to Cubby.
What We’ll Build
A simple todo app with:
- Add new todos
- Mark todos complete
- Delete todos
- Data persisted in Cubby’s default SQLite database
- User-specific data (each user sees only their todos)
Time: ~15 minutes
Prerequisites
- Node.js 18 or later
- Docker Desktop (for local development)
- Cubby account and CLI installed
npm install -g cubbypro
cubby login
Step 1: Create the Project
cubby init my-todo-app
cd my-todo-app
npm install
This creates a Next.js + Prisma project configured for Cubby’s default SQLite path.
Step 2: Define the Data Model
Edit prisma/schema.prisma to add a Todo model:
model Todo {
id String @id @default(cuid())
title String
completed Boolean @default(false)
userId String // Owner's Cubby user ID
createdAt DateTime @default(now())
}
// Keep the Example model or delete it
model Example {
id String @id @default(cuid())
name String
createdAt DateTime @default(now())
}
Step 3: Create the API Routes
List and Create Todos
Create app/api/todos/route.ts:
import { headers } from 'next/headers'
import { prisma } from '@/lib/db'
import { NextRequest, NextResponse } from 'next/server'
// GET /api/todos - List user's todos
export async function GET() {
const h = await headers()
const userId = h.get('X-Cubby-User-Id')!
const todos = await prisma.todo.findMany({
where: { userId },
orderBy: { createdAt: 'desc' }
})
return NextResponse.json(todos)
}
// POST /api/todos - Create a todo
export async function POST(request: NextRequest) {
const h = await headers()
const userId = h.get('X-Cubby-User-Id')!
const { title } = await request.json()
if (!title || typeof title !== 'string') {
return NextResponse.json({ error: 'Title required' }, { status: 400 })
}
const todo = await prisma.todo.create({
data: { title, userId }
})
return NextResponse.json(todo, { status: 201 })
}
Update and Delete Todos
Create app/api/todos/[id]/route.ts:
import { headers } from 'next/headers'
import { prisma } from '@/lib/db'
import { NextRequest, NextResponse } from 'next/server'
type Params = { params: Promise<{ id: string }> }
// PUT /api/todos/[id] - Update a todo
export async function PUT(request: NextRequest, { params }: Params) {
const h = await headers()
const userId = h.get('X-Cubby-User-Id')!
const { id } = await params
const { completed } = await request.json()
// Verify ownership
const existing = await prisma.todo.findFirst({
where: { id, userId }
})
if (!existing) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
const todo = await prisma.todo.update({
where: { id },
data: { completed }
})
return NextResponse.json(todo)
}
// DELETE /api/todos/[id] - Delete a todo
export async function DELETE(request: NextRequest, { params }: Params) {
const h = await headers()
const userId = h.get('X-Cubby-User-Id')!
const { id } = await params
// Verify ownership
const existing = await prisma.todo.findFirst({
where: { id, userId }
})
if (!existing) {
return NextResponse.json({ error: 'Not found' }, { status: 404 })
}
await prisma.todo.delete({ where: { id } })
return NextResponse.json({ deleted: true })
}
Step 4: Build the UI
Replace app/page.tsx with:
'use client'
import { useState, useEffect } from 'react'
export const dynamic = 'force-dynamic'
interface Todo {
id: string
title: string
completed: boolean
createdAt: string
}
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([])
const [newTodo, setNewTodo] = useState('')
// Load todos on mount
useEffect(() => {
fetch('/api/todos')
.then(res => res.json())
.then(setTodos)
}, [])
// Add a new todo
async function addTodo(e: React.FormEvent) {
e.preventDefault()
if (!newTodo.trim()) return
const res = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTodo })
})
const todo = await res.json()
setTodos([todo, ...todos])
setNewTodo('')
}
// Toggle completion
async function toggleTodo(id: string, completed: boolean) {
await fetch(`/api/todos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ completed: !completed })
})
setTodos(todos.map(t =>
t.id === id ? { ...t, completed: !completed } : t
))
}
// Delete a todo
async function deleteTodo(id: string) {
await fetch(`/api/todos/${id}`, { method: 'DELETE' })
setTodos(todos.filter(t => t.id !== id))
}
return (
<main style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
<h1>My Todos</h1>
<form onSubmit={addTodo} style={{ marginBottom: 20 }}>
<input
value={newTodo}
onChange={e => setNewTodo(e.target.value)}
placeholder="What needs to be done?"
style={{ padding: 10, width: '70%', marginRight: 10 }}
/>
<button type="submit" style={{ padding: '10px 20px' }}>
Add
</button>
</form>
<ul style={{ listStyle: 'none', padding: 0 }}>
{todos.map(todo => (
<li
key={todo.id}
style={{
display: 'flex',
alignItems: 'center',
padding: 10,
borderBottom: '1px solid #eee'
}}
>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id, todo.completed)}
style={{ marginRight: 10 }}
/>
<span style={{
flex: 1,
textDecoration: todo.completed ? 'line-through' : 'none',
color: todo.completed ? '#999' : 'inherit'
}}>
{todo.title}
</span>
<button
onClick={() => deleteTodo(todo.id)}
style={{ color: 'red', border: 'none', cursor: 'pointer' }}
>
Delete
</button>
</li>
))}
</ul>
{todos.length === 0 && (
<p style={{ color: '#999', textAlign: 'center' }}>
No todos yet. Add one above!
</p>
)}
</main>
)
}
Step 5: Test Locally
Start the development environment:
Open http://localhost:3000 and test:
- Add a few todos
- Mark some complete
- Delete one
Data persists between page refreshes because it’s stored in the local SQLite database.
Step 6: Deploy to Production
The CLI will:
- Package your code
- Run platform checks
- Build a Docker image
- Inject the production
DATABASE_URL
- Start your container and route traffic to it
When complete, you’ll see:
Your app is live at:
https://my-todo-app--yourname.cubby.pro
Step 7: Test in Production
Visit your app URL. You’ll be prompted to log in with your Cubby account. After login:
- Add some todos
- Refresh the page - data persists
- Share the app with a friend (they’ll see their own todos, not yours)
What’s Next?
-
Add a secret: Try integrating an API like OpenAI
cubby secrets set OPENAI_API_KEY --env prod
cubby deploy
-
Share your app: From the dashboard, share with other Cubby users
-
Add more features: Due dates, categories, search
Troubleshooting
”Build failed”
Run cubby check to see what’s wrong:
“Not logged in”
Database Errors
Reset your local database and try again: