Case Study · Hart College

Custom Student Information System (SIS) for a Canadian Career College — Course Catalog, Multi-Intake Enrolment, Stripe Tuition & Coupon-Driven Admin (Next.js + Django + PostgreSQL)

How UnlockLive replaced three disconnected tools and one shared inbox with a single, custom Student Information System for a Canadian career college — a public Next.js course catalog, a student-facing enrolment + Stripe tuition portal, and a Django admin console for batches, courses, coupons, candidates, and document attachments. Net result: 100% of tuition collected online, 5x faster enrolment, and zero reconciliation spreadsheets.

  • IndustryHigher Education / Career-College SaaS
  • Year2024
  • CountryCanada
  • Duration5 months
Custom Student Information System (SIS) for a Canadian Career College — Course Catalog, Multi-Intake Enrolment, Stripe Tuition & Coupon-Driven Admin (Next.js + Django + PostgreSQL) hero screenshot

At-a-glance results

  • 100%Of tuition payments now collected online via Stripe (zero cheques / e-transfers)
  • 5xFaster enrolment workflow vs. the spreadsheet + inbox baseline
  • 0Reconciliation spreadsheets at month-end (SIS is the single source of truth)
  • 5Months from discovery to first live intake on the new SIS

The challenge

Hart College ran admissions on a public marketing site, course registration on a separate spreadsheet, tuition collection by cheque and e-transfer, and student communication through a shared inbox. Nothing reconciled. Admins re-keyed every new student into three places, the registrar had no single answer to 'who is enrolled in this intake?', and finance burned a full week each month chasing missing payments and matching them against names that didn't quite agree across systems.

The ask was deceptively simple: replace all of it with one Student Information System (SIS) where a prospect could discover a course on a public marketing page, enrol in a specific intake, pay tuition online, see their schedule and payment history in a self-serve portal — and an admin could create batches, courses, coupons, candidates, and attachments from a single dashboard with no parallel spreadsheets and no reconciliation work at month-end.

Our solution

We built a single, custom SIS in three coordinated surfaces: a public Next.js course catalog, a student-facing enrolment + tuition portal, and a Django + Django REST Framework admin console — all backed by PostgreSQL and a Stripe Payments + Webhooks integration that keeps the SIS as the single source of truth for every dollar.

The public site ships a fast, SEO-tuned Next.js catalog: each course has its own indexable detail page, intake schedule, prerequisites, tuition breakdown, and inline 'Enrol now' CTA — so the marketing site is also the top of the enrolment funnel, not a separate brochure. Once a prospect clicks through, they land in a Stripe Checkout session created server-side with the right amount, currency, coupon, and intake ID baked into the metadata.

On payment success, a Stripe webhook lands on the Django API and idempotently creates or updates the student, links them to the chosen batch, attaches the receipt, and emails confirmation — all in a single signed transaction. The student's portal then exposes 'My Courses', 'My Schedule', 'My Tuition' (with full payment + refund history) and a documents tab for transcripts, ID, and signed agreements.

The Django admin console gives the registrar one place to run the entire program: create a batch, attach courses and faculty, define coupon codes (percentage or flat, scoped per intake or per course), upload candidate records and attachments, push announcements, and pull enrolment + revenue reports without ever opening Excel. Roles separate registrar, finance, faculty, and admin so each user only sees the surface they need.

Finally, we wired Stripe webhooks idempotently, locked tuition records against a checksum so finance can prove the SIS and Stripe agree, instrumented every endpoint with Sentry + CloudWatch, and stored every signed PDF / ID upload in S3 with time-bounded signed URLs (no raw object access).

  • Public, SEO-tuned Next.js course catalog with one indexable page per course, intake schedule, and inline 'Enrol now' CTA
  • Stripe Checkout sessions created server-side with intake ID, coupon code, currency, and tuition breakdown baked into metadata
  • Idempotent Stripe webhooks — student, batch link, tuition row, receipt email all written in one signed transaction
  • Student portal with My Courses, My Schedule, My Tuition (full payment + refund history), and document uploads
  • Django admin console: batches, courses, coupons (percentage or flat, scoped per intake or course), candidates, attachments, announcements
  • Role-separated access for registrar, finance, faculty, and admin — each user only sees the surface they actually use
  • Tuition checksum reconciliation so finance can prove SIS and Stripe agree at any moment without opening Excel
  • S3 + signed time-bounded URLs for every transcript, ID, and signed agreement — no raw object access in the wild

How we built it

  1. 01

    Discovery, registrar shadowing, intake-aware data model

    We sat with the registrar and finance teams for a week and mapped every place a student record was created, edited, or paid for — across the marketing site, the registration spreadsheet, the bank, the email inbox, and the shared drive. Out of that came an intake-aware data model with first-class concepts of `Batch`, `Course`, `Coupon`, `Candidate`, and `Tuition` — instead of bolting student state onto a generic CRM.

  2. 02

    Public Next.js catalog + student portal + Stripe-first checkout

    We shipped a fast, SEO-tuned Next.js public catalog with one indexable page per course and inline 'Enrol now' CTAs that hand off to a server-created Stripe Checkout session with intake + coupon metadata baked in. The student-facing portal then surfaces schedule, payment history, receipts, and document uploads. The marketing site is the funnel, not a brochure.

  3. 03

    Django admin console + idempotent Stripe webhooks

    On the operations side, we shipped a Django + Django REST Framework admin: batch / course / coupon / candidate / attachment CRUD, role separation between registrar, finance, faculty, and admin, and an enrolment + revenue reports surface. Stripe webhooks land idempotently into a single Django endpoint that creates / updates the student, links them to the right batch, writes the tuition row, and emails confirmation in one signed transaction.

  4. 04

    Reconciliation, S3, observability, hand-off

    We locked tuition rows against a checksum so finance can prove the SIS and Stripe agree at any moment, stored every signed PDF / ID upload in S3 with time-bounded signed URLs, and instrumented every endpoint with Sentry + CloudWatch. The registrar got a runbook for opening a new intake, issuing a coupon, refunding a tuition payment, and exporting an enrolment report — and we stayed on retainer for enhancements.

Tech stack

  • Next.js (React)
  • Django
  • Django REST Framework
  • PostgreSQL
  • Stripe Payments + Webhooks
  • AWS S3 (document storage)
  • Sentry + CloudWatch
  • Docker
  • Custom Software Development
  • Higher-Education SaaS Engineering
  • Stripe Payment Integration
  • UI/UX Design
  • DevOps & Observability

Frequently asked questions

How do you reconcile tuition payments when a Stripe webhook lands twice or out of order?

Two safeguards. (1) The webhook handler is idempotent: every Stripe `event.id` is checked against an `processed_webhooks` table before any write, so a retry is a no-op. (2) Every tuition row stores a checksum derived from `(student_id, batch_id, amount, currency, coupon_code, stripe_payment_intent)`. Finance can run a one-line reconciliation query at any time to prove SIS rows and Stripe charges agree — no Excel, no human pairing, no end-of-month panic.

How is multi-intake enrolment modelled in the SIS?

We treat `Batch` as a first-class entity, not a tag on a student. A batch carries its own start date, capacity, courses, and faculty assignments. A student belongs to one or more batches; a coupon can be scoped per intake or per course; reports filter by batch in a single click. This is the difference between 'a CRM with an intake field' and a real SIS — and it's why the registrar no longer keeps a parallel spreadsheet.

Is the platform extensible to other career colleges or training providers?

Yes. The data model is institution-agnostic — `Batch`, `Course`, `Coupon`, `Candidate`, `Attachment`, `Tuition` are not Hart-specific. A second institution can be onboarded by re-skinning the Next.js front-end and provisioning a new Stripe account; the Django admin and the underlying schema are reusable. We've designed it so that extending into K-12, vocational training, and continuing-education programs is a configuration change, not a re-build.

How long did it take to build the Hart College SIS, and how was the team structured?

Five months end-to-end with a four-person team (1 product/PM, 2 full-stack engineers, 1 designer). Roughly: 3 weeks of discovery and registrar shadowing; 12 weeks of Django REST API + Next.js public catalog + student portal + admin console; 3 weeks of Stripe webhook hardening, S3 attachment work, observability, and registrar training; 2 weeks of pilot intake before opening the next live intake on the new SIS.

What stack powers the Hart College Student Information System?

A Next.js (React) public site and student/admin portals, a Django + Django REST Framework API, PostgreSQL for transactional data, Stripe Payments + Webhooks for tuition, AWS S3 for document attachments, and Sentry + CloudWatch for observability — all containerized with Docker and deployed via infrastructure-as-code.

Want a result like this?

Talk to the same team that built Custom Student Information System (SIS) for a Canadian Career College — Course Catalog, Multi-Intake Enrolment, Stripe Tuition & Coupon-Driven Admin (Next.js + Django + PostgreSQL). We’ll scope your project, give you a fixed-price proposal, and show you the closest analog from our portfolio.

Book a strategy call