Dev Jadiya

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

devjadiya.com — stack

cat stack.txt

LayerWhatWhy
frameworkNext.js 16, App RouterStatic pages with incremental revalidation; server components by default.
languageTypeScript, strictNo implicit any anywhere, including the database layer.
databaseNeon Postgres + DrizzleServerless HTTP driver; the schema is typed from one file.
mediaCloudflare R2Free egress; every image transcoded to AVIF and WebP at four widths.
editorTiptapPosts are stored as a JSON document, not HTML, so nothing is ever injected.
stylingTailwind CSS v4CSS-first tokens; one accent, one type scale, no component library theme.
authAuth.js v5 + GitHubOne allowlisted account. There is no users table to attack.
hostingVercel, deployed from mainA push is the whole deploy procedure. No CLI, no dashboard step.
testsVitestAn 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.

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.

devjadiya.com — controls

./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.

devjadiya.com — limits

./audit --limits

SurfaceAllowedWindow
login1010 min
adminMutation3001 min
adminUpload405 min
adminRead6001 min
comment55 min
interaction1201 min
track2401 min
resolver601 min
search1201 min
og601 min
revalidate601 min
agent61 min
agentHourly4060 min

What this site knows about you

devjadiya.com — privacy

./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.