What’s Next
Please
Supervisors and agents went from coordinating work across disconnected tools to a single platform where every task has an owner, a status, a time log, a comment thread, a file trail, and real-time notifications — whether the tab is open or not.
The Problem
No one knew
who owned what.
A single organization was running task delegation, client requests, and agent assignments through a combination of informal tools — no unified system for tracking who owns what, no visibility into task status across roles, and no audit trail for time spent or work completed.
Supervisors had no structured way to match tasks to agents by skill. Clients had no self-service portal to submit or track requests. There was no notification layer to alert anyone when state changed. The result: coordination overhead, missed deadlines, and no data to review performance.
Requirements I Gathered
Ask the right
questions first.
Before architecture was decided, these questions shaped the design:
-
Who are the actors and what can each do?
Surfaced 6 distinct roles: Super User, Task Supervisor, Task Agent, Client, District/Territory Manager, Account Executive — each needing a separate dashboard and permission set.
-
Who creates tasks, and who resolves them?
Tasks can originate from both internal users and clients. This distinction had to be captured in the schema:
creatorType,createdByUserIdvscreatedByClientId. -
How do we identify tasks externally?
Clients and agents need a human-readable reference, not a UUID. Led to a serial number system with per-category prefixes — e.g.
WD-00042. -
What does “real-time” mean here?
Notifications needed to update the UI without polling.
EventSourceincompatibility withhttpOnlycookies had to be resolved — which drove the single-use SSE token design. -
What’s the auth migration path?
Existing users on legacy JWT needed transparent migration to Keycloak/Cognito without forced re-registration.
-
What are the scale expectations?
Single-organization internal tool, not multi-tenant at the data layer. Not discussed explicitly — noted as a future constraint if the product scope expands.
Architecture Decisions
Five calls that
shaped the system.
/api/proxy/[...path]) that reads the
httpOnly cookie server-side and forwards requests
with an Authorization header.
localStorage
tokens — exposes tokens to XSS.
getServerSideProps per-route pattern — too
much boilerplate per module.
sseTokenService issues a short-lived (60s,
single-use) token fetched via the proxy; the
EventSource connects directly to the Express backend
using that token.
EventSource API does not support custom headers —
httpOnly cookies are invisible to it, and proxying a
long-lived stream through Next.js risks Vercel timeout limits.
localStorage for the
main token — security downgrade.
apps/web/cdk for S3 + CloudFront image
serving.
Problems Hit
Five bugs worth
writing down.
cognito-admin-api-called-with-sub-instead-of-username
sub UUID — but the code was passing sub.
cursor-in-useCallback-deps-causing-infinite-fetch-loop
cursor was included in the useCallback deps array; each fetch updated cursor, which re-triggered the callback.
cursor from the dependency array; fetch is now triggered explicitly, not reactively off cursor state.
DELETE-requests-silently-dropped-request-body
DELETE from body-forwarding logic, assuming REST convention (DELETE has no body).
FormData-being-JSON.stringify-d-in-the-API-client
apiClient serialized all bodies with JSON.stringify; FormData requires the browser to set the Content-Type with the correct boundary automatically.
FormData instance detection to skip serialization and omit the Content-Type header on those requests.
pnpm-dlx-prisma-version-mismatch-in-Docker-CI
pnpm dlx bypasses the lockfile and fetches the latest CLI version.
pnpm prisma binary.
What Shipped
The full scope
of what went live.
Auth & Access
-
6 role-based dashboards
Super User, Task Supervisor, Task Agent, Client, District Manager, Account Executive — distinct permission sets and UI layouts per role.
-
Hybrid IDP auth
Legacy JWT, Keycloak (dev), and Cognito (staging/prod) with transparent auto-migration on login.
-
BFF proxy route
/api/proxy/[...path]handling GET/POST/PUT/PATCH/DELETE with auth forwarding. Token never touches client JS.
Task Management
-
Status workflow
NEW → IN_PROGRESS → REVIEW → COMPLETED/BLOCKED/OVERDUE with priority levels and skill tagging.
-
Serial number system
Per-category prefix sequences (e.g.
WD-00042), atomic DB increments, format validation, and backfill migration for existing tasks. -
Work logging
Time entries in Jira format (
1w 2d 3h 30m), transactional aggregation intototalTimeSpentandlatestTimeRemaining, progress bar, soft delete audit trail. -
Batch operations
Multi-select tasks, batch status/priority/assignee/category/due date update, batch delete, and CSV export.
Views & Filtering
-
Three task views
Kanban (dnd-kit with per-column infinite scroll), Gantt (frappe-gantt with synchronized sidebar), and tabular list — switchable via query param.
-
Advanced filter builder
Dynamic condition builder with per-field operator validation (eq, contains, in, between, isNull, etc.), AND/OR logic, and saved filter system (
TaskViewFilter). -
Global fuzzy search
Fuse.js across navigation items and tasks from the top bar, cursor-based pagination, scroll-to-load-more.
Real-time & Notifications
-
SSE notification system
Multi-tab support, tab-specific connection IDs, ping/keepalive, reconnect logic, and a connection status indicator component.
-
Push notifications
Web Push API with VAPID, persistent subscription storage, and
sw.jsservice worker — delivers notifications when the tab is inactive or closed. -
Overdue task scheduler
Node.js worker thread (
overdueTasksWorker.ts) on a daily cron; transitions past-due tasks and notifies assignee and all Task Supervisors via SSE + push.
Content & Files
-
Rich text editor
TipTap with full formatting, syntax-highlighted code blocks,
@mentionsupport with user search, link auto-extraction, and file attachments per comment. -
Task comments system
Threaded replies, edit/delete permissions, author-type polymorphism (USER/CLIENT), and mention notifications via SSE.
-
File handling
Presigned S3 upload/download via AWS SDK, CloudFront serving, per-context ownership (task, profile, comment), 5 MB validation, file preview modal with zoom and PDF support.
-
In-app real-time chat
Embedded via iframe, JWT-signed init token, cross-origin
postMessagefor auth refresh and notification sync, floating chat button with unread badge.
Infrastructure
-
Dual CDK stacks
ECS/Fargate for backend, S3 + CloudFront for image serving — independent deployment lifecycles.
-
CI/CD pipelines
GitHub Actions for staging and production; Vercel for frontend. Separate staging and production environments.
-
Shared packages
@wnp/types(Zod schemas + TS types),@wnp/logger,typescript-config— single source of truth across the monorepo.
What I’d Do Differently
Two decisions
I’d revisit.
The two Prisma schemas (apps/web/prisma
and apps/backend/prisma)
being maintained separately — kept in sync manually with a note in
the README — is the highest-risk part of this codebase. A single
schema drift can cause silent type mismatches that only surface at
runtime. The right call would have been a shared
packages/database
workspace from the start.
The chat integration via iframe and postMessage
is the most fragile coupling in the system — any change to the
external chat service’s origin, auth contract, or message
protocol silently breaks the integration with no type safety. A
versioned message schema shared between both apps, or building the
chat on the same backend with WebSocket support, would have
eliminated the cross-origin dependency entirely.