Security Operations Platform · AfifAI
Case study · Security operations · 2024–2026

From camera alert to dispatched responder, on one live map

A multi-tenant security-operations platform for an Israeli security-services company. Control rooms see every site, device, responder and open alert on one map, act on an alert without leaving it, and reach field teams by app, SMS, voice call and chat. I led the architecture and the hard performance and reliability work across seven repositories over almost three years.

.NET 8 / C#SQL ServerRabbitMQReact + TypeScriptFlutterMapbox GLPusher / FCMAWS · Docker
Client
Israeli security-services company (confidential)
My role
Team lead & architect (hands-on)
Timeline
Jan 2024 – Sep 2026, in production
Status
In production
At a glance
6 to 0joins to load the open-alerts list, now one denormalised cache table
0forced re-logins from a network blip. Before, one failed refresh meant a full OTP login.
−71%fields per site in list views (35 → 10) with thin endpoints
N to 1database calls to close stale camera alerts in a sync run (one stored procedure)
480API endpoints across 4 back-end services and 2 workers
322Klines of code across 7 repositories, about 11,000 commits
6 + 5languages on web and mobile, Hebrew RTL included, about 1,000 keys each
84end-to-end tests, plus 7 timed performance flows
The problem

Many sites, many systems, and seconds that matter

The client secures residential communities, campuses and other sites, many of them at once. Alerts came from camera and video-management systems, responders were on the move, and residents and teams had to be reached by phone, SMS and messaging apps. Each site used its own tools, and nothing showed the whole operation in one place. An operator handling an alert needed to see who was close, what their status was and what the procedure said. They needed it in seconds, often in Hebrew and on a desk that stays open all shift.

What I owned

Architecture and the hard parts

  • Designed the service split: admin and location APIs, a messaging worker and a video-system sync worker, all sharing one data layer.
  • Owned the data model and its performance: 174 tables, spatial data, 327 indexes, stored procedures for hot paths and an open-alert cache.
  • Defined the real-time model (web push channels, mobile push, critical-alert siren) and the one-live-device session rules.
  • Led the web console, the responder app rebuild in Flutter and the QA automation, and reviewed the critical paths myself.
  • Set up AI-assisted development: project rules, 25 repo skills and spec-first changes, so the team shipped faster with guardrails.
Architecture

Thin clients, one data layer, slow work off the request path

The diagram is wider than the screen: swipe sideways to see all of it →

Every request is checked against the account → site → team hierarchy and a four-tier role model. Messaging and video-system sync run in separate workers, so a slow provider never holds up an operator's click.

Design decisions

Make the hot path cheap and keep the slow work off it

A cache table for open alerts

The live alerts list is the most-read screen. Instead of joining six related tables on every request, a denormalised table holds only open alerts and is written in the same transaction as the alert. The trade-off is keeping two copies consistent, and we had to fix two gaps before it held in every flow.

Thin first, details on demand

List and map views call thin endpoints (10 of 35 site fields). Alert and event panels load a thin record first and open the heavier sections only when someone expands them. It means more endpoints to maintain, but every first paint is light.

Messaging through a queue

SMS, voice-call and push fan-out to a whole site moved into a RabbitMQ task queue and a separate worker. The API answers straight away and providers can be slow or retried without blocking the operator. The cost is one more service to deploy and monitor.

One live device per type

A user can have a phone, a web tab and a control-center seat. The server decides which one is live, and clients handle three states: active, switched (in-app prompt) and logged out. A session is only torn down on a real 401 or 403, never on a network error.

AI-assisted, spec-first delivery

Each repo carries project rules, a memory bank and skills for its patterns: controllers, storage, Redux API calls and permissions. Larger changes start as a written spec; the responder app archived 57 of them. Reviews got shorter and new team members got productive sooner. The cost is keeping the rules up to date.

Engineering log

What got slow or broke, and what fixed it

Most of the measurable gains came from problems that only showed up with real sites, real alert volume and real phones. A selection:

Nov 2024

Queries scanning instead of seeking

As tenants grew, filtered lists and soft-deleted rows were scanned on every request.

Indexed every foreign key and every soft-delete flag, then kept adding covering indexes on hot filters (327 in total). A nightly job now refreshes statistics on the main and audit databases.

Mar 2025

Video-system sync closing alerts one by one

Each sync run pulled devices and alerts per site and then closed stale alerts row by row.

Bulk upserts, plus one stored procedure that closes all stale alerts and clears the cache in a single call. The worker locks one site at a time and runs a configurable number of sites in parallel.

Oct 2025

Page 2 showed the wrong rows

Some list endpoints paged before sorting, so users saw an arbitrary slice of the data that only looked sorted.

Sorting moved ahead of paging in the shared list pattern, with more sort fields and documentation, and the pattern became a repo skill so new endpoints follow it.

Jan 2026

Operators waiting on SMS providers

Sending to a whole site ran the SMS, voice-call and push sends inside the API request.

All three now go through the task queue and a dedicated messaging worker.

Feb 2026

The open-alerts list was the heaviest query

It eager-loaded site, control room, device, type, readiness state and creator for every row, on every refresh.

Added a cache table of open alerts only, read with no joins. I found and fixed two consistency gaps: one update path skipped the cache, and the control-room field was never filled.

Apr 2026

Background location that deadlocked

Native background location on the phones hit a 401 deadlock, posted duplicates and stopped when the app was killed.

Native location services on iOS and Android keep their own token store and send queue. The queue is flushed when the app opens, and there is one refresh path.

Jun 2026

A one-second network drop logged responders out

The refresh layer treated a DNS failure like a rejected token, so responders were sent back to OTP login.

Failures are now classified where they happen. The refresh makes 3 attempts with 400 → 800 ms backoff, and only a real 401 or 403 ends the session.

Jun 2026

Heavy panels over the map

Opening an alert or event panel fetched everything at once, and permission checks used a linear search through configuration arrays on every table row.

The panel fetches a thin record first and loads its sections lazily. Hot lookups use a reference-cached id → entity index. Map routes are code-split.

Outcome

Where it landed

Open-alerts list

1 table, no joins, open alerts only

Site lists and map

10 fields per site; one call for up to 1,000 sites

Closing stale camera alerts

one stored-procedure call per run, sites in parallel

Site-wide SMS / voice / push

queued to a worker; API returns in ≈0.3 s

Network blip on a responder's phone

3 attempts with backoff; session kept, offline banner shown

Responder offline in the field

queued locally (location: up to 500 points, 4 h TTL) and sent in bulk on reconnect

Deploying a service

push a version tag → Docker image to the registry (4 services), ≈10 min

Numbers with ≈ are estimates. The rest are counted from the code and the commit history.

Stack

Technology stack

Back end
.NET 8, ASP.NET Core, EF Core, worker services, JWT
Data
SQL Server with spatial types, stored procedures, audit database, nightly statistics job
Messaging
RabbitMQ, Pusher, Firebase Cloud Messaging, chat channels, SMS, voice calls, email
Web console
React 18, TypeScript, Vite, Redux Toolkit, Mapbox GL, Turf, fabric.js
Mobile
Flutter (Dart), GetX, native Kotlin and Swift background location, critical alerts
Delivery
Docker, AWS (container registry, S3), GitHub Actions, Bitbucket Pipelines, Playwright + Allure

Free scoping call · 30 minutes · No commitment

Bring one process. Leave with a plan and a number to beat.

Write a few lines: the process or system, what hurts today (time, errors, cost or an AI pilot that stalled) and who uses it. After a short call you get a written plan and a fixed quote within ≈3 days.

Get a free scoping callafif.mansour@gmail.com

Opens a pre-filled email · reply within one business day

The model reads. The code counts.

All projects