Building an Operating System for a Movement: The Engineering Journey Behind Campus Revival
By Olabode Olusegun (Lead Engineer) | June 2026 | Status: V3 Live in Production
Every software project starts with a delusion. For me, it was thinking that the Campus Revival Movement (what we internally call CRM-Platform) would just be a "simple website."
The premise was straightforward: create a map of UK university campuses and let users click a button to say, "I'm praying for this campus." A classic CRUD app. Spin up Next.js, add a database, throw some Tailwind on the frontend, and call it a weekend.
But as the movement grew, the requirements mutated. A simple map turned into a nationwide coordination platform. "Praying for a campus" evolved into an adoption workflow involving leader verification, hierarchical workspaces, team permissions, and real-time activity feeds. Suddenly, we needed multi-factor authentication (MFA), role-based access control (RBAC), rate limiting, event-driven background jobs, and a comprehensive search architecture.
Over six months, the project morphed from a brochure site into a complete Operating System for a movement. Today, the platform supports over 250 verified campuses and ~4,000 active registered users.
This is the story of how that happened. It is an honest, deeply technical look at the architectural decisions, the painful production incidents, and the hidden engineering work required to build a system capable of scaling without collapsing under its own weight.
1. The Original Problem & The Stakes
The Campus Revival Movement needed a digital home. The initial requirement was to give visibility to the movement: show which universities were being prayed for, who was leading the charge, and what was happening on the ground.
At first glance, a Content Management System (CMS) like WordPress or Strapi seemed sufficient. However, a CMS models content, not behavior. CRM-Platform wasn’t about publishing articles; it was about facilitating workflows.
I also considered off-the-shelf Customer Relationship Management (CRM) tools like Salesforce or HubSpot. (Yes, the acronym collision between Campus Revival Movement and Customer Relationship Management haunted us from day one). But trying to shoehorn deeply specific workflows—like "spiritual adoption," "journal entries," and "prayer requests"—into a generic "Lead to Opportunity" sales pipeline would result in a brittle, unmaintainable mess.
We needed a bespoke system because the stakes were uniquely high. When a campus hits the "Revival Threshold" (50 registered adopters), it isn't just a gamification metric—it triggers real-world logistics. The core movement leadership team physically visits that campus to host an event. If our data was wrong, we would be sending a team across the country based on a ghost metric. The domain model had to perfectly mirror the reality of the movement.
2. The Scope Creep: From Website to Operating System
The evolution from website to operating system didn't happen overnight.
- Month 1: Just a map and user accounts.
- Month 2: Users need to submit journal entries and prayer requests, but only for campuses they've adopted.
- Month 3: Campus leaders need to verify themselves to prevent misrepresentation and fraud. They need administrative control over their campus page.
- Month 4: We need a global activity feed to show momentum.
As the requirements compounded, the initial architecture—a messy tangle of Next.js Server Actions calling the database directly—began to crack. I found myself writing the same authorization checks in five different places. The UI became sluggish; p95 latency was hovering around ~3.1–3.4 seconds because single requests were doing too much synchronous work.
3. The Architecture Evolution
To prevent the codebase from becoming an unmaintainable "Big Ball of Mud," I had to draw hard boundaries.
Why Vertical Slice Architecture?
Initially, I organized the code by technical concerns: components/, api/, models/. When I needed to add a feature to the "Adoption" workflow, I had to touch five different folders.
I transitioned towards a Vertical Slice Architecture. Instead of organizing by technology, I organized by feature. Everything related to "Campus Adoption"—the database model, the business logic, the API route, and the DTOs—lived together.
graph TD
subgraph Layered Architecture (The Old Way)
Controllers --> Services
Services --> Repositories
Repositories --> DB[(Database)]
end
subgraph Vertical Slice (The New Way)
FeatureA[Campus Adoption] --> DB2[(Database)]
FeatureB[Prayer Requests] --> DB2
FeatureC[Auth] --> DB2
endThe Repository Pattern
I chose MongoDB because the schema for "activity feeds" and "journal entries" was highly polymorphic in the early days. However, I made a critical rule: No Next.js API route or Server Component was allowed to import Mongoose models directly.
Instead, I implemented the Repository Pattern. By wrapping database calls in a Repository interface, I decoupled the business logic from the persistence layer, making unit testing actually feasible.
4. Auth, Privacy, and Capability-Based RBAC
Authentication and privacy are where faith-based platforms face unique challenges. Prayer requests and journal entries are deeply personal, sensitive data. Furthermore, verifying real campus leaders is critical to prevent spiritual misrepresentation on a campus.
Capability-Based Permissions (Not Roles)
The biggest mistake I made early on was role-based checks (if (user.role === 'ADMIN')). The moment we needed a CAMPUS_LEADER role, the entire codebase broke.
I refactored the system to use Capability-based permissions. The API doesn't ask "Are you an admin?" It asks, "Do you have the capability to delete this journal entry?"
// The old way (Fragile)
if (user.role !== 'ADMIN' && entry.authorId !== user.id) {
throw new UnauthorizedError();
}
// The new way (Robust)
const capabilities = getCapabilitiesForUser(user, entry);
if (!capabilities.canDeleteJournal) {
throw new UnauthorizedError('Missing required capability: canDeleteJournal');
}Data Privacy & GDPR
Because we operate in the UK, GDPR compliance was non-negotiable.
- Retention: Prayer requests are auto-deleted after 90 days via a background cron job.
- Encryption: Highly sensitive journal entries are encrypted at rest using AES-256-GCM. Even the database administrator cannot read them. They are only decrypted just-in-time at the application layer if
capabilities.canReadJournalevaluates to true. - Right to Erasure: We built a dedicated background workflow for user-initiated account deletions. When a user requests erasure, an Inngest worker scrubs their PII, destroys their encryption keys (crypto-shredding their journal entries), and anonymizes their public activity log so that aggregate campus metrics aren't corrupted by the deletion.
5. Background Jobs: Entering the Event Bus
When a user adopted a campus, the system had to:
- Update the database.
- Check if the campus hit the 50-adopter Revival Threshold.
- Generate an Activity Timeline event.
- Send an email to the campus leader.
Doing this synchronously took well over 3 seconds. It was fragile; if the email provider failed, the user saw an error.
I integrated Inngest to build an Event Bus directly into the Next.js application.
// The API Route just fires an event and immediately returns
await inngest.send({
name: "campus/adopted",
data: { userId, schoolId }
});
return Response.json({ success: true });This decoupled architecture dropped our p95 latency from ~3.1–3.4s down to a steady 100–150ms.
6. The Command Palette & Search Architecture
A movement operating system needs to be fast. I realized that users were spending too much time clicking through menus to find their university or specific users.
I built a global Command Palette (Cmd + K) powered by a robust Search Provider architecture.
Rather than writing a monolithic, terrifying MongoDB aggregation pipeline that searched everything at once, I built independent Search Providers.
graph LR
User[Command Palette] --> SearchAggregator[Search Aggregator]
SearchAggregator --> ProviderA[School Provider]
SearchAggregator --> ProviderB[User Provider]
SearchAggregator --> ProviderC[Journal Provider]
ProviderA --> Mongo
ProviderB --> Mongo
ProviderC --> MongoEach provider implements a simple interface executeSearch(query). The aggregator fires them all in parallel using Promise.allSettled, merges the results, and normalizes them into a standard DTO for the frontend. Adding a new search domain (e.g., "Sermons") is now as simple as registering a new provider class.
7. The 2AM Incident: Phusion Passenger & The Memory Leak
Here is where the "modern frontend" dream crashed into reality.
Vercel is amazing, but with heavy database polling and edge functions, our projected Vercel bill at scale was north of $150/mo. The movement had a tight budget. We acquired an 8-core, 16GB RAM Namecheap VPS (cPanel) for ~$25/mo.
Deploying a modern Next.js App Router application to a traditional cPanel/VPS environment using Phusion Passenger is brutal.
The Incident: June 12th, 2:15 AM.
Sentry triggered a critical alert: the Next.js production server was repeatedly restarting with FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory.
The Cause: Namecheap's Phusion Passenger manages Node processes aggressively. When our cron job hit an endpoint to generate the global activity feed, it triggered a massive, un-paginated MongoDB $lookup aggregation joining 4 collections. Because Passenger caches memory per-thread, this single request bloated the Node process heap to 1.5GB, crashing the instance and taking the entire site offline.
The Fix:
- I SSH'd into the VPS and manually restarted the Passenger instance using
touch tmp/restart.txtto restore immediate service. - I ripped out the MongoDB
$lookuppipeline entirely. We denormalized the Activity Feed schema so reads were flat (O(1)index scans). - We offloaded all heavy aggregation tasks to Inngest background workers, ensuring the main Next.js request threads were never blocked by heavy compute.
We haven't had an OOM crash since.
8. Testing, CI, and Observability
You can't maintain an operating system without a safety net.
The CI Pipeline
Every Pull Request to main triggers our GitHub Actions pipeline (ci.yml).
- Vitest runs our unit and integration tests. We spin up an ephemeral
mongo:6.0service container in the runner so the Repository layer is tested against a real database, not mocks. - Playwright runs End-to-End (E2E) tests. It boots the Next.js dev server, seeds the ephemeral database, and visually navigates the Adoption and Login workflows.
If Playwright fails, the deploy is blocked. This exact pipeline caught a bug where our JWT signing secret validation was failing in edge environments before it reached production.
Observability & Rate Limiting
Because we manage our own VPS, we lack Vercel's beautiful dashboard.
- Sentry captures all unhandled exceptions and React boundary errors, piping them directly to our developer Slack.
- Upstash Redis provides telemetry and strict rate-limiting. To prevent brute-force attacks and abuse, we implemented a global
100 requests / 15 minuteslimit per IP, with a much stricter5 requests / 15 minuteslimit on the/api/auth/loginand MFA endpoints.
9. What Building Campus Revival Taught Me
Building the Campus Revival Movement Operating System was a masterclass in pragmatism.
When you read engineering blogs from Stripe or GitHub, it’s easy to get seduced by microservices and Kafka. But when you are a solo engineer or a small team building a system from scratch, those technologies are a death sentence.
The biggest lesson I learned is the Power of Bounded Complexity.
I didn't use microservices, but I strictly separated my modules using Vertical Slice Architecture. I didn't use Kafka, but I achieved resilient background processing using Inngest. I didn't use an Enterprise API Gateway, but I secured my routes with comprehensive Middleware and Redis rate limiting.
Good architecture is about delaying decisions until you absolutely have to make them, and building interfaces that allow you to change your mind later without rewriting the world.
Appendix
A. Tech Stack
- Frontend: Next.js 14 (App Router), React, TailwindCSS
- Backend: Node.js, Next.js Server Actions & API Routes
- Database: MongoDB (Atlas) via Mongoose
- Caching & Rate Limiting: Upstash Redis
- Background Jobs: Inngest
- CI/Testing: GitHub Actions, Vitest, Playwright
- Deployment: Namecheap cPanel VPS (Next.js Standalone mode)
- Observability: Sentry
B. Advice to Engineers Building Systems That Outgrow Their Scope
- Stop writing
if (role === 'admin'): Adopt capability-based permissions on day one. It saves 100 hours of refactoring later. - Never return raw DB objects: Enforce DTOs at the network boundary. You will accidentally leak a password hash if you don't.
- Embrace the Event Bus early: The moment a user action triggers more than one side effect, move it to a background job. Keep your main request thread lightning fast.
- Test against real databases in CI: Mocking a database hides your worst queries. Spin up a Docker container in GitHub Actions and test reality.