Skip to content
All work
05Power-outage reporting platform2026

PowerWatch

Community-reported electricity status for Nigerian neighbourhoods: an Expo mobile app, a Fastify API and a landing page. A neighbourhood's status follows the majority of distinct people who reported in the last 30 minutes.

74
API endpoints
21
Prisma models
26
Mobile screens
6
Geographic levels
Role
Author
Year
2026
Status
Open source
Type
Power-outage reporting platform

01Overview

PowerWatch lets people report in one tap whether power is on or off where they live, and see the status of their neighbourhood before they get home. Everyone following that neighbourhood gets a push notification when the status changes.

It began as a team project for the Orange internship programme 2026, where I built the backend. The repository now holds three parts: the API, a mobile app for Android and iPhone, and a landing page. The API is Fastify 5 and Prisma 7 over MySQL, with 74 endpoints across eight route groups.

The interesting part is the geography. "Is the power out?" is a question about a neighbourhood, but outages are caused at substation and feeder level, which does not map onto any address a person would type.

02The problem

Nigerian grid supply is unreliable in ways that are locally obvious and nationally invisible. There is no shared record of who is off, for how long, or how often, so nobody can tell a bad week from a bad transformer.

Crowd-sourcing that record has two hard parts. The first is location: a report is only useful if it lands in the right area, and Nigerian addressing does not reliably resolve below a local government area. The second is trust: one wrong or repeated report must not flip the status of a whole neighbourhood.

It also has to work on a phone, on mobile data, during a blackout. That rules out anything heavy.

03Architecture

  1. 01

    Six-level geographic hierarchy

    Country → State → LGA → City → Town → Neighborhood, modelled as first-class entities and seeded with Nigeria's 37 states and their LGAs and towns. A report attaches at the finest level the user can identify, and every summary rolls up through the chain, so the same data answers both "is my street off" and "how did Ondo State do this month".

  2. 02

    Consensus decides the status

    Each person is counted once, by their most recent report in the window. The neighbourhood's status is the majority of those reporters over the last 30 minutes, and a tie keeps the current status. The server also refuses a second report from the same person for the same neighbourhood within 5 minutes.

  3. 03

    Reports, outages and the join between them

    Report is what a person submitted. Outage is the event. OutageReport joins them. When the consensus flips to OFF an outage starts at the earliest OFF report that was counted, and when it flips back the outage closes with its duration recorded.

  4. 04

    Pre-aggregated summaries

    DailyReportSummary, WeeklyOutageSummary and MonthlyStatistic hold report counts, outage totals and the longest outage per neighbourhood, so weekly and monthly figures are not recomputed from the report table on every request.

  5. 05

    Layered Fastify backend

    routes → controllers → services → repositories, with services split into commands and queries across ten areas, and Swagger generated from the route schemas. There are 21 Prisma models, and Prisma 7 talks to MySQL through the MariaDB adapter.

  6. 06

    Auth and delivery

    JWT access and refresh tokens with device-bound sessions, a 6-digit email code for sign-up and password reset, and rate limiting on the public routes. Push notifications go through the Expo Push Service, and outage alerts, restoration alerts and community updates each have their own switch. Every notification is stored in NotificationLog.

  7. 07

    Mobile app and landing page

    The app is Expo SDK 57 with Expo Router, 26 screens in light and dark mode, and a MapLibre map of nearby neighbourhoods coloured by status. The session token lives in SecureStore. The landing page is React 19 on Vite with Tailwind CSS and explains the app from 320 px phones up to desktop.

  8. 08

    Audit trail

    AuditLog and NotificationLog record who changed what and what was actually delivered. On a civic dataset that may be cited, being able to reconstruct the record matters more than saving the rows.

04Key decisions

Decision 01

Count people, not reports

A status that follows the latest report can be flipped by one person. Counting each person once inside a time window means a wrong report is outvoted, and reporting ten times counts as reporting once.

Decision 02

Geocode, then fall back offline

GPS coordinates are resolved through OSM Nominatim's boundary data, falling back to a local LGA coordinate dataset when that fails, and mapped onto the seeded six-level hierarchy. A first version used LGA centre points, which put reports near a boundary in the wrong area.

Decision 03

Materialise the summaries

The map and history read from pre-aggregated tables. Live aggregation over reports would have been simpler until the first thousand rows, and then permanently slower.

05Stack

  • TypeScript
  • Fastify 5
  • Prisma 7
  • MySQL
  • Expo
  • React Native
  • MapLibre
  • React 19
  • Vite
  • Tailwind CSS
  • Expo Push
  • Swagger
  • Zod
  • Nodemailer