Colophon
How this site is built, and what it knows about you
Everything here is checkable. The stack is what the repository actually depends on, the limits are printed from the table the rate limiter reads, and the privacy list includes the answers that are not flattering.
The stack
cat stack.txt
| Layer | What | Why |
|---|---|---|
| framework | Next.js 16, App Router | Static pages with incremental revalidation; server components by default. |
| language | TypeScript, strict | No implicit any anywhere, including the database layer. |
| database | Neon Postgres + Drizzle | Serverless HTTP driver; the schema is typed from one file. |
| media | Cloudflare R2 | Free egress; every image transcoded to AVIF and WebP at four widths. |
| editor | Tiptap | Posts are stored as a JSON document, not HTML, so nothing is ever injected. |
| styling | Tailwind CSS v4 | CSS-first tokens; one accent, one type scale, no component library theme. |
| auth | Auth.js v5 + GitHub | One allowlisted account. There is no users table to attack. |
| hosting | Vercel, deployed from main | A push is the whole deploy procedure. No CLI, no dashboard step. |
| tests | Vitest | An offline unit suite and an opt-in one that hits the real services. |
Architecture
How the pieces fit together
Every box is a real part of this site, and the panel says why it is there rather than the obvious alternative. Nothing here is illustrative.
11 components · 12 connections · select any one for what it is and why it is there · scroll the diagram sideways on a narrow screen
Client
Reader's browser
What a visitor actually receives: static HTML, a small number of client components, and images from a different origin.
The public path is kept close to empty on purpose. The reveal animation is CSS rather than a motion library, the command palette is loaded on first open rather than on every page, and no map or graph library reaches the browser at all — the world map is a cached SVG mask and this diagram is arithmetic. That is what holds a mobile Lighthouse score in the high eighties on a home page eight thousand pixels long.
Edge
Vercel edge + proxy
The CDN, and `proxy.ts` running in front of every `/admin` and `/api/auth` request.
The admin guard lives here as well as in every server action, so an unconfigured production deployment answers 404 instead of exposing the CMS. It also canonicalises `www` to the apex on the auth surface, which is what stops an OAuth callback failing on a four-character prefix. Functions are pinned to `sin1` in `vercel.json`: the default is Washington and the database is in Singapore, and that one setting was most of a fifteen-second admin stall.
Service
Next.js App Router
The application. Server components render every page; server actions are the entire write path.
There is no REST layer between the pages and the database because there is no second consumer — an API would be a second thing to secure, version and keep in step for no reader. The trade is that every server action is an independently addressable POST endpoint, so each one re-checks the session itself through `requireAdmin`. The proxy is the convenience; that call is the control, and a test asserts every exported action makes it.
Datastore
Neon Postgres
Every post, every structured row and this diagram, in one Postgres database reached through Drizzle.
Postgres over the document store this content came from, because the record is relational — a decision belongs to a project, a component belongs to a diagram — and because full-text search is a generated column here rather than a second service. The driver is the serverless HTTP one rather than a pooled TCP connection, which suits functions that come and go; the cost is that there are no transactions, so every multi-row write in this app is written to be idempotent if it is interrupted.
Cache
Static + ISR cache
Every public page is prerendered and revalidated on an hourly window.
A reader is served a file and never waits on Postgres, on GitHub or on a cold start. That is also what makes the free tier honest: Neon scales to zero and cannot be stopped from doing so, and this is what keeps that cold start off the reader and on the regeneration. Publishing calls `revalidatePath` rather than waiting the hour out.
External
GitHub OAuth
The only way into the admin: Auth.js v5 against a GitHub OAuth App, with a one-name allowlist.
There is no users table and no password anywhere in this system, because there is one writer — authorisation is a single comparison against `ADMIN_GITHUB_LOGIN`, re-checked on every request so changing it invalidates a session that already exists. Sessions are JWTs rather than database rows, which is what keeps the config edge-safe enough for the proxy to call it.
Edge
img.devjadiya.com
A custom domain bound to the media bucket, serving every image.
R2 charges nothing for egress, so media is served from here rather than through Vercel's bandwidth. Objects are immutable and cached for a year, which means re-editing an image writes a new key instead of overwriting one and every reference follows it. The sharp edge of this: the domain is bound to the *whole* bucket, so anything private has to live in a different bucket rather than behind a prefix.
External
GitHub API
Four cached calls an hour behind the activity dashboard, the heatmap and the repository evidence on the skills page.
Read on the server and cached for an hour rather than fetched in the browser, so no key is ever exposed and the anonymous limit of sixty requests an hour is never close. Every call has a six-second timeout and returns a result object instead of throwing — GitHub being slow is not a reason for this site to be down, and a failure is printed on the page rather than swallowed.
Client
The admin
The CMS: the Tiptap editor, the media library, the section forms, and this diagram's own builder.
A CMS rather than Markdown in the repository, because the point is to be able to write and publish from a phone at a conference. The one thing it does that a CMS normally does not: it decodes, crops, resizes and encodes an image in the browser *before* uploading, because a Vercel function request body is capped at 4.5 MB and a three-megabyte photo through a server action was slow enough to feel broken.
Job
Image pipeline
sharp derives an AVIF and WebP ladder at 400, 800, 1200 and 1600 pixels from each upload.
Derived once on upload rather than on request, because a variant that is generated per visitor is a bill and a latency spike for a file that never changes. It never upscales past the source, so a small original stays small rather than pretending to be a large one. A PDF is rasterised in the browser first and enters the same pipeline, so a certificate is stored as the same ladder as every other image.
Datastore
Cloudflare R2
S3-compatible object storage holding every image, video and resume.
Chosen over S3 for egress, which is free here and is the entire cost of serving media from a personal site. Deleting a row in the admin removes the reference and keeps the object, so nothing a published page points at can be destroyed from a list view. Private objects live in a separate bucket with no custom domain — a `private/` prefix on a public bucket is obscurity, and Phase 11 found out the hard way that it is not access control.
How to read this
Every box is a real part of this system. Select one and this panel prints what it is, why it is there rather than the obvious alternative, and the decision record behind it.
- ClientRuns on the reader's device.
- EdgeBetween the reader and the app: CDN, proxy, middleware.
- ServiceApplication code you wrote and deploy.
- DatastoreWhere state lives and survives a restart.
- CacheDerived state that can be thrown away.
- JobSomething that runs on a schedule or a trigger.
- ExternalA third party. You depend on it and do not control it.
What is actually enforced
Security on a personal site is usually a paragraph. These are the controls in the code, each one a thing that refuses a request rather than a thing that hides a button.
./audit --controls
authorisation Every admin mutation re-checks the session server-side before touching the database. The route guard is not the UI.
validation Every public write parses into a closed Zod schema. Unknown fields are a 400, not a shrug.
csp A content security policy that pins object-src, base-uri, form-action and frame-ancestors.
private files Resume PDFs are private objects. A short-lived signed URL is fetched server-side and streamed, so the signature never reaches a browser.
audit Every admin action and every sign-in - accepted or refused - writes a row I can read.
moderation Nothing a reader writes appears until I have read it. Turnstile and the rate limit only keep the queue usable.
The rate limits
Printed from the table the limiter reads, so this cannot drift away from what the site does. Counted per instance, which is the honest limitation of doing this without a shared store.
./audit --limits
| Surface | Allowed | Window |
|---|---|---|
| login | 10 | 10 min |
| adminMutation | 300 | 1 min |
| adminUpload | 40 | 5 min |
| adminRead | 600 | 1 min |
| comment | 5 | 5 min |
| interaction | 120 | 1 min |
| track | 240 | 1 min |
| resolver | 60 | 1 min |
| search | 120 | 1 min |
| og | 60 | 1 min |
| revalidate | 60 | 1 min |
| agent | 6 | 1 min |
| agentHourly | 40 | 60 min |
What this site knows about you
./audit --privacy
cookies for readers — none
third-party analytics — none
ad or tracking scripts — none
fonts — self-hosted
your IP address — hashed with a salt that rotates daily, never stored raw
page views — counted first-party: the page, the referring site's name, and the country and city your network resolves to - no cookie, no identifier that lasts past a day
Do Not Track / Global Privacy Control — honoured: nothing about your visit is recorded
read counts — a number per post, with nothing attached to it
link opens — a timestamp and those daily hashes, so I know a share was read
The last line is the one worth saying plainly: when I share a resume or a short link, I can see that it was opened and roughly when. I cannot see who you are, and after a day the hash that grouped your visits is not reproducible even with the database in front of me.