OOOlorede.
Back to Projects
FrontendAugust 2024

FaithCare Dashboard

Dual-persona church management SPA with JWT auth, React Query, and real-time member tracking.

React 18TypeScriptViteTailwind CSS 4Radix UIshadcn/uiTanStack Query v5React Router v7React Hook FormZodRechartsFramer Motioncmdk
FaithCare Dashboard
Overview

FaithCare is a church operations platform built as a React SPA that serves two distinct user personas: individual church members and organization administrators. Individual users get a personal spiritual growth suite — a Sunday sermon journal with scripture references, a server-persisted Pomodoro focus timer, and streak tracking. Organization admins get a full membership operations dashboard covering first-timer and second-timer tracking, prioritized follow-up management, community groups, salvation records, prayer request queues, and bulk CSV/Excel imports.

Architecture

The app is a Vite-bundled React SPA deployed to Vercel with a catch-all rewrite routing all paths to index.html. The short-lived JWT access token lives exclusively in a JavaScript module-scoped variable (never localStorage), providing XSS resistance. The long-lived refresh token is stored in an HTTP-only cookie. On any 401, a single-flight refresh mechanism blocks concurrent calls while queued requests are held in a subscriber array, then replayed with the new token once resolved. Data fetching uses TanStack Query v5 with query keys namespaced by organizationId and userId to prevent cross-tenant cache hits. The global cmdk command palette queries four API endpoints in parallel on open, and a SearchContext filters already-cached data client-side with zero per-keystroke API calls.

Technical Decisions
01

Auth Security

Chose

In-memory JWT access token with HTTP-only cookie refresh token

Why

Storing access tokens in localStorage exposes them to any XSS script on the page. Keeping the access token in a module-scoped variable means it is never accessible from the DOM. The HTTP-only cookie carries the refresh token so the browser never exposes it to JavaScript at all.

Trade-off

The in-memory token is lost on hard refresh, requiring a /auth/refresh call on every page load — one extra network round-trip before the app renders protected content.

02

Token Refresh

Chose

Single-flight token refresh with subscriber queue in the shared apiRequest wrapper

Why

When an access token expires, multiple in-flight requests can simultaneously receive a 401. Without coordination, each would independently try to refresh, causing race conditions and potentially invalidating a fresh token with a redundant refresh call.

Trade-off

The subscriber queue adds statefulness to what would otherwise be a pure fetch wrapper. If the refresh itself fails, all queued requests receive the original 401 and must handle it individually.

03

Component Library

Chose

shadcn/ui + Radix UI primitives over MUI

Why

shadcn/ui generates unstyled, composable Radix primitives into the project source, giving full ownership over markup and styles. MUI was evaluated and rejected because its Emotion-based styling system would have required fighting the library to achieve the required visual output.

Trade-off

Every primitive (Dialog, Select, Command, Sheet) required manual composition and accessible wiring, increasing initial build time compared to a fully-styled library.

04

Search Architecture

Chose

Client-side global search via SearchContext filtering already-cached query data

Why

The main list views are already fetched by TanStack Query on page load and held in cache. Wiring search to server-side API endpoints would add debounced network calls on every keystroke and require managing additional loading states in the UI.

Trade-off

Search is bounded by what is already in the client cache. For organizations with very large member datasets where the API paginates results, the search will only match visible records.

05

Persona Routing

Chose

localStorage userType flag for persona routing instead of server-enforced role guards

Why

The backend returns a role field on the auth response. Reading this into localStorage as a derived userType string at login time was the fastest path to rendering the correct sidebar and dashboard without adding a dedicated roles endpoint call.

Trade-off

The userType in localStorage can be tampered with client-side, which could expose organization UI to individual users. All API endpoints enforce authorization server-side, so the risk is a confused UI state, not a data breach.

Future Improvements

Replace localStorage userType persona routing with server-side role claims read from the decoded JWT, removing the client-tampering surface entirely

Add server-side paginated search with debouncing for first-timers and follow-ups to handle large church datasets beyond what fits in a single page response

Implement WebSocket or SSE for real-time follow-up due-date alerts instead of requiring a manual page refresh to see updates from other admins

Extract the token-refresh subscriber queue into a standalone service worker or broadcast channel so multiple tabs share a single refresh cycle