One backend, one web app, one mobile app: 20 back-office modules (inventory opname, helpdesk, approvals, risk, audit follow-up, shipments) behind a single API. Designed, built, secured, and shipped by Daffa Sinulingga, solo, in three weeks.
The client, an Indonesian utility company, ran its back office across disconnected tools: spreadsheets for asset stock-taking, chat threads for approvals, paper for audit follow-up. Field staff doing inventory opname often work in storage areas with no signal, so anything mobile had to keep working offline and sync later. Office staff needed a web app with department-level access control, realtime notifications, and a bilingual UI (Indonesian/English), since documents flow in both languages. They wanted one system, not twenty logins.
A monorepo with three consumers of one API:
- Backend: FastAPI (Python 3.12, async SQLAlchemy 2 on PostgreSQL 16). 173 HTTP endpoints across 20 domain modules, schema evolved through 23 Alembic migrations.
- Web: Next.js 15 / React 19, 55 pages, TypeScript strict.
- Mobile: Expo (React Native) for Android and iOS, 23 screens, offline-first.
Realtime notifications go out over SSE, multiplexed on a single shared Postgres LISTEN connection — a redesign of an earlier connection-per-stream approach that did not survive a load review. Mobile gets the same events via Expo push.
The mobile opname flow queues scans in expo-sqlite. The queue survives force-kill, and a cold-start fix ensures scans captured while offline are never stranded when the app next launches without a network.
There is also an AI layer: documents and knowledge-base entries are embedded (voyage-3, 1024 dimensions, stored in pgvector) and queried through an Anthropic-backed assistant. With no API key configured, it degrades to plain keyword search instead of breaking.
flowchart LR
subgraph Clients
W["Next.js 15 web\n55 pages, id/en"]
M["Expo mobile\nAndroid + iOS, 23 screens"]
Q[("expo-sqlite\noffline queue")]
M --- Q
end
subgraph API["FastAPI backend"]
R["173 endpoints\n20 domain modules"]
S["SSE hub\n1 shared LISTEN conn"]
A["Assistant\nAnthropic + voyage-3"]
end
PG[("PostgreSQL 16\n+ pgvector")]
P["Expo push"]
W -->|REST| R
W -->|SSE| S
M -->|"REST (sync on reconnect)"| R
R --> PG
S --> PG
A --> PG
R --> P --> M
- ~1,500 automated tests. 701 pytest functions (90 files), ~596 Vitest cases (64 files), ~242 Jest cases (37 files). Feature commits consistently paired with test commits.
- Security hardening campaign: 17 verified findings remediated across 4 tranches, spanning backend, web, and mobile — DoS surface reduction, rate limiting (custom middleware with Retry-After and store sweeping), CSV formula-injection guards, data-loss races in offline sync, cache allowlists, and error-detail leakage to the UI. Details stay private; the count is the point.
- Auth done properly. Register, email verify, admin approve, login, refresh-token rotation, logout. The app refuses to boot on a placeholder secret. The bootstrap CLI never accepts a password as an argument — interactive hidden prompt only.
- Fine-grained access control. Department capability flags gate asset access and shipment checkpoints; approver rights are revalidated through manager linkage rather than trusted from stale role data. Each of these shipped spec-first: a dated design doc committed before the code.
- Bilingual by construction. 1,127 message keys per locale on web, 282 per locale on mobile, with exact key parity between Indonesian and English.
- Dev safety rails. The seed command hard-refuses to run unless an explicit env flag is set. Docker Compose brings up the database and API with health-gated startup. Each mobile release cut has a manual smoke checklist noting what Expo Go can and cannot verify.
- Scale of delivery: 427 commits, single author, 2026-07-02 to 2026-07-21, feature-branch to reviewed-merge throughout. ~60k lines of tracked Python and TypeScript across 605 files.
| Layer | Choices |
|---|---|
| Backend | Python 3.12, FastAPI, async SQLAlchemy 2 + asyncpg, Alembic, Typer admin CLI, uv |
| Database | PostgreSQL 16 + pgvector (Docker Compose) |
| Auth | JWT (PyJWT), Argon2 password hashing, refresh-token rotation |
| Web | Next.js 15, React 19, TypeScript 5.7 strict, Tailwind CSS 4, next-intl |
| Mobile | Expo SDK 54, React Native 0.81, expo-router, TanStack Query 5, expo-sqlite, expo-camera (QR), SecureStore, Expo push |
| AI | Anthropic API, Voyage voyage-3 embeddings (1024-dim) |
| Tests | pytest 9 + pytest-asyncio, Vitest 2 + Testing Library, jest-expo |
Shipped: a working three-platform system covering 20 back-office domains, with realtime notifications, offline-capable mobile stock-taking, role- and capability-gated access, a bilingual UI, and an AI assistant over the company knowledge base. All of it built, tested, hardened, and delivered by one engineer in roughly three weeks.
The source is private under client agreement. This case study is published with identifying details removed: the client is genericized, no code is excerpted, no internal documents are referenced, and security work is described only in aggregate. Any screenshots added later will be fresh, anonymized captures — not client-branded assets.