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.

Reference data flows from Oracle's PL/SQL packages through an OCI8 connection class to the map. Positions flow from a third-party location service into Redis, which also feeds the map. Positions are never written back to Oracle, and the Laravel database layer is not in this path. Laravel DB layer not in this path Oracle PL/SQL packages sites · employees · zones OCI8 connection class no ORM positions never written back Location service third party broadcasts Redis positions, only here no TTL Map one marker per tag live, or last known Reference data flows from Oracle's PL/SQL packages through an OCI8 connection class to the map. Positions flow from a third-party location service into Redis, which also feeds the map. Positions are never written back to Oracle, and the Laravel database layer is not in this path. Laravel DB layer not in this path Oracle PL/SQL packages sites · employees · zones Location service third party broadcasts positions never written back OCI8 connection class no ORM Redis positions, only here no TTL Map one marker per tag live, or last known
The cache has no expiry — a tag that stops reporting still shows where it was last seen.

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.