Skip to content
Boris Novichkov
ResumeGitHub ↗
ENRU
All work

Network Trail

A guide to the social graph — paste a profile, get the structure behind it

A research platform that turns public social-network data into something you can navigate: it crawls a profile's connections through official APIs, works out which communities the graph splits into, and ranks the traits those communities share. Six services — a React front end, a Flask API, a domain layer, a distributed request worker, a FastAPI analysis service and a token pool — built over three years and now published as one repository.

Archived
Role
Solo — architecture, backend, worker fleet, front end
Period
Sep 2019 — Feb 2022
ntrail — friend graph
NTrail friend graph: about 250 nodes coloured into four dense communities around one central profile

One profile's friend graph after community detection. The clusters are a school, a university course, a workplace and a home town — none of it stated on the profile itself.

The problem

You can do this by hand. Open a profile, open their friend list, open thirty of those, notice that eight went to the same school, that four of those also went to the same university, that the ones who did are the ones posting from the same city. It works, and it takes an afternoon per person, which is why nobody does it except when the stakes are high enough.

NTrail was an attempt to make the machine do the tedious half: the crawl, the joins, the counting, the ranking of what is actually unusual about a set of people — and to leave the judgement to the person reading the result.

How it works

The interface is a query bar, not a form. GET vk.user <id> friends returns a set; that set can be split into communities, intersected with another set, or asked what its members have in common. Because every result is a set, the result of one query is the input of the next, and the analysis is a sequence of small steps rather than one big configured report.

Underneath, three problems shaped the architecture:

Rate limits. VK allows about three requests a second per token, and Instagram had no usable API at all. A single client is useless at graph scale, so requests became atomic units — method, token, parameters, everything needed to run standalone — and a worker takes them in batches, spreads them over cores and machines, caches every result, and returns them together.

Dirty data. Ages that are wrong on purpose, empty fields, hidden friend lists. So attributes are inferred from the neighbourhood rather than read off the profile: if forty per cent of a person’s friends are in one university’s graph, that is a stronger signal than a blank education field.

Structure over attributes. The most useful thing about a friend graph is not any single profile — it is the shape. Communities come out of Louvain modularity over the graph, and each community gets ranked by what its members share. The clusters in the screenshot above were a school, a university course, a workplace and a home town; none of them are stated anywhere on the profile.

What it took to publish it

The project lived in six repositories with submodules pointing at each other, and in two of them a committed env_file carried a VK application secret, an access token and a database password. Publishing it meant:

  • rewriting every history to purge those values, and revoking the tokens;
  • merging six repositories into one with git filter-repo, each into its own directory, so all 470 commits survive the move;
  • removing the product screenshots entirely — they were captures of real profiles, with names and faces of people who never agreed to appear in someone’s portfolio. The graph above is the one artefact that shows the work without showing anybody.

What I would do differently

The domain layer and the worker were the right split, and both would survive a rewrite. The Flask backend accumulated query-planning logic that belonged in the core, and the front end talked to a hard-coded API host, which is exactly the kind of detail that stops a project from being deployable by anyone but its author. The analysis service arrived late and never fully replaced the backend it was meant to replace — the cost of building a second thing before the first one is finished.

the query bar
GET vk.user <id> friends
The friend list of a profile, as a set you can act on
GET vk.community <id> members
Everyone in a group, ready to intersect with another set
split clusters
Break the current set into communities by graph structure
interesting properties
Rank what the current set has in common — school, university, city, age

The interface is a query bar, not a form. Every result is addressable, and a result can be the input of the next query.

services
frontend
React 16 + Redux; sigma / vis for the graph, recharts for the distributions
backend
Flask API over PostgreSQL — query objects, selective execution, caching
core
Domain layer: VKUser, VKCommunity — friends(), groups(), posts()
worker
Executes batches of atomic API requests across cores, caches results
api
FastAPI + Redis service layer with the analysis stack
credentials
The token pool the workers draw from

Six repositories, now six directories, each keeping the history it came with.

deploy
$ docker run --rm -v $PWD/frontend:/app node:16-alpine npm run buildThe build folder is ready to be deployed.$ docker compose up -d --buildContainer ntrail-db-1       StartedContainer ntrail-backend-1  Started$ curl -o /dev/null -w '%{http_code}' http://127.0.0.1:8090/api/200

The MVP runs again — static bundle behind nginx, backend in a container beside it.