FleetOps360
Shipments, dispatch, drivers, vehicles, proof of delivery, invoicing and reports in one application, with six roles each seeing only their own work.
- Role
- Full-stack engineer
- Status
- Built
- Next.js
- React
- TypeScript
- PostgreSQL
- Prisma
- Tailwind CSS
- shadcn/ui
- TanStack Table
- Recharts
- S3-compatible storage
- Docker Compose

The problem
- Context
- A freight business runs customers, shipments, dispatch, drivers, vehicles, proof of delivery, invoices and reports, usually across several tools. Each person needs a different slice of it: a dispatcher assigns work, a driver sees today's jobs, finance sees invoices, a customer sees only their own shipments, and anyone with a reference number can check a delivery's progress.
- Constraints
- One Next.js application with business logic in Server Actions and no separate API. Custom authentication rather than a third-party service, and the whole stack (database, file storage and mail) had to come up locally with one command so it can be self-hosted.
- What was at stake
- One missed authorisation check would show a customer another customer's shipments, prices or internal notes, or expose drivers' details and private delivery photos and signatures. The public tracking page has to reveal progress and nothing else.
What I built
Access control in three layers
Route check, data layer, action guard
A lightweight check at the edge only redirects signed-out visitors and makes no decision about what anyone may do. A data-access layer resolves the user and their permissions from the database once per request, and every Server Action checks the specific permission before touching data.
Permissions as data, not role names
Six roles (Super Admin, Admin, Dispatcher, Driver, Customer and Finance) map to resource-and-action permissions defined in one place and stored in the database. Feature code asks for a permission, never for a role by name, so changing what a role can do is a one-line change.
Ownership checked separately from role
Whether someone may see a particular shipment is answered by one function: staff with the right permission, the customer who owns it, or the driver assigned to it. Detail pages and file downloads use the same answer instead of re-deriving it.
Files and personal data
No public links to private files
Proof-of-delivery photos, signatures and documents live in a private bucket. They are served only through an authorised download route that works out which record a file belongs to, checks access to that record, and refuses anything it does not recognise.
A tracking page that withholds by design
The public tracking page selects only the reference, status, cities, timings and a trimmed timeline. Prices, internal notes, contacts and addresses are never fetched, so they cannot leak through it.
Sessions that are useless if stolen from the database
Only a hash of each session token is stored; the token itself lives in an HTTP-only cookie. Passwords are hashed with bcrypt, sign-in takes the same time whether or not the email exists, and password-reset tokens are single-use and expire after an hour.
Data integrity
Status changes as state machines
Invoices can only move along allowed transitions, from draft to sent to paid or cancelled, with the timestamps stamped on each move. Shipment statuses have locked and terminal states, and validation rejects negative amounts and back-to-front delivery windows.
Reference numbers that tolerate collisions
Shipment and invoice references follow a dated sequence on a unique column, and a clash between two requests at the same moment is retried automatically instead of failing the save.
Operations
One command to run everything
The app, database, file storage and a mail catcher start together with one Docker Compose command, so the full stack runs locally without any outside services.
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
