Kasetsart University Sriracha Campus
KU DMS — approvals, budgets and document generation for a university campus
An internal system that routes documents for approval, tracks project budgets against the documents that spend them, and lets staff design the forms and documents themselves. Built on Bun, ElysiaJS and SvelteKit, hosted entirely on the university's own infrastructure.
- Web application
- REST API
- Document & PDF pipeline
- Role-based access control
- Self-hosted deployment
- Build span
- 3 months, ongoing
- Backend modules
- 19
- Database tables
- 35
- Migrations
- 32
- Svelte components
- 242
- Commits
- 722
The problem
A campus runs on paper that has to be signed in the right order by the right people. A request is drafted, given a document number, printed, carried to a desk, signed, carried to the next desk, and filed. If it stalls, nobody can say where. If it is lost, it is redrafted.
The money moves separately. A project has a budget, documents spend against that budget, and the running total lives in a spreadsheet that somebody updates afterwards. The two records drift, and the drift is only discovered when a report is due.
Underneath both problems is a slower one: every new kind of request needs a new form, and a new form needs a developer. Staff who know exactly what the form should ask cannot make it themselves, so the queue for small changes never empties.
What we built
Approval routing. A workflow template defines the steps a document type must pass through and who acts at each one. Submitting a document creates an instance of that workflow, and every approval, rejection and return is recorded as an action against a step. The current position of any document is a fact in the database, not a phone call.
Budgets against documents. Expenditures are attached to the documents that authorise them, so a project’s total is derived rather than maintained. Budget and expenditure reports read from the same records the approvals ran through, and export to Excel for the reporting the university already does.
Form and document builders. Staff compose forms by dragging fields, and design the document those fields print into. Templates are versioned, so a form can change without invalidating the documents already issued under the previous version. This is the part that removes us from the loop: a new kind of request no longer needs a release.
Around those three sit the unglamorous pieces that decide whether a document system is actually usable — sequential document numbers issued without collisions, QR codes on issued documents, stored digital signatures, retained PDF versions of every generated document, notification and email logs, and an audit log covering who changed what.
How it is built
One type definition, from database to screen. The API is ElysiaJS, and the SvelteKit front end
consumes Elysia’s own App type through Eden Treaty. There is no code generation step and no
generated client to keep in sync: if a response shape changes, the screens that read it stop
type-checking. Drizzle covers the same ground below the API, so a column rename surfaces at compile
time rather than in production.
PDFs are rendered on the server. A long-lived Playwright worker drives headless Chromium to produce every PDF. Rendering in the browser instead would have been less work, and would have made the output depend on the user’s browser, fonts and zoom level — unacceptable for a document that is archived and carries a signature. The backend image is built on Playwright’s own base image so Chromium is present rather than downloaded at deploy time.
Permissions are data, not conditionals. Roles, permissions and their assignments are tables, scoped by organisation membership, with the resulting matrix written down. Departments can be added and authority reassigned without a code change, which is the only version of access control that survives a reorganisation.
The university holds every key. PostgreSQL, Redis, secret storage, CI and nginx all run on infrastructure the client controls. There is no account they need to keep paying us for, and no service they cannot move. We hand over systems we do not need to stay inside.
Built with
- Bun
- ElysiaJS
- Drizzle ORM
- PostgreSQL
- Redis
- SvelteKit
- Svelte 5
- Tailwind CSS
- better-auth
- Playwright
- Docker
- nginx