Skip to content
Ali Hassan — home

Work

Expense tracker

An expense panel that records spend per currency and shows a total and category breakdown for every currency in use.

Role
Full-stack engineer
Status
Built
  • Next.js App Router
  • PostgreSQL
  • Tailwind CSS
Per-currency analytics totals, the expense list and categories in an expense panel that never converts currencies.

The problem

Context
An internal panel for recording company expenses in any of twelve currencies, with categories, receipts and an analytics page that shows the total and a category breakdown for every currency in use. Admins manage users, categories and currencies; editors record expenses.
Constraints
There is no row-level security in the database, so every server action has to check permissions itself. Receipts are private financial documents and must never become public files. Amounts have to stay exact.
What was at stake
Adding amounts in different currencies together, or converting them at a guessed rate, produces totals nobody can reconcile. One missed permission check would let anyone read or change company spending, and a careless file path would leak receipts.

What I built

Money handling

  • Totals kept per currency, in SQL

    Two SQL functions do the analytics: one totals spend per currency, the other breaks a single currency down by category. No figure is ever added across currencies, so every number on the page reconciles.

  • Exact amounts with guard rails

    Amounts are fixed-precision decimals that must be positive, with at most two decimal places and no future dates, checked both in the form and in the database. Currencies come from a reference table rather than free text.

  • Nothing in use can be deleted

    A currency, category or user still referenced by a recorded expense cannot be removed; the database refuses and the panel explains why, so history never loses its labels.

Access control, checked twice

  • Edge redirect plus a check in every action

    An edge check keeps signed-out users out and non-admins away from admin pages. Every server action then re-reads the user's role from the database before acting, so a stale or tampered session cannot raise anyone's permissions.

  • Signed sessions and safe sign-in

    Sessions are signed tokens in HTTP-only cookies, passwords are hashed with bcrypt, and the post-login redirect only accepts paths on the site. The first admin is created from the command line, not from a public page.

Private receipts

  • Receipts outside the public folder

    Receipts are stored outside anything the web server publishes and served by one route that requires a session, confirms the file belongs to a recorded expense, and tells browsers never to cache it.

  • Uploads checked and paths contained

    Only images up to 5 MB and PDFs up to 10 MB are accepted, each saved under a random name per expense. Every path is resolved against the storage root so it cannot escape it, and a failed save removes the uploaded file.

Data model

  • Soft deletes with a full audit trail

    Expenses record who added, changed and deleted them and when; deleting hides a row instead of erasing it, and a partial index keeps the active list fast.

  • Parameterised SQL in one place

    All queries go through one server-only connection pool with placeholders for every value, including the dynamically built filters, so user input is never stitched into SQL.

Tell me what you’re building and where it’s stuck.

I’ll tell you the cleanest path forward, including if it’s “don’t build that.”

Or write tocontact@alihassan.dev

Ali Hassan in a dark winter jacket, looking off to one side, standing in a stone courtyard with a minaret and cloudy sky behind him.