The map has to work when the tag goes quiet
A live view of where people are across an industrial site, built on two data sources that had nothing in common.
Role
Contract developer, alongside the client's engineering team
Stack
Laravel, Oracle, OCI8, Docker, Apache, PHP 7.4, Redis, Leaflet
Constraint
Client system, still in active development. Architecture only — no client name, no vendor, no schema.
What it's for
People working across a large industrial site carry location tags. Whoever is coordinating needs to know two things: who has reached a given location, and where everyone else was last seen.
That second half shapes the whole design. A system that only shows live positions shows nothing for the person whose tag has gone quiet — and a tag goes quiet for the reasons that matter most. Dead battery, no coverage, someone in trouble and not moving. Live positions are the easy case.
Two sources, nothing in common
Reference data — sites, employees, zones — lived in Oracle. Positional data came from a third-party location service. Neither knew about the other, and the application was where they met.
Laravel, without the database layer
Laravel's database layer supports MySQL, PostgreSQL, SQLite and SQL Server. Oracle needs a third-party package on top of PHP's OCI extension.
I didn't use one. The Oracle side wasn't a set of tables to model — the schema sat behind PL/SQL package functions, so the database exposed an API rather than a structure, and every call carried session-scoped state. An ORM has nothing to offer against that. It would have been an abstraction over an abstraction, each making the other harder to debug.
So data access runs on the OCI8 extension directly, against the existing package interface, through a connection class carried forward from the system's previous incarnation. I adapted that pattern rather than inventing one, because it already matched the contract the database exposed.
One habit worth keeping when connecting to Oracle: refuse to open the connection if credentials are missing, before attempting anything. Oracle locks a service account after repeated failed authentication, so an application that retries a bad connection doesn't just fail — it takes the account down for everyone using it.
The container was the hard part
OCI8 needs Oracle's client libraries present at the system level. That's not a Composer dependency — it's a package repository, a specific release and update version, and shared objects the extension has to be able to find.
The image builds on Oracle Linux and installs the client libraries with their development and SQL*Plus packages. One of the symlinks points a library version the stack expects at the version actually installed — a mismatch you only find by watching the extension fail to load. PHP 7.4 with OCI8 and Redis compiled in, Apache in front, and a service user created with fixed uid and gid so file ownership inside the container matches the host. Without that last part, every file the application writes has the wrong owner on the other side of the mount.
This is the section I'd want anyone evaluating me to read. The map is a day's work. A reproducible environment where an Oracle connection opens the same way on every machine is what took the time.
Why positions live in cache and nowhere else
Position updates arrive as broadcasts — devices report as they move, continuously, across the site.
Writing each one to Oracle would mean constant writes to a database already serving other work, for values that are stale within seconds. So positions go to Redis and stop there. No table, no write-behind, no durable copy.
And they don't expire. A cached position is overwritten when a newer one arrives and persists otherwise. That turns the cache into a record of last known position rather than a window of recent activity — a tag that went quiet three hours ago still shows where it was when it stopped.
A TTL would have been the conventional choice and the wrong one. It would silently erase exactly the person you're searching for.
The trade-off comes with the pattern: cached state is not durable state, so anything held this way has to be treated as reconstructible rather than authoritative, and that assumption belongs in the operational documentation rather than in someone's head.
The map
Leaflet, one marker per tag, popups carrying device detail. The broadcast payload contains considerably more than coordinates — device state, alert flags, signal source readings — so the work here was deciding what belonged on a map and what belonged elsewhere.
How it ended
A second location vendor was planned, and with it an abstraction layer so the application could treat different vendors' data interchangeably. The client's vendor roadmap changed and that work was deferred. With it postponed indefinitely I took another contract and ended the engagement.
Before stepping back I documented the codebase — how the UI is assembled, how existing features work, how to read from the cache layer, and how position data arrives. Their team is still building on it, and I'm called back occasionally for questions.