When Networks Fail, SARA Stands Up: Offline Flood Rescue with Gemma 4 E4B

SARA End-to-End Rescue Flow

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

During major floods—like the catastrophic 2022 Pakistan Floods that displaced over 33 million people—mobile towers lose power and internet services collapse. This creates a critical communication blackout where stranded victims cannot signal for help, and rescue teams deploy boats, helicopters, and medical assets based on guesswork.

SARA (Safety And Rescue Assistant) is a 100% offline-first, local emergency command center. Deployed on a single coordinator laptop alongside a simple Wi-Fi hotspot, it creates a private local network—no internet required.

The SARA End-to-End Rescue Flow

SARA simplifies disaster coordination into a seamless, offline process:

SARA End-to-End Rescue Flow

Flood victims connect to the hotspot (SARA-HELP) and access SARA’s intake form using their mobile browser—no app installation needed.

  • Victims can submit emergency details via text, photo evidence (water depth, injuries), or a recorded voice message.

Victim Emergency Request Form

  • Coordinators manage resources through a live-updating Glassmorphic Admin Dashboard equipped with offline Leaflet maps, live WebSocket streams, and RAG-integrated medical protocols.

SARA Command Center Dashboard

Demo

Here is the walkthrough of SARA’s offline system deployment, victim-side emergency reporting form, and real-time dashboard triage updates:

Code

The complete codebase, configurations, and deployment steps are fully open-source and available on GitHub:

👉 GitHub Repository: SARA Offline Rescue

How I Used Gemma 4

At the center of SARA is Google’s Gemma 4 Edge-optimized family (gemma4:e4b / 4B) running locally on the coordinator laptop via Ollama.

Gemma 4 powers SARA in three major ways:

1. Intentional Model Selection: Why Gemma 4 E4B?

Disaster response centers operate on battery backups or portable generators. I needed a highly capable model that could run locally on consumer-grade laptop CPUs/GPUs without needing a connection to cloud servers. Gemma 4 4B fits comfortably within under 8GB VRAM, delivering stable, sub-5-second local inferences in the field.

2. Native Multimodality & Offline RAG Integration

Stranded victims report emergencies under high stress. Gemma 4’s native multimodal capabilities allow me to process multiple modalities in a single pipeline without context switching.

SARA Offline RAG and AI Pipeline

  • Audio: Local voice messages are transcribed seamlessly.
  • Vision: Photo uploads are evaluated directly by the model to detect water depth, trapped individuals, or visible injuries.
  • Offline RAG: The system searches local manuals from WHO and the National Disaster Management Authority (NDMA) Pakistan using local nomic-embed-text embeddings, injecting critical first-aid instructions into Gemma’s prompt.
  • Bilingual Generation: Gemma 4 acts as a translation engine, analyzing English/Roman Urdu inputs and writing simple, reassuring Urdu summary updates for the victim.

Victim Status Update in Urdu and English

3. Agentic Task Coordination via Tool Calling

SARA provides a natural language command box for rescue coordinators. When a coordinator types “Are there any available rescue boats?” or “Dispatch helicopter to case #3”, Gemma 4 maps the query to custom Python tools (dispatch_rescue_team, get_resource_status, etc.) via Ollama’s native tool calling. It updates the SQLite database, triggers WebSocket alerts, and returns structured confirmation text—all fully offline.

Avoiding the Great Treasure Hunt Stall of 2025: What I Learned from Building a Scalable Hytale Server

The Problem We Were Actually Solving

We needed to support a large player base with thousands of concurrent players for the Treasure Hunt game mode. The game’s event-driven architecture meant that every player movement, item pickup, and treasure collection triggered a flurry of events that needed to be processed quickly and efficiently by the server. The catch was that the event bus was prone to congestion, leading to unpredictable delays and stalls.

What We Tried First (And Why It Failed)

Initially, we attempted to mitigate the congestion by introducing multiple event bus instances, each with its own set of event handlers. We also implemented a load balancer to distribute the traffic across multiple servers. However, this setup ultimately led to a “server farm effect,” where the load balancer would redirect traffic to a server that was already congested, resulting in an even bigger stall.

In hindsight, we should have recognized that our approach was focused on “distributing the pain” rather than “mitigating it.” By spreading the congestion across multiple servers, we were merely delaying the inevitable stall, rather than truly addressing the root cause of the problem.

The Architecture Decision

After a series of intense discussions with my team, we decided to take a different approach. We introduced a concept called “event chunking,” where we grouped related events into larger chunks and processed them in batches. This allowed us to significantly reduce the number of events being processed in real-time, making the system much more efficient and scalable.

We also implemented a custom event queue using Amazon SQS and a message-driven architecture. This enabled us to offload the event processing to worker nodes, freeing up the main server to focus on handling game logic and player input.

One of the key insights we gained from this experience was the importance of understanding the specific nature of the events being processed. By analyzing the event patterns and frequencies, we were able to optimize the event chunking and message-driven architecture to better suit the needs of our game.

What The Numbers Said After

After rolling out the new architecture, we were able to achieve a significant reduction in stall times, from an average of 10 minutes to under 5 seconds. The increased scalability also enabled us to handle a much larger player base, with our server supporting over 5,000 concurrent players with ease.

The metrics also revealed that the event chunking approach reduced the number of events being processed in real-time by a factor of 10, resulting in a corresponding decrease in server load. We also observed a significant reduction in latency, from an average of 200ms to under 50ms.

What I Would Do Differently

If I had to do it over again, I would focus even more on understanding the specific nature of the events being processed and the underlying systems requirements. I would also invest more time in analyzing the event patterns and frequencies to inform the design of the event-driven architecture.

One area I would explore further is the use of more advanced load balancing techniques, such as machine learning-based load balancers, to identify and redirect traffic to less congested servers. I would also consider implementing more sophisticated monitoring and alerting systems to detect early warning signs of congestion and prevent the stall from occurring in the first place.

In conclusion, the experience of building a scalable Hytale server taught me the importance of taking a holistic approach to solving complex engineering problems. By understanding the specific requirements of the system, analyzing the underlying systems and data, and implementing targeted solutions, we can create more efficient and scalable systems that meet the needs of our users.

How we moderate a live video-chat app in real time (without going broke on AI calls)

I work on Camdiv, an anonymous one-to-one video chat. You open the page, you get matched with a stranger, you talk. It’s the Omegle-style format, and from the outside the hard part looks like the video: WebRTC, NAT traversal, keeping latency down.

It isn’t. WebRTC is mostly a solved problem. The hard engineering is moderation. You’re putting two anonymous strangers on a live camera together, with almost no friction, and you have a few seconds to catch it if one of them does something that gets your platform pulled from every app store on earth.

Three things shaped every decision below, and they fight each other the whole way. The first is cost: moderate live video naively and the bill alone will sink you. The second is false positives, because a wrong ban is a real person you just kicked off for nothing. The third took a near-miss to learn, so it gets the longest section here: you can’t actually trust the video frame you’re moderating.

Why live anonymous video is the worst moderation surface

Most moderation problems give you time. A user uploads a photo or writes a comment, and you can scan it before anyone else sees it. The content sits still while you decide.

Live video gives you none of that:

  • There’s no upload step to gate. The stream is already happening.
  • The content is ephemeral. By the time you’ve “reviewed” a frame, the next one is different.
  • Anonymity plus zero signup friction means abuse is cheap and repeatable.
  • You have seconds, not minutes. A human moderator can’t sit on every one of thousands of concurrent streams.

So whatever you build has to be automated, run per frame, stay fast, and be cheap enough to run nonstop. Those goals do not sit comfortably together.

The pipeline

The browser samples a JPEG from the local video every few seconds and sends it over Socket.IO to our backend. The backend forwards it to a separate moderation microservice (a small FastAPI app on its own host) over HTTPS, locked down with an internal shared key and an origin allowlist at the reverse proxy. The service runs the classifier and returns a compact verdict.

flowchart LR
  A["Browser<br/>samples a JPEG every few seconds"] -->|Socket.IO| B["Backend<br/>Node / TypeScript"]
  B -->|"HTTPS + internal key<br/>origin allowlist"| C["Moderation service<br/>FastAPI, isolated host"]
  C -->|"verdict JSON:<br/>nsfw, minor, confidence, reason"| B
  B --> D{Act on the verdict}
  D -->|explicit| E[Confirmation + ban path]
  D -->|possible minor| F[Human review queue]
  D -->|safe| G[Do nothing]

Splitting moderation into its own service pays off in a few ways. A crash or memory spike in the ML host doesn’t take the chat backend down with it. The two scale independently, since the Node app is I/O-bound and the moderation box is CPU- and GPU-bound. And the heavy model dependencies stay out of the application runtime, so a backend deploy doesn’t have to drag a model toolchain along with it.

The verdict shape is deliberately tiny:

{ "unsafe": false, "minor": false, "score": 0.0, "reason": "...", "source": "gemini-safe" }

Two independent booleans, a confidence score, and a short human-readable reason that lands in our logs. That reason field has paid for itself many times over when I’m trying to work out why something did or didn’t fire.

The cost wall, and why we schedule the model instead of running it on every frame

Our moderator is a vision-language model (Gemini Flash Lite). Per call it’s cheap. The trouble is the multiplication: concurrent users times a frame every few seconds is millions of calls a day. Run a VLM on all of them and the model bill, long before infrastructure, becomes the thing that kills the company.

We started somewhere more conventional: an on-box NSFW CNN (NudeNet) with an escalation tier, where ambiguous scores got a second opinion from a hosted nudity API and Google Vision’s SafeSearch. It worked. But it was three systems to keep healthy, and the CNN was biased: much better at detecting female anatomy than male, which is a real gap on a platform where most of the abuse is the latter.

We replaced the whole thing with a single VLM because it reads context in a way a pure classifier can’t. It can tell a shirtless guy on a couch from actual exposure. It handles the common trick of holding explicit content up on a phone to the camera. And it returns structured JSON I can trust to parse, with a built-in safety filter whose refusal to even describe an image is itself a useful signal.

The cost math only works because of one decision: we don’t moderate every frame. Each chat gets a small number of model calls, front-loaded into the first minute, and then we stop.

flowchart TD
  F["Frame arrives for a match<br/>(session key = userId:roomId)"] --> Q{"Scheduled check due<br/>in this match's first minute?"}
  Q -- no --> S["Return safe — no model call"]
  Q -- yes --> R{"Within global rate<br/>and daily budget?"}
  R -- no --> S
  R -- yes --> C["One VLM call · consume the slot"]
  C --> V["Verdict: nsfw, minor, confidence"]

The premise, borne out by our logs, is that bad actors reveal themselves quickly. They don’t behave for ten minutes and then flip. They flip in the first few seconds, because the reaction is the whole point for them.

The part that matters is the session key. The schedule is keyed per match (userId:roomId), not per user. Every new match starts a fresh schedule. Key it per user instead and someone could behave for their first 60 seconds, exhaust the schedule, then expose themselves to every later partner for free. Keying per match means partner #2 is a brand-new session with a brand-new set of checks. You can’t outwait the system by being patient once.

On top of the per-match schedule there are global backstops: a rate limit, a daily budget ceiling, one in-flight call per room, and a lock that dissolves a room’s moderation the moment it returns an unsafe verdict. A bad chat costs exactly one billable model call instead of a flood of them.

The frame you can’t trust

Here’s the section I’d tell my past self to read first.

The model returns two independent flags: is this explicit, and does the person look like a minor. The naive enforcement rule writes itself: explicit plus minor equals instant permanent ban, no appeal, done.

We deliberately don’t do that, and here’s the attack that taught us why.

The frame we moderate is sampled and sent by a client. In a peer-to-peer video session, the bytes we classify can’t be cryptographically proven to have come from the partner’s live camera. A malicious client can send a frame of its own choosing. So if a single AI verdict on an unauthenticated frame triggered an instant permanent ban, any user could permanently ban any partner just by feeding our pipeline a chosen image. The most severe, least reversible action in the system would be trivially weaponizable by the person who stands to gain from it.

So we split enforcement by how severe and how reversible the call is:

flowchart TD
  V["VLM verdict: nsfw, minor"] --> M{"Looks like a minor?"}
  M -- yes --> H["Human review queue<br/>(HIGH priority if also explicit)<br/>never an automatic ban"]
  M -- no --> N{"Explicit?"}
  N -- no --> OK["Safe · do nothing"]
  N -- yes --> CF{"Single frame at<br/>very high confidence?"}
  CF -- yes --> BAN["Enforce ban + capture evidence"]
  CF -- no --> ACC["Add to confidence over<br/>a short rolling window"]
  ACC --> T{"Evidence adds up?"}
  T -- yes --> BAN
  T -- no --> WAIT["Wait — no action yet"]

Anything that flags a possible minor goes to a human review queue and is never auto-banned, however confident the model is. A person makes that call, looking at captured evidence, because the cost of getting it wrong in either direction is too high to hand to a script. Explicit-but-adult content goes through the confirmation path below.

If you take one thing from this post, take this: in any system where the input can be shaped by the party who benefits from the outcome, an automated decision is an attack surface. Authenticate the input before you automate the verdict.

Not banning the wrong people

Even for clear-cut explicit content, one frame shouldn’t end someone’s session. Cameras produce garbage: bad lighting, a weird angle, a half-second of motion blur that a model misreads.

So a single frame only acts immediately if it comes back at very high confidence. Below that bar, we add up confidence across a short rolling window and act only once the evidence agrees with itself. A one-off false flicker never reaches the threshold. A genuinely explicit stream trips it almost at once, because frame after frame says the same thing.

We check three signals when we ban: IP, a device fingerprint, and the account (sign-in is Google, with an age-verification gate). Stacking them makes coming back more than a one-click affair, without banning everyone behind a shared NAT because of one person.

Reports, without taking them at face value

Users can report each other. We treat a report as a signal, never as a verdict, because a report is weaponizable too.

Image reports (nudity, suspected minor) get validated by the model. We send the reported snapshot through the classifier, bypassing the schedule since a human explicitly asked us to look, and let that be the source of truth. High-confidence explicit gets enforced, borderline goes to human review, suspected-minor always goes to human review, and a clean frame quietly drops the report.

Reports we can’t check with an image model, like verbal or racial abuse, work differently. There we use a weighted score: independent reporters each add weight to a target, and a ban only triggers once enough distinct people report the same person inside a window. One furious stranger can’t get you banned. A pattern of them can.

Failing open, on purpose

Eventually your moderation service will be unreachable. A deploy, a crash, a network blip. You have to decide ahead of time what happens to live chats during that window: block everyone, or let them through?

We chose to fail open, behind a circuit breaker. After several failures in a row the backend trips the breaker, stops hammering the dead service for a cool-off period, then sends one test call to see if it’s back. While it’s tripped, chats keep flowing unmoderated.

stateDiagram-v2
  [*] --> Closed
  Closed --> Open: consecutive failures exceed threshold
  Open --> HalfOpen: cool-off elapsed
  HalfOpen --> Closed: test call succeeds
  HalfOpen --> Open: test call fails
  note right of Closed: calls flow normally
  note right of Open: skip moderation,<br/>chats continue

It’s an uncomfortable tradeoff and I won’t pretend otherwise. It’s only defensible because of what’s around it: the per-match schedule re-checks every new pairing, user reports keep working, the face-presence gate still runs, and every action is logged so we can act after the fact. Failing closed, which freezes everyone’s video the instant the ML box hiccups, is its own kind of harm, and on a real-time product it’s the more visible one. Pick your failure mode on purpose. Don’t let it be an accident of which try/catch you forgot.

Due process: evidence and appeals

Automated enforcement gets things wrong sometimes. Ship it without a way to be wrong gracefully and you’ve built something you’ll regret.

So every ban captures the triggering frame as evidence and stores it server-side. Every ban is appealable. An admin reviews the evidence and either upholds or overturns it, and overturning also deletes the stored evidence. Bans persist with their trigger, confidence, and reason, so there’s an audit trail. The appeals queue isn’t something you bolt on later. It’s part of the enforcement system, and having it is what lets you turn the automation up at all.

What’s still hard

I don’t want to end on a victory lap. A few things here are genuinely unsolved for us:

  • We still can’t prove a frame came from the real camera stream. That’s the root cause of the weaponization problem above, and in browser WebRTC it’s a hard one. We mitigate it; we haven’t solved it.
  • We moderate video, not audio. Purely verbal abuse only gets caught through reports.
  • Adversarial timing is an arms race. The per-match reset raises the cost of gaming the schedule, but a determined actor still probes it.
  • How many checks to run, how far to front-load them, where to set the budget ceiling: that’s a knob we’ll be turning forever, not one we got right on day one.

What keeps me interested is that almost none of this is about video. It’s about building enforcement cheap enough to run nonstop, accurate enough to trust with real consequences, and fair enough that the appeals queue doesn’t make you wince. If you want to see where it ends up, it’s live at Camdiv.

Happy to go deeper on any piece in the comments. The scheduling math and the fail-open call are the two I’d most like to be argued with about.

AWS Backup: Resiliencia ante Desastres y Ransomware (en español sencillo)

Hola comunidad,

Hoy quiero compartir con ustedes sobre un servicio muy interesante que de manera simplificada nos ayuda a garantizar la resiliencia y disponibilidad de nuestra información en la nube: AWS Backup.

Además de explicar sus conceptos básicos, estaré detallando cómo implementar una estrategia de copias de seguridad robusta: Cross-Account y Cross-Region que en español sencillo es crear copias de respaldo en una región distinta dentro de una cuenta distinta de donde está el repositorio de información.

¿Qué es AWS Backup?

AWS Backup es un servicio de copias de seguridad completamente administrado por AWS diseñado para proteger los datos de diferentes servicios que almacenan datos.

Anteriormente, administrar backups significaba crear scripts personalizados o usar configuraciones individuales por servicio (ej. instantáneas manuales para RDS, S3, DynamoDB, etc.). AWS Backup resuelve esto brindando un panel de control único donde puedes configurar políticas de respaldo que aplican de forma unificada a todos tus recursos compatibles.

Principales beneficios:

  • Gestión centralizada: Un solo lugar para configurar, gestionar y auditar respaldos.
  • Automatización basada en políticas: Define planes de respaldo (frecuencia, retención, ciclo de vida) que pueden ser asignados todos los recursos de un mismo tipo o específicos a través de reglas de etiquetado.
  • Seguridad y Cumplimiento: Integración con AWS KMS para cifrado de respaldos y generación de reportes.

Estrategia Cross-Account y Cross-Region

Para cumplir con arquitecturas bien diseñadas y prácticas sólidas de Disaster Recovery (DR), es recomendable aislar algunas copias de seguridad adicional a las que creas en tu cuenta principal, y por ello implementamos una estrategia de respaldo en otra región y en otra cuenta.

  • Cross-Region (Entre Regiones): Garantiza que, en el raro escenario de que una región completa de AWS esté indisponible, tus respaldos persistan en una región secundaria y estén listos para ser restaurados.
  • Cross-Account (Entre Cuentas): Asegura que, si una cuenta sufre un compromiso de seguridad, los atacantes no puedan alterar o eliminar los respaldos que residen en una cuenta completamente distinta (cuenta de bóveda aislada).

Implementación Práctica

He preparado un repositorio público en GitHub donde puedes encontrar las plantillas de infraestructura como código para desplegar este escenario:
https://github.com/pangoro24/aws-backup-cross-account–cross-region-solution

Pasos a Alto Nivel para la Implementación:

  1. Configurar AWS Organizations: Debes habilitar el “Trusted Access” para AWS Backup en tu organización y permitir los respaldos “Cross-Account” en tu consola de administración.
  2. Crear Bóveda (Vault) en la Cuenta DR: Desplegamos la bóveda de AWS Backup junto a su llave de KMS para recibir de forma segura las copias.
  3. Crear Plan Principal en la Cuenta de Origen: En la cuenta principal, configuramos el Plan de AWS Backup estableciendo reglas (ej. copia local diaria y copia remota semanal), creando respaldos en esta cuenta y enviando copias hacia la bóveda de nuestra cuenta DR.
  4. Desplegar Recursos con Etiquetas: Para validar que AWS Backup está funcionando, desplegamos dos repositorios de información (un bucket S3 o una tabla DynamoDB), y que contengan las etiquetas establecidas para que sean considerados para el plan de backup.

Te invito a revisar el archivo README.md dentro del repositorio para ver los comandos detallados de despliegue usando AWS CLI. Uno de los errores comunes es la asignación de permisos en las llaves de KMS. Si no se están creando las copias de seguridad, empieza por revisar cloudtrail y ver si hay acciones de Backup denegadas por falta de permisos.

Próximos pasos: puedes intentar restaurar los repositorios de información a partir de los respaldos creados y configurar el vault lock a conveniencia.

Costo

Luego de tener el demo activado por varias semanas y donde el bucket de s3 y la tabla de dynamodb solo tienen datos de prueba, realmente el costo lo verás en las llaves de kms así que podrán realizar varios respaldos y copias antes de que decidan eliminar la infraestructura. A medida que el tamaño de los repositorios de información sea más grande, entonces sí podrías ver gastos significativos en almacenamiento de las copias, las restauraciones y la transferencia de datos entre regiones. El detalle de los costos está acá: https://aws.amazon.com/es/backup/pricing/

Conclusión

La tranquilidad de saber que tus datos están seguros, respaldados y en otra cuenta dedicada no tiene precio. AWS Backup facilita drásticamente la configuración y el monitoreo de estas estrategias de recuperación ante desastres que antes requerían un esfuerzo inmenso de desarrollo personalizado.

¿Ya estás usando AWS Backup de forma centralizada en tu organización? ¡Déjame saber en los comentarios!

Gracias por leer.

Referencias:

ASP.NET Core Request & Exception Logging with a Built-In Dashboard

Most ASP.NET Core logging setups tell you that something failed — but not what actually happened during the request lifecycle.

I built AsGuard to provide lightweight request + exception logging with a built-in dashboard and minimal setup.

GitHub Repository:
https://github.com/mahmood-alsarraj/asguard

Why AsGuard?

  • centralized request + exception visibility
  • built-in dashboard
  • minimal setup
  • easier inspection during debugging
  • lightweight integration
Feature Description
🚀 Queue-based persistence Non-blocking async writes – your API never waits for database
📊 Built-in dashboard Beautiful Razor UI with dark/light mode and live updates
🔒 Sensitive data masking [AsGuardMasked] attribute + configurable header redaction
📝 Body capture Request/response bodies with content-type allowlist
🔗 Correlation IDs Distributed tracing with configurable headers (default: X-Correlation-ID)
⚡ Live SSE updates Real-time push notifications without SignalR
🗄️ Multiple databases SQL Server, PostgreSQL, SQLite, or In-Memory
📈 Exception analytics Trends, severity summaries, and configurable alerts
🎯 Host ILogger capture Automatically captures ILogger<T> warnings/errors
🔔 Alerting system Queue pressure, exception spikes, persistence failures
📡 REST API Full programmatic access to logs and stats
🧹 Retention policies Auto-cleanup with configurable intervals

Quick Setup

dotnet add package AsGuard
builder.Services.AddRequestLogging(options =>
{
    options.DatabaseProvider = LoggingDatabaseProvider.Sqlite;
    options.ConnectionString = "Data Source=AsGuard.db";
    options.DashboardRoute = "/request-logs-ui";
    options.DashboardUsername = "admin";
    options.DashboardPassword = "admin123";
    options.EnableExceptionLogging = true;
    options.LogRequestBody = true;
    options.LogResponseBody = true;    
});

var app = builder.Build();

app.UseExceptionHandler("/error");
// After the exception handler
app.UseRequestLogging();

Navigate to “/request-logs-ui”, enter your credentials, and watch your application’s heartbeat in real-time.

Final Thoughts

Modern applications need visibility.

Sometimes you don’t need a massive observability platform — you just need a fast and practical way to inspect requests and exceptions.

That’s the problem AsGuard aims to solve for ASP.NET Core developers.

Resources:

⭐ Star the repo if AsGuard helps you!