A full-stack, multi-tenant ticket management platform for issue tracking, role-based workflows, organization management, and automated ticket distribution.
TicketFlow is built for small teams and organizations that need a focused, reliable ticketing experience — without the overhead of a bloated project-management suite. It was built as a 2-month deep dive into scalable system design after finishing an internship: the goal was not speed of delivery, but deliberately exploring system design patterns, edge cases, fault tolerance, and iterative architecture refinement.
- Features
- Tech Stack
- Architecture Overview
- Key Design Decisions
- Trade-offs
- Screenshots
- Getting Started
- Documentation
- Challenges & Solutions
- Performance Optimizations
- Key Learnings
- Future Improvements
- License
- Authentication & Authorization — session-based auth with role-based access control (Admin, Agent, User)
- Multi-Tenant Organizations — each user can create their own organization and join others as a member, with strict tenant data isolation
- Ticket Lifecycle Management — Open → In Progress → Resolved → Closed, with transition history and activity logging
- Ticket Escalation — escalation workflows with optimistic locking to handle concurrent updates
- Comment System — threaded discussion on tickets
- Inbound Email-to-Ticket Conversion — turn incoming support emails into tickets automatically
- Automated Ticket Assignment — load-balanced routing to the agent with the fewest active tickets
- Agent Groups & Queue Management — organize agents into groups and route tickets through queues
- Queue-Based Email Processing — background workers handle email ingestion without blocking the main app
- Structured API — validated requests/responses with consistent error handling
| Layer | Technologies |
|---|---|
| Frontend | React, TypeScript, Tailwind CSS, shadcn/ui, React Query, Zustand |
| Backend | Node.js, Express, TypeScript, Zod, Prisma, BullMQ, Redis, PostgreSQL |
| Deployment & DevOps | Docker, DigitalOcean, CI/CD pipeline |
Architecture style: modular, layered structure with clear separation of concerns between controllers, services, and the database layer.
The system follows a layered architecture:
- Controllers handle request/response concerns
- Services contain business logic
- Prisma manages database access
This separation keeps routes clean, business logic testable, and the codebase easier to scale and maintain.
Frontend data flow:
React UI → React Query → Axios API Client → Backend API
Backend request flow:
Client Request
→ Authentication Middleware
→ Tenant Resolution Middleware
→ Permissions / RBAC Middleware
→ API Controller
→ Service Layer
→ Database Layer
→ PostgreSQL
Inbound email processing flow:
Email Provider (Resend / any provider)
→ Webhook Endpoint
→ Provider Verification
→ Email Parsing
→ Ticket Creation
→ Assign to Default Group
→ Queue Routing
→ Agent Assignment
→ Load Balancing (least active tickets)
→ Ticket appears on Dashboard
When an email arrives, the provider fires a webhook, which is verified against a trusted source before the email body is parsed into a ticket. The ticket is assigned to a default group, routed through the correct queue, and automatically handed to the agent with the fewest active tickets — appearing on that agent's dashboard immediately.
Full write-up: docs/architecture.md
- Service Layer Abstraction — business logic is separated from controllers so routes stay thin, reusable, and easy to test.
- Prisma ORM — chosen for type safety and faster iteration, cutting down on runtime query errors.
- Role-Based Authorization — middleware-based role checks enforce access control consistently across every endpoint.
- Organization-Based Multi-Tenancy — every user can create their own organization and join others as a member; data is isolated per tenant at the database level.
Deliberate scope decisions made to keep the system focused and shippable:
- No microservices — kept deployment simple with a modular monolith instead
- No real-time webhook notifications (yet) — see Future Improvements
- Limited caching to reduce system complexity
- Prioritized backend robustness over UI polish
- Manual authentication (access + refresh tokens) rather than a third-party auth provider (later replaced — see Architecture docs)
- Email handling is integrated inside the app rather than fully externalized
Spin up the entire stack (frontend, backend, and database) in containers.
# 1. Configure environment
cp .env.example .env
# 2. Launch containers
docker compose up -dThis initializes three orchestrated containers. Once healthy, the app is available at http://localhost (port 80).
Use this if you want active code changes with Hot Module Replacement (HMR).
Backend setup
# 1. Create backend/.env from backend/.env.example
# 2. Install dependencies (from root or /backend)
pnpm install
# 3. Ensure a local PostgreSQL server is running
# 4. Generate the Prisma client and sync the schema
pnpm run generate
pnpm run migrate
# 5. Create the restricted runtime DB role (enables Row Level Security)
pnpm run db:setupRun everything
From the root directory:
pnpm run devDefault port mapping
| Service | Port |
|---|---|
| Backend | 3000 (configurable via .env) |
| Frontend | 5173 (Vite dev server) |
| PostgreSQL | 5432 |
Deeper reference docs live in /docs so this README stays scannable:
- Architecture — layered design, request/email flows, and how row-level tenant isolation works
- API Reference — endpoints, auth, and error handling conventions
- Database Design — schema, multi-tenancy model, and indexing strategy
1. Preventing cross-tenant data leaks in a shared schema
Running multi-tenant data in a shared database schema makes accidental data leaks an easy mistake. The fix was to split database access into two connection paths — DIRECT_URL for internal migrations and DATABASE_URL for runtime queries — combined with Postgres Row Level Security, so a query that forgets to filter by organizationId fails loudly instead of leaking data. Full write-up: Row Level Security blog.
2. Linking inbound emails to the correct organization Accepting tickets via provider webhooks (Resend, Mailgun, etc.) raises the question of which organization an incoming email belongs to. TicketFlow solves this with a per-organization email provider system: users link a supported provider, store encrypted API keys, and configure a webhook URL. Incoming emails are matched against the verified domain linked to that organization before a ticket is created.
More detail in docs/architecture.md.
- Lazy loading at the data-fetching layer to cut initial load time
- Code splitting via React
lazy+Suspensefor smaller initial bundles - React Query caching — disabled unnecessary refetch-on-focus, added interval-based refetching (WebSockets planned as a future replacement for polling)
- Database indexing on frequently queried tables
- Data prefetching for smoother perceived performance
- Background email processing offloaded to workers so it never blocks the main app
- Optimistic locking on ticket escalation to safely allow concurrent escalation attempts (conflict-handling UX is still being refined)
- Unit tests
- Integration tests
- Managing a scalable monorepo and inter-package dependency injection
- Multi-tenant data modeling and Row Level Security for enterprise SaaS
- Handling race conditions in state-heavy applications and stabilizing CI/CD for consistent builds
- Designing secure, performant, well-validated APIs
- OAuth Integration — Google/GitHub sign-in
- Performance Scaling — advanced caching layers for lookup APIs
- UI/UX — full dark mode and expanded accessibility
- Mobile Experience — React Native app for basic status tracking
- Real-time notifications — replace polling with WebSockets
This project is licensed under the MIT License.


