A CRM to personally track various types of media you consume, such as videogames, books, movies, or TV shows.
| Layer | Technology |
|---|---|
| Frontend | React 18, TypeScript, Vite |
| Backend | Node.js, Express, tRPC |
| Database | SQLite (better-sqlite3) |
| ORM | Drizzle ORM |
| Validation | Zod (shared schemas) |
| Monorepo | pnpm workspaces |
media-crm/
├── packages/
│ ├── shared/ # Zod schemas, types, constants
│ ├── server/ # Express + tRPC API
│ │ ├── src/
│ │ │ ├── schema.ts # Drizzle table definitions
│ │ │ ├── db.ts # Database connection & queries
│ │ │ └── routers/ # tRPC procedures
│ │ └── drizzle/ # Migration files
│ └── client/ # React frontend
├── docker-compose.yml
└── Dockerfile
- Node.js 18+
- pnpm
pnpm installpnpm devThis starts both:
- Server at http://localhost:3001
- Client at http://localhost:5173 (proxies API requests to server)
pnpm build
pnpm startMigrations are managed with Drizzle Kit. The schema is defined in packages/server/src/schema.ts and derives enum values from the shared Zod schemas.
After modifying packages/server/src/schema.ts:
cd packages/server
pnpm db:generateThis creates a new SQL migration file in packages/server/drizzle/.
Migrations run automatically on server startup. To run them manually:
cd packages/server
pnpm db:migratecd packages/server
pnpm db:studioOpens Drizzle Studio for browsing and editing data.
- Edit
packages/shared/src/schemas/media.tsto add/modify enum values - Update
packages/server/src/schema.tsif adding new columns - Run
pnpm db:generatein the server package - Review the generated SQL in
drizzle/ - Restart the server (migrations apply automatically)
docker-compose up --buildAccess at http://localhost:3000
SQLite database is stored in a Docker volume (media-data) at /data/media.db.
tRPC procedures available at /trpc:
| Procedure | Type | Description |
|---|---|---|
media.list |
Query | List all media (optional type/status filter) |
media.getById |
Query | Get single item by ID |
media.create |
Mutation | Create new item |
media.update |
Mutation | Update existing item |
media.delete |
Mutation | Delete item |
Media items have the following fields:
| Field | Type | Description |
|---|---|---|
id |
number | Auto-incremented primary key |
title |
string | Required |
type |
enum | book, movie, tv_show, game, podcast |
status |
enum | want_to_consume, in_progress, completed, dropped |
rating |
number | null | 0-10 scale |
notes |
string | null | Free-form notes |
completedAt |
string | null | ISO date string |
createdAt |
string | Auto-set on creation |
updatedAt |
string | Auto-updated on changes |