Case study · Full-Stack ITSM
DeskFlow
Building the parts of a service desk that are easy to get subtly wrong
DeskFlow is a small ServiceNow-style helpdesk: tickets with SLA timers, a hardware inventory, a knowledge base and an optional AI assistant. I built it on my own, from data model to deployment, to learn what makes this kind of software trustworthy.
- Role
- Solo project: design, backend, frontend, tests and deployment
- When
- 2026
- Stack
- Angular
- Angular Material
- TypeScript
- FastAPI
- SQLAlchemy
- PostgreSQL
- Redis
- Alembic
- Claude API
- Docker
- nginx
- Railway
- GitHub Actions

01.The problem
A helpdesk is only useful if people trust what it tells them. An agent needs to know which tickets are about to breach their SLA. An admin needs to know who changed what, and be sure nobody quietly changed it back. And an AI suggestion is worse than nothing if it invents a fix.
Those three things — time, history and AI answers — are where I focused. Tickets, inventory, roles and search had to be solid too, but these are the parts that are easy to build so they look right and are subtly wrong.
02.Key decisions
01One module decides every SLA state
SLA status shows up in four places: the ticket page, the dashboard tiles, the ticket-list filter and a background worker that flags tickets at risk or breached. If each place worked it out for itself, sooner or later they would disagree — a ticket “at risk” on the dashboard but missing from the filter.
So all four call one module. Each priority has a response and a resolution target, stored as deadlines on the ticket. Moving a ticket to Pending pauses both clocks, and when it leaves Pending the deadlines move back by the time spent waiting. A ticket is at risk once 75% of its window has passed.
Trade-off: The clocks run on wall-clock time, not business hours. That kept the rules simple enough to test thoroughly, but it's the biggest gap compared with a real service desk.
02An audit log nobody can rewrite
Every change to a ticket, asset or article records who made it and the values before and after, in the same database transaction as the change itself. A request that fails leaves no trace, and one that succeeds can't skip the log. Entries are only ever inserted, and they outlive the records they describe: an entry about a deleted record is shown as “deleted” instead of a broken link.
The public demo resets every night, so visitors always find a realistic desk. The first version of that reset cleared the audit log along with the tickets. That made the demo tidy and the log meaningless, so I changed it: the reset only adds to the log, recording what it removed and what it created, and ids are never reused, so an old entry can never come to describe a new record.

The admin audit log: every change with who, what and before → after, filterable and exportable. Entries can't be edited or deleted. 03An AI assistant that can only repeat the knowledge base
The assistant suggests troubleshooting steps for a ticket, but the model is only given up to four published knowledge-base articles, found with the same full-text search as the rest of the app. If nothing matches, the model isn't called at all.
It has to say which articles it used. Ids it made up are dropped, and an answer that cites nothing it was given is thrown away and shown as “not covered”. Ticket text is passed as data with an instruction not to follow anything in it, the answer goes through the same sanitiser as articles, and it never takes an action: it's advice for a person to check. It only runs when an agent asks, it's rate-limited, and every run is audited.
Trade-off: Grounding it this strictly means it often answers “not covered”. For a helpdesk that's the right way to fail: a wrong fix costs more than no suggestion.

The assistant on a ticket: suggested steps, and the published article they came from. 04Search in Postgres instead of a search service
The knowledge base uses PostgreSQL full-text search: a generated tsvector column with a GIN index, titles ranked above body text, word forms matched (“printers” finds “printer”), and support for “exact phrases” and -exclusions. Queries go through websearch_to_tsquery, so odd input never causes an error.
Matches come back highlighted with private delimiter characters rather than HTML, so the browser never has to trust markup coming from the database.
Trade-off: One less service to run and keep in sync, at the cost of things a dedicated search engine gives you, like typo tolerance.
05Tests against a real database
Backend tests run against PostgreSQL, not SQLite, each inside a transaction that's rolled back afterwards. That catches what SQLite would hide: constraints, sequences and full-text search.
It also taught me a trap. Because the tests share one rolled-back session, a route that forgot to commit its changes still passed. Routes that write now have tests that check the commit explicitly.
03.Results
- 259 backend tests on PostgreSQL and 163 frontend tests, run in CI on every push
- Deployed on Railway as five services: Angular behind nginx, the FastAPI API, a background worker, PostgreSQL and Redis
- A live demo with three roles that resets itself every night and keeps its history
- Access enforced on the server: requesters only see their own tickets, and someone else's ticket returns 404, not 403, so ids can't be probed
04.What I'd do differently
Business-hours SLAs from the start
Real desks measure SLAs in working hours, with holidays. I'd model a calendar per team from day one; adding it now would touch every deadline calculation.
Measure the assistant's answers
I tested the request format against the real SDK and the logic with a fake model, but never measured the quality of its answers. Next I'd collect a set of real tickets with the articles that should answer them and score the assistant against it.
Notifications before polish
Email notifications and attachments are still on the roadmap. They're what a real team would need first, so I'd build them before some of the finer touches.