I spent the last few months working on a Laravel 12 platform that combines two things that usually live in separate products: field operations (sites, shifts, patrols, incidents, evidence) and emergency alerts (panic buttons, eWeLink relays, PWA clients, alert monitoring).
The result is a multi-tenant application with a central control plane, isolated tenant databases, an offline-first PWA and a relay integration that has to work when the network does not.
This is not a “look at my SaaS” post. I packaged the source code as a one-time commercial license, so I want to share the engineering decisions that mattered and the parts I would do differently.
The architecture
- Laravel 12 / PHP 8.2+
- Stancl Tenancy with one central database and one database per tenant
- Blade + Vite + Tailwind for the admin and tenant UI
- An installable PWA with IndexedDB sync for guards and community users
- Web Push for alert notifications
- eWeLink Cloud/WebSocket plus an optional local relay agent
- Dompdf for incident and shift reports
The central database stores organizations, tenants, plans, modules, users and branding. Each tenant gets its own database and runs tenant-aware migrations. The tenant bootstrapper selects the connection per request and per queued job.
What was actually hard
1. Tenant isolation is not just a where clause
The dangerous bugs are not the obvious ones. They are:
- a queued job that runs without tenant context;
- a global scope that is bypassed by a raw query;
- a cache key that does not include the tenant ID;
- a file path shared between tenants;
- an admin route that forgets the tenant middleware.
I ended up treating tenant context as part of every boundary: request, job, cache key, filesystem path and notification. If a code path can run without a tenant, it needs an explicit “central” decision.
2. Offline-first is a conflict-resolution problem
The PWA lets guards capture check-ins, incidents and evidence without connectivity. The sync queue is not just “POST when online”. You need:
- client-generated IDs so retries are idempotent;
- a server-side deduplication key;
- explicit conflict states instead of silently overwriting;
- a visible sync status for the user;
- a safe way to expire stale local data.
The most useful decision was to store the sync state on the client and on the server, not only in memory.
3. Relay state is eventually consistent
A panic button is a physical action. The relay may be online, offline, busy or reporting a stale state. The platform has to separate:
- logical alert state (the event exists and needs a response);
- device state (the relay acknowledged the command);
- operator state (someone acknowledged and is handling it).
Trying to make those one field produces false “closed” alerts. Keeping them separate made the monitoring screen honest.
4. Multi-tenant permissions get deep
Roles are not enough. You need role + module + tenant + record-level checks. A supervisor in tenant A must not see tenant B, even if the role key is the same. Permissions also need to be cached per tenant, not globally.
What I would do differently
- Start with a tenant context test suite before building features.
- Put idempotency keys on every device command from day one.
- Treat the PWA sync queue as a first-class module, not a helper.
- Add observability for relay latency and queue lag earlier.
- Keep a single source of truth for alert status transitions.
The source-code option
I decided to sell the source code instead of running a SaaS. The buyer deploys it on their own infrastructure, adapts it to their market and operates it locally. The package includes the central/tenant migrations, the PWA, the relay integration, documentation and a commercial license.
If you are building something similar, the two products are:
- SuGuardia — field operations, patrols, incidents, PWA and PDF reports: source code
- PulsoAlerta — panic alerts, eWeLink relays, PWA and operations, with an optional POS/ERP module: source code
There is also a smaller Android TV remote-control project, FlashLink MX10: source code.
Disclosure: I'm the author and I sell these source-code licenses. No customers, revenue or production data are included.
If you have built multi-tenant Laravel apps with offline sync or device integrations, I would like to hear what broke first in your case.
Top comments (0)