Knex — the social graph, drawn.

A social network that renders your connections as a live force-directed graph instead of a list.

RoleArchitecture · Full-stack
Year2025
StackExpress · React 18 · D3.js
Team5 · FAST-NUCES capstone
Overview
Problem
Connections you can’t see
Approach
Ring-by-ring graph traversal
Surfaces
Graph · Connections · Messages
Realtime
Socket.IO, authed at handshake

Social graphs are graphs, but no product draws them. LinkedIn labels a stranger “2nd degree” and leaves the rest to your imagination — the structure is in the data and nowhere on the screen. Knex makes the topology the interface: you sit at the center, your circle forms the first ring, theirs the next, and the paths between people become the thing you actually look at.

An Express API walks the friendship graph outward from whoever is asking and returns everyone within reach, each stamped with their distance in hops. D3 turns that into a physics simulation in the browser, where distance drives size, color and position. Friend requests reshape the graph as they’re accepted, and a WebSocket layer riding the same server and the same token carries direct messages between anyone already connected.

Architecture

One server, two protocols, one token.

01

API

Owns every read and write. Twenty-three REST endpoints behind JWT middleware, the social graph stored as embedded adjacency lists, a breadth-first traversal that stamps each person with their hop distance, and profile images streamed straight through to Cloudinary without ever touching disk.

Express 5 · Mongoose 8 · MongoDB

02

Realtime

Socket.IO mounted on a custom path on the same HTTP server, verifying the same token during the connection handshake. Messages are persisted before they broadcast, and sender identity is read from the token rather than the payload — the client can address a room but never claim to be someone in it.

Socket.IO 4 · jsonwebtoken

03

Client

A React SPA where no component calls the network directly. Every page goes through a service layer that injects tokens, normalizes errors and hands back plain data. The graph is a self-contained D3 surface owning its own loading, error and retry states.

React 18 · TypeScript · Vite 5 · D3 7

One network, three surfaces.

Graph

Distance, drawn.

Four forces negotiate the layout on every data change — links pulling at a fixed distance, charge pushing bodies apart, a centering pull, and per-node collision sized to the radius. Hop distance drives size and hue, so structure reads before you’ve clicked anything.

  • Drag to pin a node, release to hand it back to the physics
  • Zoom 0.5× to 3×, double-click for fullscreen
  • Live node and connection counts, retry on failure
Connections

Search, request, redraw.

Find anyone by name or email, send a request, and watch the network reshape as it’s accepted. Requests are tracked in both directions as separate queues, and every irreversible action passes through a confirmation step naming the person.

  • Incoming and outgoing requests, counted per tab
  • Accept, decline, cancel, unfriend
  • Relationship state resolved per search result
Messages

Optimistic by default.

Conversations open with history from the API and stay live over the socket. Your own message renders the instant you send it, then earns its delivery state as the server confirms the write and the broadcast returns.

  • Sent and delivered ticks driven by server acks
  • History, live and in-flight streams reconciled into one thread
  • Room membership derived, never registered
Under the hood

The hard part was never drawing a circle.

No mislabeled friends01

Breadth-first, one query per ring

The traversal expands ring by ring, fetching an entire level in a single query, so everyone carries their shortest distance rather than whichever route reached them first. A depth-first walk stamps a direct friend as three hops out the moment a longer path finds them earlier — and since radius and colour derive from that number, the error renders.

ring 0you40px
ring 1friends22px
ring 2friends of16px
ring 3+beyond12px

Each mutual friendship emits one edge rather than two, so the connection count reflects relationships instead of directions.

No room registry02

Rooms that derive themselves

Both participants independently compute the same channel from the two user IDs, sorted. No lookup table, no allocation, no cleanup — two people who have never spoken land in the same room the first time either one opens the conversation.

room = [myId, otherId].sort().join('_')handshake → verify token → attach user → join room

The token is checked in connection middleware, so an unauthenticated socket is refused before it can join anything.

No account without a verified inbox03

Verification as the gate

No token is issued until a six-digit code comes back. Logging into an unverified account doesn’t simply fail — it mints and sends a fresh code, and the client reads the response and drops you straight into verification. The resend cooldown is persisted, so a refresh doesn’t hand you a new code early.

register → code, 10 min → verify → token, 7 days
Signupcode → verify → token
Unverified logincode resent → verify → token
Password resetcode → verify → new password

One verification component serves all three flows.

No wait between send and see04

Three sources, one thread

A message renders immediately as pending, flips to sent when the server acknowledges the write, and to delivered when it returns through the room broadcast. Loaded history, live socket traffic and unconfirmed sends are merged into a single ordered thread with duplicates collapsed.

  1. sending
  2. sent
  3. ✓✓delivered

The server persists first and broadcasts second, so an acknowledged message is already durable.

Built with

Backend
Node.jsExpress 5Mongoose 8jsonwebtokenbcryptjsMulterstreamifierdotenv
Frontend
React 18TypeScriptVite 5Tailwind CSS 3shadcn/uiRadix UID3 7React Router 6Axios
Realtime
Socket.IO 4socket.io-client
Data
MongoDB2-collection schemaEmbedded adjacency lists
Services
CloudinaryResend
Infra
RenderVercel
Next project

Metamorphix