I've been building TaskFlow — an internal task management platform for IDFA Digital AI Institute — for several months now. JWT auth, RBAC, employee management, performance analytics, WhatsApp/email reminders, file uploads, calendar events. It's grown.
At some point you stop "knowing" your codebase and start navigating it. So I ran a static code graph analysis over the entire project: 80 files, ~57,000 words of code, extracted into 215 nodes and 199 edges, clustered into 54 communities.
What came back was a blueprint I didn't entirely expect.
The Stack at a Glance
Before we dig into the graph, here's what we're working with:
- Framework: Next.js 15 (App Router)
- Language: TypeScript
- ORM: Prisma 7
- Database: PostgreSQL via Neon
- State: Zustand
- Styling: Tailwind v4
-
Auth: Custom JWT (
signToken/verifyToken) + middleware proxy - File Uploads: UploadThing
-
Notifications: Email (
nodemailer) + WhatsApp API The API surface is entirely Next.js Route Handlers — no Express, no separate backend. Everything lives insidesrc/app/api/.
What Is a Code Graph, Anyway?
A code graph treats every function, file, and component as a node, and every calls, imports, or contains relationship as an edge. Run community-detection over that graph and you get natural clusters — modules that are more connected to each other than to the rest of the codebase.
The tool I used extracted 92% of edges directly from static analysis and inferred the remaining 8% from structural context (with an average confidence score of 0.8). That's the "surprising connections" section — more on that below.
The God Nodes
The most-connected nodes in a codebase are your de facto architectural pillars. In graph theory, these are high-degree nodes — the traffic hubs everything routes through.
Here are mine, ranked by edge count:
| Rank | Function | Edges | What It Means |
|---|---|---|---|
| 1 | GET() |
19 | Every read in the app flows through here |
| 2 | POST() |
15 | Auth, creation, integrations |
| 3 | PATCH() |
8 | Updates for tasks, users, templates |
| 4 | getSession() |
7 | Session validation is called everywhere |
| 5 | DELETE() |
6 | Soft and hard deletes across resources |
| 6 | proxy() |
4 | Middleware gatekeeper |
| 7 | formatDateRange() |
4 | Date logic reused across 4+ pages |
| 8 | buildActivitySentence() |
4 | Activity feed string builder |
| 9 | formatDateShort() |
3 | |
| 10 | startTimer() |
3 | Timer inside AssignTaskModal
|
What This Actually Tells You
The top 5 are all HTTP verbs or session logic — that's expected. But formatDateRange() and buildActivitySentence() sitting at #7 and #8 with 4 edges each is a flag.
These are pure utility functions that live inside a page file (holidays/page.tsx) rather than in src/lib/. They're being referenced from 4 different contexts, which means they've quietly become shared infrastructure — but they're not treated as shared infrastructure. They should be extracted into a src/lib/date.ts or src/lib/activity.ts module before they become a maintenance burden.
The graph just surfaced something I would have noticed eventually — but later.
The Architecture in Communities
The community detection grouped the code into some natural-feeling clusters and some surprising ones.
Community 0 — The Core Hub (cohesion: 0.1)
The lowest cohesion score, with 8 nodes:
sendEmailTaskReminder(), MyTasksContent(), ProfilePage(),
emptyHML(), GET(), POST(), getSession(), sendWhatsAppTaskReminder()
Low cohesion in Community 0 isn't bad — it is the core. It's the hub that connects auth, user API, notification dispatch, and primary page content. A sprawling community with low internal cohesion means it bridges many others. The GET() and POST() handlers living here confirm this: they're cross-community bridges.
Community 1 & 4 — The Timer/Recording Module (cohesion: 0.11 and 0.22)
pauseRecording(), resumeRecording(), startRecording(),
startTimer(), stopRecording(), stopTimer()
These appear twice in the graph (Communities 1 and 4) — a duplicate community that happens because the same functions exist in two contexts: once inside AssignTaskModal.tsx and once in a separate recording hook. This is a classic sign of copy-paste reuse that should be unified into a single useRecorder hook.
Community 6 — Date Utility Cluster (cohesion: 0.43)
buildActivitySentence(), formatDate(), formatDateRange(),
formatDateShort(), formatTime()
This is the src/lib/date.ts file that doesn't exist yet. The graph carved it out naturally — these functions belong together and are already acting as a unit. The refactor is obvious once you see it.
Community 7 — Auth & Access Control (cohesion: 0.29)
signToken(), verifyToken(), proxy(), canAccess()
This is the security layer. proxy() in src/proxy.ts acts as the middleware that sits in front of all authenticated routes, calling verifyToken() and then canAccess() from rbac.ts before requests reach any handler. Clean separation.
Community 15 — Performance Analytics (cohesion: 0.67)
ScoreBar(), scorePct()
High cohesion, small scope. PerformanceAnalytics.tsx is a self-contained component with its own internal utility (scorePct()) and a tight render cycle. The graph essentially validated that this component is well-encapsulated.
The Surprising Connections
The 8% inferred edges were the most interesting part of the report. The static analyzer couldn't directly trace these links but reasoned them from co-location and call patterns:
AllTasksContent() → GET()
my-tasks/page.tsx → api/users/route.ts
MyTasksContent() → GET()
all-tasks/page.tsx → api/users/route.ts
GET() → fetchNotifications()
api/users/route.ts → components/layout/TopNav.tsx
POST() → signToken()
api/users/route.ts → lib/auth.ts
proxy() → GET()
src/proxy.ts → api/users/route.ts
The first two (AllTasksContent and MyTasksContent both calling GET() at the users endpoint) make complete sense — both pages fetch the user list for the "Assign To" dropdown but do it via fetch('/api/users') inside a useEffect. The graph correctly inferred this even though the static link is a URL string, not a direct import.
The GET() → fetchNotifications() inferred edge is more interesting. The users endpoint and the notification fetch aren't obviously linked in the source, but they share a session check and are called in sequence during the initial page load. The analyzer spotted the pattern.
The one I want to verify and potentially refactor: proxy() → GET(). The middleware routes through to api/users/route.ts for permission validation. That's a tight coupling between the auth proxy and the user data API — worth reviewing whether the RBAC check should be reading from the session token alone rather than re-fetching user data.
Knowledge Gaps the Graph Flagged
54 communities for 215 nodes means a lot of singleton and doubleton communities (Communities 24–53 mostly). These are "thin communities" — isolated files or pairs that haven't been wired in deeply enough.
Some worth noting:
useTimeTheme.ts (Community 36) — my custom time-based theming hook. It shows up as an isolated doubleton, meaning it's only referenced in one place. Given that the whole calendar UI was built around it, I'd expect it to be imported in more components. This is a gap: either the hook's usage is too narrow, or other components are reimplementing the same logic locally.
useTaskStore.ts / filterTasks() (Community 39) — the Zustand store for task state. Also isolated. In theory, this should be imported everywhere task state is consumed. Finding it as a thin community means some pages are probably fetching tasks directly from the API on mount instead of reading from the store — inconsistent state management that could cause stale UI bugs.
uploadthing-client.ts and uploadthing.ts (Communities 50–51) — both isolated singletons. UploadThing is wired in but not deeply connected to the broader graph, which is actually fine — it's an integration boundary and should be isolated.
The Architectural Takeaways
Running this analysis gave me a concrete refactor list:
1. Extract the date utilities
formatDate, formatDateShort, formatDateRange, formatTime, and buildActivitySentence are already a community. Move them to src/lib/date.ts and src/lib/activity.ts.
2. Consolidate the timer/recording logic
startTimer, stopTimer, startRecording, pauseRecording, resumeRecording, stopRecording live in AssignTaskModal.tsx but the duplicated community suggests they need a useRecorder.ts hook.
3. Audit Zustand store adoption
useTaskStore is too isolated. Pages should be reading task state from the store rather than issuing fresh API calls on every mount.
4. Review the proxy ↔ users API coupling
If proxy() is calling GET() on /api/users for permission checking, that's a performance and separation-of-concerns issue. Session token claims should be enough for RBAC decisions at the middleware layer.
5. Promote useTimeTheme
The hook is doing real work — if the calendar uses it, more UI surfaces probably should too.
Final Thoughts
The thing that surprised me most about this exercise wasn't any individual finding — it was the density of the insight from purely structural data. No runtime profiling, no manual tracing. Just parse the files, extract relationships, cluster the graph.
The god nodes tell you what your architecture depends on. The low-cohesion communities tell you what's doing too many jobs. The thin communities tell you what's underutilized. And the inferred edges tell you what your instincts built that the code doesn't yet make explicit.
If you haven't mapped your codebase yet, do it. You'll find the library you forgot to build and the refactor that's been waiting since month two.
Built with Next.js 15 · Prisma 7 · Neon PostgreSQL · Tailwind v4 · Zustand
Graph: 215 nodes · 199 edges · 54 communities · 92% extraction accuracy
Top comments (0)