An example ToDo Application built with Next.js + Drizzle ORM + PostgreSQL
This is a an example ToDo application built with Next.js 15.4 and Drizzle ORM. It leverages Server Actions for server-side operations and PostgreSQL for data persistence.
- Framework: Next.js 15.4.7 (App Router)
- Language: TypeScript
- ORM: Drizzle ORM
- Database: PostgreSQL
- Styling: Tailwind CSS v4
- Package Manager: pnpm
- ✅ Add new todos
- ✅ Toggle todo completion status
- ✅ Edit todo text (click to edit)
- ✅ Delete todos
- ✅ Display remaining task count
- Node.js 22.18 or higher
- pnpm
- PostgreSQL server
git clone https://github.com/buun-ch/sample-web-app
cd sample-web-apppnpm installCreate a .env.local file and set your PostgreSQL connection string:
DATABASE_URL=postgresql://[user]:[password]@[host]:[port]/[database]Example:
DATABASE_URL=postgresql://todo:todopass@localhost:5432/todoPush the schema to your database:
pnpm db:pushOr generate and run migrations:
pnpm db:generate
pnpm db:migratepnpm devOpen http://localhost:3000 to view the application.
Tilt provides a powerful development environment with hot reloading and automatic rebuilds.
- Tilt installed
- Local Kubernetes cluster (Docker Desktop, minikube, kind, k3d, or Rancher Desktop)
- kubectl configured to access your cluster
Start Tilt by running:
tilt up -- --registry ghcr.io/your-usernameIf you do not specify a registry, Tilt will use the default registry localhost:30500.
If the registry requires authentication, create a secret in your Kubernetes cluster:
kubectl create secret docker-registry regcred \
--docker-server=ghcr.io \
--docker-username=<your-username> \
--docker-password=<your-github-token> \
--docker-email=<your-email>(This is an example for GitHub Container Registry; adjust for your registry.)
Then create a values-dev.yaml file in the root directory with the following content:
imagePullSecrets:
- name: regcredRun Tilt with the custom values file:
tilt up -- --registry ghcr.io/your-username --extra-values-file values-dev.yamlYou can also specify the port forwarding option to access the app locally:
tilt up -- --registry ghcr.io/your-username --port-forwardThen open the URL: http://localhost:13000.
If you use Telepresence, you can run:
tilt up -- --registry ghcr.io/your-usernameThen enable Telepresence connectivity:
telepresence connectand open the URL: http://sample-web-app-tilt.sample-web-app.svc:3000.
Edit values-dev.yaml to customize your development environment:
- Environment variables
- Resource limits
- Database connections
To deploy PostgreSQL alongside your app, uncomment the PostgreSQL section in Tiltfile.
Press Ctrl+C in the terminal or run:
tilt down# Development server (with Turbopack)
pnpm dev
# Production build
pnpm build
# Production server
pnpm start
# Lint code
pnpm lint
# Drizzle commands
pnpm db:generate # Generate migration files
pnpm db:migrate # Run migrations
pnpm db:push # Push schema directly to database
pnpm db:studio # Open Drizzle Studio (GUI for database)sample-web-app/
├── app/ # Next.js App Router
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page (ToDo app)
│ └── globals.css # Global styles
├── src/
│ ├── actions/ # Server Actions
│ │ └── todoActions.ts # ToDo CRUD operations
│ ├── components/ # React components
│ │ ├── TodoList.tsx # ToDo list container
│ │ ├── TodoItem.tsx # Individual todo item
│ │ └── AddTodo.tsx # Add todo form
│ └── db/ # Database related
│ ├── schema.ts # Drizzle schema definition
│ └── index.ts # Database connection
├── drizzle/ # Migration files
├── drizzle.config.ts # Drizzle configuration
└── .env.local # Environment variables
todos {
id: serial (primary key)
text: text (not null)
done: boolean (default: false)
createdAt: timestamp (default: now)
updatedAt: timestamp (default: now)
}Build the production image:
docker build -t sample-web-app:latest .Run the production container:
docker run -d \
--name sample-web-app \
-p 3000:3000 \
-e DATABASE_URL="postgresql://todo:todopass@host.docker.internal:5432/todo" \
sample-web-app:latestBuild the development image:
docker build -f Dockerfile.dev -t sample-web-app:dev .Run the development container with hot reload:
docker run -d \
--name sample-web-app-dev \
-p 3000:3000 \
-v $(pwd):/app \
-v /app/node_modules \
-v /app/.next \
-e DATABASE_URL="postgresql://todo:todopass@host.docker.internal:5432/todo" \
sample-web-app:devWhen running containers, set the DATABASE_URL environment variable.
Build for production with pnpm build and start with pnpm start.
- Build the production image:
docker build -t sample-web-app:latest . - Push to your container registry
- Deploy to your container platform (Kubernetes, ECS, Cloud Run, etc.)
Here is an example for pushing images to GHCR (GitHub Container Registry):
docker build -t ghcr.io/yourusername/sample-web-app:latest .
docker push ghcr.io/yourusername/sample-web-app:latest
docker build -f Dockerfile.dev -t ghcr.io/yourusername/sample-web-app:dev .
docker push ghcr.io/yourusername/sample-web-app:devThen you can deploy it using your preferred container orchestration platform.
A Helm chart is included in charts/sample-web-app for Kubernetes deployment.
- Kubernetes cluster
- Helm 3.x installed
- kubectl configured
- Create a namespace (optional):
kubectl create namespace sample-web-app- Create image pull secret if using private registry:
kubectl create secret docker-registry regcred \
--docker-server=ghcr.io \
--docker-username=<your-username> \
--docker-password=<your-github-token> \
--docker-email=<your-email> \
-n sample-web-app- Create a
values.yamlfile with your configuration:
# Example values.yaml
image:
imageRegistry: ghcr.io/yourusername
repository: sample-web-app
tag: latest
pullPolicy: IfNotPresent
imagePullSecrets:
- name: regcred
env:
- name: DATABASE_URL
value: "postgresql://todo:todopass@postgres-cluster-rw.postgres:5432/todo"
ingress:
enabled: true
ingressClassName: traefik
hosts:
- host: sample.yourdomain.com
paths:
- path: /
pathType: ImplementationSpecific
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
tls:
- hosts:
- sample.yourdomain.com- Deploy using Helm:
helm upgrade --install sample-web-app ./charts/sample-web-app \
-n sample-web-app --wait -f values.yamlYou can also use ConfigMaps or Secrets for environment variables:
# Using Secret for DATABASE_URL
envFrom:
- secretRef:
name: app-secretsCreate the secret separately:
kubectl create secret generic app-secrets \
--from-literal=DATABASE_URL="postgresql://todo:todopass@postgres:5432/todo" \
-n sample-web-app# Check pods
kubectl get pods -n sample-web-app
# Check service
kubectl get svc -n sample-web-app
# View logs
kubectl logs -n sample-web-app deployment/sample-web-apphelm uninstall sample-web-app -n sample-web-appMIT