CampusGist

Type
Product
Role
Solo: design, build, deploy
Year
2026
Stack
Python, Flask, PostgreSQL, JavaScript, Tailwind, Alpine.js, Paystack, Cloudinary, Cloudflare, Hetzner, Coolify
Live site
Visit site

A white-label student platform for Nigerian universities, starting with RSU.

Where the idea came from

The idea began in my first year, at 100 level, with two friends. We tried to build it then and it didn't work out. Later I came back to it and built it myself, on my own, from the idea we'd shared at the start.

The idea is a place of students' own online for the things they need day to day: finding past questions and study material, hearing what's happening on campus, asking questions without everyone knowing who asked, and finding who sells what nearby. CampusGist is a mobile-first web app, installable as a PWA, built for a cheap Android phone on mobile data. It's a campus utility first and a social layer second. It isn't run by or affiliated with the university.

What I built

A Flask and PostgreSQL backend with a front end of plain pages, Alpine.js and Tailwind. There's no front-end build step and no framework, and a small router script loads each screen. The main parts are:

  • Town Square. A posts feed with comments, likes, polls and search.
  • The Gist Board. An anonymous board where students post under a random alias such as "Silent Tortoise".
  • The Library. Study material that students can browse, search and download.
  • Events and announcements, with web push notifications and live in-app notifications.
  • A merchant directory for campus businesses, with Paystack payments for listings and for a Pro subscription.
  • Student verification. A student uploads a school fee receipt and staff approve or reject it from an admin queue.
  • Moderation and admin tools, including per-school settings and an audit log.

I worked with AI coding agents throughout. The repo carries a blueprint and per-feature design documents that I used to direct them. I made the architecture calls described below, and I tested and debugged the result, including against the production server. The repo has about 365 commits between June and August 2026 and roughly 490 test functions.

Decisions I made

  • One codebase, many schools. It started hardwired to RSU. Partway through I changed it so the school comes from the request's Host header and every table carries an instance_id. RSU is just the first instance, at rsu.campusgist.ng. A new school means a new row and a new subdomain, not a new deployment. It was a data-model change, not a UI change, and it meant redoing decisions I'd already made.
  • Money in kobo. Prices are stored as integers in kobo (naira times 100), so there's no floating-point arithmetic near payments. Paystack webhooks are logged as raw events and processed by reference.
  • Search without a search server. Posts, users, library items and merchants each have a tsvector column that a Postgres trigger keeps up to date, with a GIN index on it. Search is an ordinary query against PostgreSQL.
  • Live notifications over SSE, and the bug that came with it. The notification stream uses Server-Sent Events. Under load, ordinary Gunicorn workers each got tied up holding a stream, the database showed connections stuck "idle in transaction", and some requests started failing with 503s. I moved the app to Gunicorn with gevent workers, patched the Postgres driver to cooperate and fixed the order of a rollback in the stream handler. After that, 20 concurrent streams held open for over seven minutes with none stuck.
  • Push notifications that silently never sent. I found they were never dispatched at all: an after_commit hook was crashing without any error showing. The service worker also had to be served from the site root to have the scope it needed. Fixing those two things made push work.
  • Hosting. It began on Render's free tier, hit the usage limit at the end of June 2026 and moved to a Hetzner server running Coolify, with Let's Encrypt certificates through Traefik, Cloudflare in front and Postgres in a container.

The Gist Board

Posts on the Gist Board show no author, and each post and each comment gets a fresh random alias, so the same person doesn't appear under one name across posts. A database constraint enforces it: a gist must have no author id on its row, and a normal post must have one. Only verified students can post.

The author is stored encrypted, in a separate table from the post, using Postgres pgcrypto and a key kept in the server's environment. The app has no feature that reveals it: there is no screen, endpoint or moderator tool that decrypts it, and moderators can delete a gist but can't ban its poster. The record exists so that serious abuse can be dealt with if I'm ever legally required to. Only someone holding both the database and the key could read it.

Privacy measures I built

  • No phone numbers. Accounts hold an email and a display name; the user model has no phone field.
  • Receipts don't stay. A fee receipt is deleted from storage as soon as staff decide on it (approve, reject or ban), and the record is marked as shredded.
  • Account deletion that wipes. A deletion request starts a 30-day hold. After that a scheduled job deletes the avatar and cover photo, detaches the person's comments, removes their push subscription, blanks their personal fields, and writes an audit-log row.
  • Consent recorded. Sign-up stores when the terms were accepted and when the person confirmed their age.
  • Encrypted anonymous authorship, as described above.

Where it stands

It's in private beta with friends, family and testers at rsu.campusgist.ng. It hasn't launched school-wide.

The next step is people using it: getting teachers to post material before the semester so the first thing a student sees isn't an empty app.

CampusGist's Town Square feed on desktop, showing student posts and campus polls
The anonymous Gist Board, where each post shows a random alias instead of a name
The anonymous Gist Board, where each post shows a random alias instead of a name
The Events page listing campus events, with one event's details open beside the list
The Events page listing campus events, with one event's details open beside the list
Official announcements from university departments in CampusGist
Official announcements from university departments in CampusGist