← back to all work

Application portal & college integration

After a student pays, they have to be enrolled with the partner college. That was email, chasing and re-keying by hand. I replaced it with a portal the student completes, synced into the college's systems, with every step tracked and chased automatically.

RoleArchitect and lead engineer
TimelineJul 2026 to present
StatusIn production
ContextBuilt at Oneday, an online education company selling degree programmes to US founders. One funnel, seven stages, five teams. I own the revenue numbers for the business and write the software that runs it. Enrolment is the last stage of the funnel and the one where a paid customer can still fall out.
TypeScript on DenoSupabase Postgres + edge functionspg_cronAirtableBitrix24 CRMTransactional emailSMSCollege records integration
1,183students onboarded · since 27 Jul 2026
6.7% → 11.5%deals won within 5 days of booking · p = 0.03
9.3 → 7.9 daysmedian from booking to won
27.5 → 17 daysmedian from win to financial-aid confirmation

01 — The problemA paid customer, a shared inbox, and no status.

Once a student paid, a staff member emailed them a list of documents. The student emailed some back. Someone re-keyed the details into the college's system. Missing items were discovered at the enrolment deadline, when it was too late, and nobody could answer "where is this application?" without opening three tools.

Deferrals and drop-outs at this stage were a direct revenue leak, after acquisition cost had already been spent.

02 — What I builtA portal for the student, a board for staff, a sync to the college.

StudentApplication portalThe student signs in from a link in their confirmation, uploads documents, completes each step and sees exactly what is left. Access is minted automatically when a booking or payment event arrives.
StaffOne boardEvery open application and what it is waiting on, in one view. Staff handle exceptions instead of admin.
IntegrationCollege syncApplication and enrolment records move between our systems and the college's without re-keying. The record identity follows the deal owner so hand-offs do not break it.
EngineState and chasingA state machine reads the real stage from the CRM and the real completion from the portal, then sends the right chase by text and email: missing documents, transcript evaluation, payment steps, day-one actions. Each chase anchors on an event the engine observed, never on a date someone typed.
EngineOnboarding wavesOnce a cohort is confirmed, day-one instructions go out in waves by email and text, with per-message delivery tracking.

03 — How it worksEverything hangs off one gate: has the deal actually been won.

  1. A booking or payment event arrives from the calendar or the CRM and mints portal access for that student.
  2. Each portal step writes completion to the operations database, which is what staff and the chasers read.
  3. The engine ticks on a schedule, compares the stage in the CRM against portal completion, and queues the next chase, capped per run and resumed by cursor.
  4. Post-admission steps, including the financial check, unlock only when the CRM stage says won. Not from a timestamp field, which turned out to be set at row creation.
  5. Records sync to the college on completion, and the status flows back.

04 — What brokeMostly: trusting a field to mean what its name said.

"Deal won time" was not the winA timestamp named for the win was stamped at row creation. Every row carried it, including students at admit stage. The post-admission financial check appeared for a student who had not won.
Gate on the stage, not the stampEvery student-facing send and unlock keys on the stage value. A ledger marker records the first tick the engine saw the won stage, since no field records when the win happened.
A sync that never advancedThe sync resumed from the newest synced timestamp. A budget-capped run left unsynced rows above that point, so the watermark stuck and every run re-did the same prefix.
A forward-only cursorThe run persists the last item it processed. Only unparameterised runs may move the cursor, so a targeted re-run cannot skip the backlog.
Five silent daysThe CRM rate-limited with a 503, the retry ladder only caught 429, and the sync died before any state write. The scheduler reported success on every tick.
Retry both, and check the response503 is retried patiently. Scheduled calls are now collected with their real response status, so a function that fails after the 5-second accept window shows as failed.
A door left openTwo database functions were callable by anonymous clients for about two months because the default grant was never revoked.
Revoke, then verifyEach privileged function is locked down explicitly and verified with a privilege check in the migration.

05 — What I'd do differentlyHold out a control group from the start.

The portal results are measured on before-and-after cohorts. Wins within five days of booking went from 6.7% to 11.5%, and the time from win to financial-aid confirmation fell by ten days, but other things changed in the same period, including a deposit policy. I report them as descriptive, not causal. A randomised hold-out in the first month would have settled the question cheaply. I would also instrument staff time saved on day one; it is the number everyone asks for and the one I cannot give.