Case Study
Inside NearImpact: One Engineer, a Full Stack, and a $6 Server
How a final-year project became a live, self-hosted, two-sided platform — and what it taught its author about building and running software end to end.
Overview
NearImpact is a two-sided web platform that connects community organisations — NGOs, charities, universities and local groups — with the people who want to take part. Organisations publish structured opportunities; participants discover them by list or interactive map, apply, and track their application status. It was designed, built and is operated solo, and runs live in early access at nearimpact.site.
The Problem
Community opportunities are scattered across social feeds, mailing lists, bulletin boards and word of mouth. The people most in need of them — newcomers, those without established networks — are precisely the ones who never hear about them. Organisations, meanwhile, repeat the same manual outreach for every programme and struggle to see who actually engaged.
The Solution
One place to publish, discover and apply. Organisations manage a profile and post opportunities with structured fields; participants browse a searchable feed, explore a map of what is nearby, and apply through the platform with their status tracked from submission to decision.
A recommendation step surfaces relevant opportunities per participant, and full-text search makes the catalogue navigable as it grows. Everything sits behind a single sign-on so the same identity works across the app, its API and the admin tools.
Architecture
↓ HTTPS
↓ ↓ ↓
↓ backing services
5 Docker containers on one $6 droplet · Caddy TLS · Supabase, OpenAI & Google are external · GitHub Actions typecheck + build on push
Every request enters through Caddy, which terminates TLS and routes three subdomains — the app, the API and the auth server — to their containers. The Next.js front end never touches the database directly: it calls a separate Express API with a bearer token, over a consistent {data, error} contract. Authentication is delegated to a self-hosted Keycloak (OIDC), which keeps its users in its own PostgreSQL; the API verifies every access token against Keycloak's JWKS — checking issuer and audience — and the caller's role before doing any work. Application data lives in managed Supabase (PostgreSQL + Storage), reached with a service-role key, and an optional OpenAI call powers recommendations.
Engineering Challenges
One trustworthy authorisation boundary. Rather than spread access rules across the database with row-level security, the platform enforces them in one place — middleware that verifies the Keycloak JWT and the user's role. It is easier to audit and reason about, a decision I am happy to defend.
A language model that cannot run up a bill. Recommendations are optional. An OpenAI gpt-4o-mini call ranks up to five opportunities per participant and caches the result for an hour; the moment a key is missing or the API is down, it falls back to the most recently posted listings. Nothing breaks, and inference cost stays negligible.
Search that survives growth. Listings are indexed with PostgreSQL trigram (pg_trgm) full-text search, so the catalogue stays navigable as it fills, without bolting on a separate search service.
Operating it, not just shipping it. Five services had to start, talk to each other, obtain certificates and survive a redeploy — solved with Docker Compose, a Caddy reverse proxy that issues its own TLS, and continuous integration on every push.
Lessons Learned
Owning the operations is where the real learning lives — watching how the parts behave together in production taught me more than any single feature did.
A single, well-guarded boundary beats clever rules scattered everywhere; future me has to read this code.
Treat a language model like any external dependency: cap its cost, validate its output, and have a fallback. And write the deployment down — the runbook in the repo is as much a deliverable as the code.