← Back to blogEngineering

Why Attenzo installs like a native app instead of shipping one

Attenzo Team · July 9, 2026 · 5 min read

A workforce app lives on people's phones, in their pocket, checked dozens of times a day between shifts. That's a native-app usage pattern, and it deserves a native-app feel — but building and maintaining two separate native codebases (plus the App Store and Play Store review queues that come with them) is a real ongoing cost we didn't want to pay before it was earned. So Attenzo is a Progressive Web App: installable, offline-capable, and — this is the part most PWAs get visibly wrong — indistinguishable from a native app in daily use.

The tell almost every PWA has

Add nearly any PWA to an iPhone's home screen and open it, and you'll likely see it: content sliding underneath Safari's collapsing bottom toolbar, or a status bar overlapping the header. It happens because of one missing line — `viewport-fit=cover` on the viewport meta tag — without which `env(safe-area-inset-*)` (the CSS values that describe where the notch, home indicator, and Safari's chrome actually are) silently returns zero, and every safe-area calculation in your CSS quietly does nothing.

We added that one line, then built actual padding on top of it for the fixed top bar and bottom navigation, so the interactive parts of the app always sit clear of the home indicator and Safari's toolbar — instead of half-hidden behind them like the tab bar in a lot of shipped PWAs.

What 'installable' actually means here

  • On Android and desktop Chrome, we hook the browser's native install prompt directly — one tap, and the app is on the home screen or in the dock like anything else there.
  • iOS Safari has no equivalent API, so we show the real 'tap Share, then Add to Home Screen' instructions instead of pretending a one-tap install exists where it doesn't.
  • Once installed, a service worker caches static assets and serves an offline fallback page for navigation — but deliberately never caches API responses. Attendance and payroll data is tenant-scoped and changes constantly; serving a stale cached response for someone else's organization would be a real bug, not a performance win.

The result is one codebase, one deploy pipeline, and an app that opens full-screen, survives a flaky connection, and doesn't visually announce itself as 'just a website' the moment you scroll to the bottom of the screen.