Context.
It started as a class project in my web development diploma. I then rewrote it from scratch on my own to learn what I really wanted: Spring Boot on the backend, React on the frontend, real authentication, database constraints and tests against a real PostgreSQL.
It is the project that taught me most about roles and permissions: which roles exist and what each of them can do.
I later gave it its own design with scroll-linked motion, tidied the frontend and made it accessible. The target is WCAG 2.2 AA and axe checks it in CI.
The problem.
Admins, managers, receptionists and guests share one platform, but each of them must only see and touch their own part.
What I decided, and why.
Four profiles, three of them staff
- what
- Admin, manager and receptionist in the ERP, and guest in their own area. The rules live in the backend; the frontend only shows what each role can use.
- why
- If the rule only lived in the interface, calling the API directly would bypass it.
Rotating refresh tokens
- what
- The access token lives in memory and the refresh token changes on every use. If one is used twice, the whole token family is revoked.
- why
- My first version issued a long-lived token and called it done. Reading OWASP properly showed me that rotation with reuse detection was cheap and far safer.
Let the database block overlapping bookings
- what
- The service rejects overlapping bookings and PostgreSQL also has an EXCLUDE USING gist constraint as a safety net.
- why
- My first two hand-written checks were wrong: one on the checkout day, one on time zones. The third time I let the database do it, and the test is one line.
Accessibility checked on every PR
- what
- A CI job walks 18 screens at 390 and 1440 px with axe and measures horizontal overflow. It fails on serious or critical violations.
- why
- A manual review at the end gets forgotten. With the check on every PR, a regression shows up the same day.
Demo data that does not expire
- what
- An optional job refreshes the demo bookings, invoices and tasks so the dashboard does not show 0 € or 0 bookings. Demo guests have no account and their bookings send no emails. It is off by default and is switched on with an environment variable.
- why
- An empty dashboard does not show what the ERP does, and hand-written dates expire.


Result.
- 31 backend tests, including Testcontainers tests against PostgreSQL.
- axe in CI: 18 screens at 390 and 1440 px, with no serious or critical violations.
- Free deployment with a public demo.
What I learned.
- Refresh-token rotation is not optional.
- A database constraint is worth more than three hand-written checks.
- TanStack Query changed how I think about frontend state.
What I'd do differently
- I would start with bigger components and split them only when something genuinely repeats.
