Sprintern
An internship discovery platform with real users. It polls community-maintained job repositories, normalises and deduplicates what it finds, matches postings against each user's filters, and delivers them as instant Telegram alerts or a curated daily email digest. Users track applications from saved through to offer, and administrators can add, preview or disable a source without touching application code.

What needed to change.
Internship postings are scattered across community-maintained repositories that each use a different table format, and the same role frequently appears in several of them at once.
The system I chose to build.
An ingestion pipeline that polls each source with conditional requests, tolerates schema drift, and merges duplicates on a canonical fingerprint. Matching runs against user-defined filters and fans out to Telegram and email through a Postgres-backed job queue with idempotency keys and leased claims. Row-level security in the database sits behind the API's own ownership checks, so a mistake in one layer still cannot expose another user's data.
My role: Solo. Ingestion pipeline, REST API, background workers, database security model and frontend.
Under the surface.
The interesting parts rarely stay simple.
- The same posting shows up across several repositories, so deduplication had to merge on company, title, location and term without collapsing two genuinely different roles at one employer.
- Upstream repositories change their table format without warning, so every row is validated and rejects are surfaced for review rather than failing quietly.
- Alerts must never double-send, which pushed the queue toward idempotency keys, leased claims and exponential backoff.
What came out the other side.
In production at sprintern.ca with real users, aggregating nine sources into a single feed of 1,000+ live postings.
What I would improve next.
- Expand ingestion beyond GitHub-hosted sources.
- Surface application deadlines more prominently in the daily digest.