Emran K.

HomeProjectsAboutExperience

Contact

Emran K.

Live at crowd-automation.vercel.app

Crowd Automation — Enterprise Operations Platform

Crowd Automation is an operations platform for companies. A company registers and gets its own environment on its own subdomain, with attendance, structured reporting and KPI tracking, plus an AI layer that reads that data and reports where the organisation is on track and where it is not. It ships as two clients on one backend: a web app where admins run the organisation, and a mobile app staff use for location-tracked attendance and notifications. Underneath, tenants are separated by Postgres row-level security rather than by application code, and the backend is a modular monolith whose boundaries are checked in CI.

Crowd Automation landing page

My Key Contributions

Tenant Isolation Enforced by the Database

Tenant data is separated in four layers instead of one: repositories scope every query, Postgres row-level security is forced on and reads the tenant id set inside the transaction so a missing value fails the query rather than returning everything, and foreign keys are composite on (tenant_id, id) so a row cannot point at another tenant's row. CI fails the build on any table without RLS, and a cross-tenant attack suite runs on every commit.

PostgreSQL RLSComposite FKsNOBYPASSRLS roleCI schema check

Modular Monolith with Checked Boundaries

The code is split into kernel, modules, custom and shared, and import-linter fails CI if a module reaches into another module's internals — modules depend only on kernel contracts. Those contracts import no FastAPI or SQLAlchemy, so pulling a module into its own service is a transport change rather than a rewrite.

import-linterstrict mypyFramework-free contracts

Events Through a Transactional Outbox

Domain events are written to an outbox table in the same transaction as the data, so there is no dual write and nothing is lost if a publish fails. A separate relay process dispatches them and arq workers on Redis run background jobs, so a message broker can replace the relay without touching the modules.

Outbox patternarqRedis 7Background workers

Auth Split Across Four Host Planes

Tenant, central identity, public and operator each run on their own plane, and tokens are bound to the plane they were issued for — an operator token carries no tenant claim, so no code path can quietly gain tenant authority. Access tokens are signed with Ed25519 and published through JWKS with key rotation, and permissions are resolved per request instead of being carried in the token, so a role change applies immediately.

Ed25519 / JWKSPlane-bound tokensPer-request permissions

Inside the Platform

A Subdomain per Company

A company signs up and gets its own environment on its own subdomain. Wildcard DNS and TLS mean no per-tenant setup, and the tenant is identified from the subdomain on every request.

Web App for Admins

Admins run the organisation from the browser: onboarding people and setting roles, drawing work sites as geofences on a map, setting KPI targets, and reviewing attendance and the reports teams submit on a schedule.

Mobile App for Staff

Staff check in from their phone and the check-in is matched against the work site geofence by device location, so attendance is tied to being on site. The app also carries notifications and queues writes while offline, replaying them without duplicates once it reconnects.

AI Reading the Operations Data

An AI layer reads attendance, reports and KPI history and summarises where the organisation is on track and where it is falling behind.

Company profile setup in Crowd Automation

Build, Release and Operations

One Image, Three Entrypoints

The API, the worker and the outbox relay run from the same immutable image with different entrypoints, on ECS Fargate. Migrations run pre-deploy, releases are tagged core-vX.Y.Z and the running version is exposed on /api/v1/meta, and a rollback is redeploying the previous tag.

Offline Writes from Mobile

The API is OpenAPI 3.1 across 126 paths and 160 operations, returns RFC 9457 problem+json errors, and takes an Idempotency-Key and client_event_id on writes, so a phone that was offline can replay its queue without duplicates.

Invariants Tied to Named Tests

CI runs ruff with bandit, strict mypy, import-linter, an OpenAPI completeness lint with a baseline diff and the RLS conformance check, then roughly 2,200 tests sharded with a dedicated database and Redis index per worker. A checker maps each architectural invariant to a named test and fails if that test disappears.

Client Forks That Stay Mergeable

Client-specific work lives only in custom/, and a CI parity gate keeps the kernel identical to a pinned upstream tag. Kernel fixes reach a fork through a git merge instead of being reimplemented in each one.

Operations Visibility

Logs are structured JSON in CloudWatch carrying tenant, correlation id, module and event, with per-tenant usage metering and alarms on outbox lag and RLS denials. Config comes from Secrets Manager and SSM, and a startup self-check fails fast if anything is missing.

Tech Stack

An async FastAPI backend on PostgreSQL and Redis, a React 19 web app and a React Native mobile app, all running on ECS Fargate from a single image with three entrypoints.

Core & API

Python 3.12FastAPIPydantic v2Fully async

Data & Isolation

PostgreSQL 16Row-Level SecurityJSONBSQLAlchemy 2.x

Async & Events

Redis 7arq workersTransactional outboxOutbox relay

Auth & Identity

EdDSA / Ed25519JWKS + kid rotationEmail OTPPer-device sessions

Web

React 19TypeScriptTanStack QueryTailwind v4Leaflet

Mobile

React Native 0.86Expo 57expo-routerexpo-location

AWS

ECR → ECS FargateALB + Route 53RDSElastiCacheS3SES

Quality Gates

ruff + banditimport-linterOpenAPI lint~2,200 tests
Attendance flow in Crowd Automation

Are you interested?

Let's work together on your project.

HomeProjectsAbout
GithubLinkedinXTelegram
Let's Connect and Discuss

© 2026