How we built a multi-layered defense system for browser sessions that run for days on unattended factory hardware.
Contents
We're dedicated to reliability
Our Reliability Promise →Most web applications assume users interact with a page for a few minutes, then navigate away or close the tab. Session management, CSRF protection, and deployment strategies are all designed around this assumption.
Factory floor software breaks this completely. Workstation kiosks sit on a single page for entire shifts, sometimes for days. Supervisor dashboards run on wall-mounted Smart TVs that may not be touched for weeks. These aren't edge cases; they're the primary use case.
Andon Alert is a real-time factory alerting platform built on Laravel. Workers trigger alerts from kiosk workstations, supervisors receive SMS and email notifications with automatic escalation, and wall-mounted displays show live factory status with automatic updates. The entire system is designed to run unattended around the clock.
During a customer trial, two issues surfaced overnight that both traced back to the same root tension: browser sessions that outlived the assumptions standard web development is built on. While our implementation is Laravel-specific in places, the underlying patterns apply to any web application with long-lived sessions on unattended hardware.
A factory worker left the kiosk running overnight. When the machine woke from sleep the next morning, the worker tapped an alert button and got a "419: Page Expired" error. On a factory floor where someone is reporting a machine failure, an error page is not an acceptable outcome.
CSRF (Cross-Site Request Forgery) protection ensures form submissions originate from your own application, not from a malicious third party. The server embeds a unique token in every form and validates it on submission. Without this protection, an attacker could craft a page that silently submits requests using a visitor's session — in our case, that could mean triggering false alerts or acknowledging real ones without a supervisor's knowledge. Disabling CSRF was never an option.
We already had a JavaScript timer refreshing the token at regular intervals. But when a computer sleeps, the browser suspends all JavaScript execution. Timers created with setInterval freeze entirely and resume where they left off on wake, regardless of how many hours have passed. Meanwhile, the server-side session has expired. The first form submission after waking gets rejected.
The first layer is reactive: if a 419 happens, handle it gracefully. A global submit event listener intercepts all POST forms and submits them via the Fetch API instead of the browser's default behavior. If the response is a 419, the handler fetches a fresh token, updates all form inputs, and resubmits using the native form.submit() method.
A key detail: calling form.submit() programmatically does not re-trigger the submit event, so the resubmission bypasses the interceptor naturally. The server never processed the first request (it was rejected at the middleware layer), so the retry is a clean first submission with no risk of duplicate actions. The worker never sees an error page.
The form interceptor is a safety net, but ideally the token should be fresh before the worker interacts with the page. The visibilitychange event fires when a machine wakes from sleep or a user switches back to a browser tab. Our listener calls the same token refresh function immediately on visibility change.
By the time the worker reaches the screen, the token is already refreshed. The 419 never happens. Browser support is excellent (Chrome 33+, Firefox 18+, Safari 7+), and it requires no polling loops or timers — just a single event listener that fires exactly when needed.
The same factory was running the supervisor dashboard on a wall-mounted Smart TV. Overnight, we deployed an update that changed the dashboard's HTML and JavaScript. The next morning, the TV was still running the old code.
The dashboard automatically polls for fresh data, and workstation statuses update in real time without a page refresh. But the page structure itself — the HTML that renders the cards, the JavaScript that processes the API response — only updates on a full page load. A deployment can change how data is rendered while a long-running client keeps feeding new data into old rendering code.
In most applications, this gap is invisible. Users navigate between pages and refresh their browsers constantly. A Smart TV on a factory wall does none of these things. This isn't unique to our stack — any web application with long-lived pages faces the same gap between data freshness and code freshness.
The solution is to give every page a way to know whether it's running current code. We generate a build identifier from the git commit SHA and make it available application-wide through a config value. Every long-lived page embeds this identifier as a JavaScript variable at render time.
The identifier is included in JSON responses from existing API endpoints — the same ones the page already calls for data updates and token refreshes. On every response, the client compares the server's identifier against its own. A mismatch triggers location.reload() immediately, before any data processing occurs.
The key design decision was to avoid introducing new polling mechanisms. Adding the build hash to existing responses costs one additional string field per response and zero additional HTTP requests. Each page type gets version detection at whatever cadence it already communicates with the server. This approach generalizes well: if your application already has any kind of heartbeat or polling loop, you can piggyback version detection onto it.
Every layer so far depends on JavaScript executing correctly. On modern browsers, this is a safe assumption. On a 2016 Smart TV that has been running continuously for a week, less so.
The meta http-equiv="refresh" tag is implemented at the browser engine level, below the JavaScript runtime. It doesn't depend on setInterval, event listeners, or the Fetch API. We set it to one hour on the two pages that sit idle the longest: the kiosk home screen and the supervisor dashboard.
An important detail for kiosk pages: the timer resets on every page navigation. When a worker taps an alert button, the new page loads with a fresh timer. The meta refresh only triggers on idle screens where there's no user input to interrupt. It's not elegant, but in a defense-in-depth system, the final layer doesn't need to be elegant. It needs to be unkillable.
The four layers aren't a sequence; they're independent systems that overlap. Each one is sufficient on its own to handle the failures we encountered, but they cover different timing windows and failure modes.
Scenario: Machine sleeps overnight, deployment happens while asleep
On wake: The visibilitychange event fires. The handler calls the token refresh endpoint. The response includes a build hash that doesn't match. The page reloads, picking up both a fresh session and new code in a single action.
If visibilitychange fails: The periodic token refresh detects the build mismatch on its next cycle and reloads.
If JavaScript is broken: The meta refresh fires within the hour.
If the worker submits before any refresh: The form interceptor catches the 419, refreshes the token (which also detects the build mismatch), and the page reloads with current code.
Scenario: Smart TV running continuously, deployment happens midday
Normal path: The next automatic status update returns a mismatched build hash. The page reloads immediately. Nobody touches the TV; nobody notices.
If polling fails: The meta refresh fires within the hour.
The layers also share infrastructure. The token refresh function is called by three different triggers (the periodic interval, the visibility change handler, and the 419 retry handler), but it's a single function that handles both token updates and build hash comparison. Adding version detection required no new endpoints and no new HTTP requests.
Browser timers freeze during sleep and resume where they left off. If you depend on periodic timers for session validity, you need a complementary wake-detection mechanism. The Page Visibility API is the right tool.
A 419 is a recoverable error. Intercepting form submissions with the Fetch API lets you detect the failure, fix the cause, and retry without the user ever knowing. This pattern applies to any transient server-side rejection where the client can self-heal.
Polling for data keeps your content current but does nothing for your page structure. If your pages run for hours or days, you need a mechanism to detect when the page's own code is outdated. A build identifier in your API responses is a lightweight solution.
Don't create new polling loops for version detection. If your page already talks to the server, add the build identifier to that response. Version detection should be a passenger on existing infrastructure, not a new vehicle.
The meta refresh tag is ancient, inelegant, and operates below the JavaScript runtime. That's exactly what makes it the right final layer. When everything else could theoretically fail, your last resort should have the fewest dependencies possible.
Developers refresh browsers constantly. QA environments restart daily. Production IoT devices do neither. If your software runs on unattended hardware, your testing should include putting machines to sleep for hours, deploying without refreshing clients, and letting pages run for days.
The entire system was designed, implemented, and deployed within 24 hours. No new dependencies, no new database tables, no new endpoints. Every layer was built by extending existing infrastructure with small, focused additions.
Sometimes the most impactful engineering work isn't building something new. It's making what you've already built resilient to the conditions it actually runs in.
Real-time alerts, automatic escalation, and built-in reliability for your factory floor.