Skip to main content

Home · Projects · Web Development

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
ReactNode.jsMongoDBTailwind
PR Card Renewal Platform — applicant renewal workflow

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.

Problem

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.

Solution

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.

Architecture
Frontend
React + TailwindApplicant wizardAdmin triage console
API
Node.js RESTMultipart validationAuth on admin routes
Database
MongoDB (cases · history)Files by ID — not embedded
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.
Key features
  • 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
Engineering challenges

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.

Technology decisions

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.

My ownership
  • 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.
Results

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.