Aditya Gaur
Work

Internal platform

An internal wiki that refuses to persuade

A read-optimized company wiki compiled to static pages, where the auth model's one limitation ended up designing the content pipeline.

Role
Solo — product, design, build pipeline
Timeframe
2026
Status
shipped
Stack
Node.js·Notion API·Cloudflare Pages·Pages Functions·Vanilla JS

The reader is in a hurry

You have a specific question (what an acronym stands for, what an endpoint returns, who declares an incident) and you have about thirty seconds of patience. The answer exists. It's in the company Notion, four clicks deep, behind a workspace search that ranks by recency and a page tree that reflects how documents were written rather than how they're looked up.

An internal wiki has exactly one job: get that person to that answer before their patience runs out. Everything else is decoration, and decoration that doesn't help, hurts.

The design brief for this one was a single sentence: the wiki is gated to company email, which means every reader is already trusted, which means the design does not need to perform credibility for outsiders. That sentence eliminates most of what makes corporate tools look the way they do. No hero sections, no persuasion, no brand experience. A page here should read like a well-organized index prepared by someone competent, and a stub is allowed to look like a stub — a privilege no public site has.

122
pages built per run
26 KB
search index, entire corpus
504
lines in the whole build script
3
runtime dependencies

Why not just use Notion

We do, for writing. Notion is the editorial system: comments, drafts, simultaneous editing, all the things a static site is bad at. What Notion is bad at is being read in a hurry: heavy client, slow first paint, search tuned for the author rather than the reader.

So the wiki is a compiler. A build script walks the workspace from a root page, converts each page to markdown and then to HTML, rewrites internal Notion links to wiki URLs, downloads every image, and emits a directory of static pages plus one search index. Cloudflare Pages serves the result; a cron rebuilds it daily so the published surface trails the editorial one by at most a day. Writers keep their tool. Readers get sub-second pages.

Walk the workspace

Starting from one root page, recurse into every child page and database — including pages nested inside column layouts, which the API hides one level deeper and which cost a real bug to find. Three concurrent requests, because the API rate-limits and a wiki build has no reason to hurry.

Page map
{ id, title, url, parentId,
  children, depth, summary,
  status, lastEdited }

Figure 1. One page's trip through the build. Everything upstream of the render is source-specific; everything downstream doesn't care where the content came from.

The rejected references did the designing

The fastest way to specify this design was to name what it must not look like. Each rejection carries its reason, and the reasons, not the rejections, are the actual spec.

Too friendly to be fast

Notion's public sites read consumer: generous whitespace as decoration, soft illustration, a tone that's selling something. An internal reader isn't being sold to, and the whitespace they need is the kind that separates answers, not the kind that performs calm.

Figure 2. Five references, five refusals, and what each refusal bought.

The same discipline produced a short list of banned defaults: pure-grey neutrals and link-blue (the uncustomized-template tell), viewport-scaled hero type, shadow ramps, backdrop blur. The palette is warm cream and warm near-black in OKLCH, one restrained accent with a stated budget of under ten percent of any surface, and depth carried by borders and background shifts rather than shadows. Three typefaces, each load-bearing: a serif for landmarks, a tight sans for body and UI, a mono for identifiers.

Search that fits in one paragraph

12 pages · index 2,606 bytes · linear scan, no fetch after first load

  1. PAN verificationtitle prefix · 784
  2. PAN-Aadhaar link statustitle prefix · 777

Figure 3. The production scoring ladder over a synthetic corpus. Watch the tier label — title beats prefix beats substring beats body.

Scoring, tie-breaks, the ten-result cap and the two-character floor are the shipped code, unchanged. The corpus is synthetic — the real index carries internal content.

There is no search server. The index is one JSON file, 26 KB for the whole corpus, fetched lazily the first time the palette opens, then scanned linearly per keystroke. The ranking is a four-tier ladder: exact title match wins outright, then title prefix, then title substring, then body substring. Two tiebreakers do quiet work: among prefix matches, the shorter title wins, and within any tier an earlier match position beats a later one.

No stemming, no fuzzy matching, no inverted index. At 122 pages, a linear scan over 26 KB completes faster than a keypress repeats, and every clever thing search could do is a way to return something the user didn't type. The honest upgrade path is indexing more — full body and headings instead of a 500-character prefix — not matching smarter.

The auth model designed the content pipeline

The access model is deliberately flat: a Google sign-in restricted to the company domain, a signed cookie, seven days. One tier. Any employee sees everything. The auth layer is three small serverless functions and cannot express "this page is for managers only" — and adding per-page tiers would mean a real authorization system, which is exactly the infrastructure this project exists to not have.

That limitation had to go somewhere, and it went upstream. If the serving layer can't do need-to-know, the build has to: sensitive pages are excluded from the crawl entirely, decided page by page in a sign-off document with three verdicts — include, exclude, or include-scrubbed. Excluded means never fetched. Not published-and-hidden, not access-controlled, not in the nav, and — the part that actually matters — not in the search index, which would otherwise leak 500 characters of every page it touched. A page the pipeline never saw cannot appear in any surface the pipeline builds.

That's the architecture story I didn't expect this project to have: a constraint in the smallest component, 278 lines of auth, dictated the design of the largest one. The alternative reading is that it's a workaround. Both are true. The difference between a workaround and a design is whether you wrote it down before someone found out, and this one is documented as a decision with its alternatives priced.

What I'd do differently

Retire the upstream dependency sooner. The Notion collector is roughly sixty percent of the build script: the rate limiter, the block transformers, the image downloader, all of it exists because the source is an API rather than files. The planned second version moves content into the repo, halves the script, and replaces the daily cron with "publishing is merging a PR." I'd also add the guardrail the current build lacks: assert a minimum page count and zero broken links before deploy, so a bad content edit can't quietly ship a gutted site.

Two pages titled "Untitled" made it to production. The build trusts upstream data quality, and upstream is a workspace edited by everyone. A build that fails loudly on an empty title would have cost ten minutes and caught both.

I still can't tell you how many people use it. The success test was that anyone can hit ⌘K and land on the right page in three keystrokes, and there is no instrumentation behind the auth wall to say whether that's true. For an internal tool the fix is one counter, searches per week, which is the difference between believing the wiki works and knowing it does.