PR Card Renewal Platform
PR card renewal with validated uploads, shared case state, and a staff triage queue.
- Role
- Full Stack Development
- Type
- Applicant + caseworker product
- Client
- PR Card Renewal
- Status
- Live

Outcome
Replaced email-and-spreadsheet handoffs with a state machine, per-file upload checks, and a queue that does not download the full case until drill-in.
Applicants faced unclear steps, manual document checks, and delayed status. Support volume grew because nobody shared a case state. Caseworkers needed faster review without loading every attachment on the list view.
Explicit application states; APIs only allow valid transitions. Validation is centralized so incomplete uploads and wrong MIME types fail before persistence. The applicant UI renders from the same status model the admin queue uses.
- Frontend
- React SPA: wizard for applicants, queue/detail for caseworkers.
- Backend
- Node HTTP API. Transition rules and auth live in services, not in the client.
- APIs
- REST split by applicant vs admin scope. Stable error codes for the SPA.
- Databases
- MongoDB documents per case. Indexes aligned to triage (status + updatedAt). Blobs referenced, not embedded.
- Auth
- Admin routes guarded; applicant routes scoped to the owning case.
- Deployment
- Coordinated deploys; no public v1/v2 URL versioning.
- Multi-step wizard with persisted step state; the server rejects advances when required fields or files are missing
- Multipart uploads with type/size checks; metadata stored separately from blobs
- Service-layer transitions — not scattered UI flags
- Admin list filters/sorts (status, age) without embedding full case payloads
- Touch-first layout on long forms
Challenge
Users uploaded incomplete or incorrect documents.
Decision
Reject at upload and again at submit, with errors keyed to fields.
Implementation
Per-file validation on multipart plus a pre-submit aggregate check. The client highlights gaps without a round trip per file.
Result
Invalid packets never become working cases. Caseworkers are not the first MIME/size check.
Challenge
Support volume increased because status was ambiguous.
Decision
One case-state model for applicant and staff.
Implementation
Applicant progress UI reads the API status contract. Copy cannot drift from what staff see.
Result
Fewer 'what is happening with my file?' loops when state is unambiguous. No invented reduction percentage.
Challenge
Caseworkers needed faster review cycles.
Decision
Shape list endpoints for triage; load the graph on drill-in only.
Implementation
Filtered, paginated admin APIs. Queue rows stay slim (on the order of 10× less JSON than embedding full applications — from payload shape, not a timed A/B).
Result
Queue path stays usable as volume grows. Tradeoff: more endpoints versus one fat list response.
MongoDB documents per case
Checklist, files, and status history vary in shape; a document per case maps the workflow.
Alternative. Normalized SQL tables per step.
Tradeoff. Weaker relational constraints; indexes and service-layer transitions have to enforce what FKs would.
REST with implicit versioning
Applicant and admin ship together; a public /v2 was unnecessary.
Alternative. Versioned public API.
Tradeoff. Breaking changes require coordinated deploys instead of leaving old clients on v1.
- BuiltApplicant wizard and admin triage UI
- BuiltREST handlers for applications, uploads, and admin actions
- BuiltCase state transitions and upload validation
- Client owns program policy and production operations.
Invalid documents fail before they occupy a caseworker. Applicant and staff share a status contract. Queue payloads stay small by not embedding full cases. No third-party conversion metrics to cite.