4.9 KiB
Tessera
Open-source ticketing system — Request Tracker's paradigm rebuilt in modern TypeScript.
Architecture
tessera/
├── src/ # Backend: Bun + Hono + Drizzle ORM
│ ├── index.ts # Hono server entry (port 9876)
│ ├── config.ts # Zod-validated env config
│ ├── db/ # Drizzle ORM schema + migrations
│ ├── routes/ # REST API endpoints
│ ├── models/ # TypeScript types + Zod schemas
│ ├── scrip/ # Scrip engine (prepare/commit two-phase)
│ └── lifecycle/ # State machine validator
├── web/ # Frontend: Next.js 15 + shadcn/ui
│ └── src/app/ # App Router pages
├── drizzle/ # SQL migration files
└── docs/ # Architecture + design specs
Stack
Backend: Bun runtime, Hono web framework, Drizzle ORM, PostgreSQL 17, Zod validation, Handlebars templates, nodemailer
Frontend: Next.js 15 App Router, shadcn/ui (Tailwind CSS), next-themes (light/dark), React Hook Form + Zod, TanStack Table, date-fns, lucide-react icons
Fonts: Inter (variable, with cv01+ss03 OpenType features), JetBrains Mono
Running Locally
Prerequisites
- Bun (
nix-shell -p bunor install globally) - Node.js 22+ (
nix-shell -p nodejs_22) - Docker (for PostgreSQL)
- PostgreSQL container:
docker run -d --name tessera-db -e POSTGRES_USER=tessera -e POSTGRES_PASSWORD=*** -e POSTGRES_DB=tessera -p 127.0.0.1:5433:5432 postgres:17-alpine
Start backend
cd ~/projects/tessera
cp .env.example .env # Edit DATABASE_URL to point to postgres://tessera:***@127.0.0.1:5433/tessera
npm run dev:backend # Starts API on port 9876
Run migrations
npm run db:migrate
npm run db:seed # Optional demo data for UI review
npm run db:seed:reset # Reset local app data, then recreate demo data
Start frontend
cd web
npm install # Use npm, NOT bun (bun has compatibility issues with Next.js dev server)
npm run build # Production build
npm run start # Production server on 127.0.0.1:3100
Note: bun run dev (Turbopack) has WebSocket HMR issues in this environment. Use production mode only.
API Endpoints
All endpoints are served by the backend on port 9876. The frontend proxies /api/* to the backend via next.config.ts.
| Method | Path | Description |
|---|---|---|
| GET | /health | Health check |
| GET | /tickets | List tickets (?queue_id=&status=) |
| POST | /tickets | Create ticket |
| GET | /tickets/:id | Get ticket with custom fields |
| PATCH | /tickets/:id | Update ticket (validates lifecycle, runs scrips) |
| POST | /tickets/:id/preview | Dry-run scrips for status change |
| POST | /tickets/:id/comment | Add comment to ticket |
| GET | /tickets/:id/transactions | List ticket transactions |
| GET/POST | /queues | List/create queues |
| GET/POST/PATCH | /scrips | CRUD scrips |
| GET/POST | /custom-fields | List/create custom fields |
| GET/POST | /lifecycles | List/create lifecycles |
Key Design Decisions
- Ticket IDs are sequential integers (1, 2, 3...), formatted as
TKT-0001for display. No UUIDs. - Transaction-centric: Every state change creates a transaction record. The scrip engine runs on transactions.
- Two-phase scrip engine: Prepare (no side effects) then Commit (execute actions). Supports dry-run mode.
- Lifecycle state machines: Per-queue configurable status transitions with wildcard support.
- Light mode is default. Dark mode available via theme toggle (next-themes).
- No ORM for frontend: Drizzle is only on the backend. Frontend uses a typed fetch wrapper (
web/src/lib/api.ts).
Git Workflow
Repo: https://git.gjermund.xyz/gjermund/tessera
Push via HTTPS with token auth (SSH port 2222 is not configured on Gitea):
git remote set-url origin https://gjermund:TOKEN@git.gjermund.xyz/gjermund/tessera.git
Development Workflow
All code is written by OpenCode (AI coding agent). Hermes writes specs, OpenCode writes code, Gjermund reviews.
OpenCode server: opencode serve --port 4096 (Gjermund attaches with opencode --attach http://127.0.0.1:4096)
After OpenCode makes changes:
cd web && npx next build— verify zero errorsnpm run start— restart production server on 127.0.0.1:3100git push— push to origin
Common Issues
- Frontend shows skeleton/blank page: The frontend dev server (Turbopack) has WebSocket HMR issues. Use production mode (
npx next build+npx next start). - Backend not running on 9876: Restart with
bun run src/index.ts. Check port withss -tlnp | grep 9876. - Database connection refused: Docker container may be stopped.
docker start tessera-db. - Build errors after migration: Run
bun run src/db/migrate.tsto apply new migrations.