Skip to content
Built 2020, rebuilt 2026a scholarship programme

Rebuilding the trust model of a scholarship platform

A multi-step application platform with payments, e-signatures and staff review — and a ground-up rebuild of a security model that originally trusted the browser.

9
classes of privilege escalation and tampering closed
0
authorisation decisions left to the browser
100%
changes verified against the Firebase emulator

Who counts as an administrator, who qualifies for a scholarship, who has been admitted and what has been paid are no longer things a client can forge. The charge amount is computed from the programme cost rather than accepted from the browser. Each fix was verified against the emulator suite before it shipped, and the residual risks that remain are written down rather than quietly hoped about.

The problem

Applicants register, complete a long application across personal, academic, family and essay sections, gather recommendations, sign forms electronically and pay fees. Staff review, score and admit. It worked, and it was one of the first real systems I built.

Coming back to it years later, the uncomfortable part was obvious: like a great many first Firebase projects, it trusted the browser for everything that mattered. Applicant records and payment history were world-readable. Cloud Function endpoints — including create-admin, delete-user and change-any-user's-email — were world-callable. The charge amount was sent by the client, so anyone could pay a cent. Eligibility, review scores and payment flags were all values the applicant could write themselves.

None of that is unusual in an early project. Publishing the fix, in detail, with the parts that are still not enforced listed honestly, is less usual.

The approach

Every authorisation decision moved server-side. Roles became Firebase custom claims verified in both security rules and functions, rather than booleans on a user document that the user could edit.

Money stopped being something the browser could describe. The charge is derived server-side from the programme cost; the client only names which payment it is making. Payment confirmation arrives through a signature-verified Stripe webhook written with the Admin SDK, not from the browser reporting its own success.

Derived facts became genuinely derived. Firestore triggers recompute scholarship eligibility and admission scoring from raw inputs, overwriting whatever the client submitted, so neither is self-declarable.

Every change was verified against the Firebase Emulator Suite, with the tests recorded in the commit messages — including one subtle recursive-wildcard rule leak that allowed parent-document writes and had to be closed separately.

What it does

Roles as custom claims

Enforced in rules and functions. Self-promotion to admin was previously one PATCH request.

Server-derived charge amounts

The browser names the payment; the server decides what it costs.

Signature-verified Stripe webhooks

Payment status recorded by the Admin SDK, never writable by the applicant.

Triggers for derived state

Eligibility and admission scores recomputed server-side from raw sub-scores.

Field-level write rules

Applicants can no longer edit their own review scores or delete their registration.

Documented residual risk

The write-up states what is still not enforced, because that is the interesting part.

Built with

  • React
  • Redux
  • Firebase Auth
  • Cloud Firestore
  • Cloud Functions
  • Firebase Emulator Suite
  • Stripe
  • SignNow
  • SendGrid

Recognise the problem?

First conversation is free, and ends with an honest answer about whether custom software is the right call.