The Approach
The app is built as a single Flutter codebase for iOS and Android. The domain logic – entries, shifts, role logic, presentation – therefore exists exactly once, and both platforms receive the same change at the same time. What stays platform-specific is essentially whatever genuinely depends on the operating system: location permissions, background behavior, power-saving rules. Those parts sit behind a narrow interface, so the rest of the application never needs to know which platform it is running on.
Data and authentication live in Google Cloud. Identity comes from the authentication layer, and the role assignment belongs to the backend – not to the device and not to a variable set in the client. Server-side rules decide, per request, which entries, shifts and locations a role is allowed to read or modify. The role-dependent interface is therefore a projection of that same permission model rather than its enforcement: whatever the app does not show a role would not be retrievable server-side for that role anyway.
The real-time overview of shifts and entries comes out of the same data layer. Clients subscribe to updated state instead of polling at intervals – that keeps the view current without every open app generating requests by the minute. For a management role this means an overview that moves along with the work; for the architecture it means the permission rules have to hold for live subscriptions as well, not just for one-off reads.
Geofencing sits on top as an optional layer: locations are defined, and wherever the option is active an entry is tied to the matching location. Optional is a deliberate choice here, not a compromise – there are job sites where reliable positioning simply is not available. Having authentication, storage, real-time distribution and scaling come from managed cloud services is precisely what makes it feasible to build an application of this size and keep maintaining it without a large operations overhead: operational effort stays where it can be automated.