Shreyas Sane
Explore

Coding

This portfolio

The site you are reading: two complete experiences over one corpus, where the conversational one is optional and the indexable one is not a fallback but a peer.

Role
Sole author: product architecture, design direction, and full-stack implementation
Years
Present
Built with
Astro, React, TypeScript, Zod, Hono, Cloudflare Workers, Durable Objects, Cloudflare D1, Cloudflare R2, Turnstile, Drizzle ORM, Turborepo, Vitest, Playwright

Two front doors, one corpus

There is a version of this site where you type a question and it answers, filters, and moves you around. There is another where you click through pages and read. Neither is the real one.

That matters more than it sounds. The usual shape of an AI-first product is a conversational layer sitting on top of something that has been hollowed out to serve it — the chat is the product and the pages are what you get when the chat breaks. Building it that way here would have made the argument I am trying to avoid making: that the interesting part is the model.

So the readable experience was built first and completely. Every project, role, and photograph has a server-rendered page that stands on its own, is indexable, and does not need JavaScript to be useful. The conversation reads from exactly the same corpus and can only do things a person could do by clicking. Turn generation off and nothing disappears.

Most questions are not questions for a model

A large share of what people actually ask a portfolio assistant has one correct answer that does not require synthesis. Show me the photography. What did you use for this. Are you available. Tell me about Kinnections.

All of those resolve deterministically here, before any provider is contacted: greetings, help, tag and filter changes, sorting, exact entity lookups, availability. They run against D1 and the approved tag vocabulary, they return the same result every time, and they do not count against the generated-turn budget — because spending a model call on a lookup is both slower and worse.

What is left for generation is the part that genuinely needs it: synthesis across several records, comparison, and questions phrased in a way no keyword match will catch. Those calls are grounded in retrieved evidence from the same corpus, and a personal claim that has no retrieved support does not get made.

The budget is finite and stated. When it is exhausted, deterministic search and every readable route continue working, which is the whole point of having built them first.

One home per fact

The hardest thing to keep honest in a system with a search index, a vector store, a cache, and a database is which one is right when they disagree.

The answer here is fixed in advance. D1 owns structured content and publication state. Full-text search is derived from it and is allowed to lag; when it disagrees with D1, D1 wins and the index gets rebuilt. A vector index is bound and specified on the same terms, and is not yet in the retrieval path. R2 holds processed media and nothing else — no untouched masters, ever. Project claims like this page live in checked-in, checksummed manifests in Git, which means a claim about a project cannot be edited without a reviewable diff. Each chat transcript belongs to its own Durable Object.

const sectionChunks = await Promise.all(
  manifest.sections.map(async (section, index) => ({
    id: `${documentId}:${section.id}`,
    documentId,
    headingPath: [manifest.title, section.heading],
    body: section.sourceText,
    position: index + 1,
    embeddingVersion: options.embeddingVersion,
    checksum: await sha256(
      canonicalize({
        heading: section.heading,
        sourceText: section.sourceText,
      }),
    ),
    canonicalUrl: `${manifest.route}#${section.anchor}`,
  })),
);

coding-manifests.tsShreyasdbz/shreyas-sane · packages/content/src/

The section you are reading becomes a retrievable chunk here, checksummed and addressed by its own anchor, which is how a citation lands on a paragraph rather than a page.

id
coding:portfolio-platform:sources-of-truth
heading path
This portfolio › One home per fact
position
3
cites
/explore/coding/portfolio-platform#sources-of-truth
embedding
pending-vector-v1
checksum
24e50f83df4f82a4705be2402109fdd58f7496773ac8b7d643cf6eaa00a11427

The record for the section you are reading, generated during this page's build by the function above. Change a word of it and the checksum moves.

None of that is unusual on its own. Writing it down before building was what stopped the usual drift where a cache slowly becomes the source of truth because it happened to be convenient.

Failure is a designed state

Quota exhausted. Provider erroring. Search index lagging. Stream interrupted halfway through a sentence. Someone who has turned off animation, or is reading at 200 percent, or is not using a pointing device at all.

Each of those is a state this site is designed to be in, not an exception it handles. The interrupted stream resumes from where the transcript actually got to, because the transcript is durable and the connection is not. Retrieval is lexical today — slower to impress than a semantic index, and never confidently wrong. The exhausted budget leaves a complete, navigable site.

Accessibility preferences work the same way: motion, contrast, type size, and gallery behaviour are read from one versioned model and applied to the document before the first paint, so nothing flashes and no individual component gets to invent its own setting.

The photographs are the hard part

Photography looks like the simple half of this site and is not. The requirements conflict: images have to be genuinely large, they have to be fast, they must not leak where they were taken, the original files must never be stored, and the grid must not reorder itself in a way that breaks keyboard and screen reader navigation.

So derivatives are produced in the browser before upload. A dedicated worker re-encodes fresh pixels — it does not copy the original's bytes, which is how location metadata would otherwise survive — into a fixed ladder of modern and fallback formats plus one download copy carrying rights metadata. The server validates the result again before anything is published, and the original never leaves the machine it was selected on.

The layout is CSS. There is no JavaScript deciding where images go, because a masonry implementation that repositions elements has changed the reading order for anyone not looking at it.

Four owners, no hidden handoff

Each kind of evidence has one home. Derived indexes can lag or be rebuilt without changing what the site considers true.

D1
Structured content, publication state, approved tags, and authoritative lexical retrieval.
R2
Immutable processed media and rights-bearing downloads, never untouched photo masters.
Git manifests
Reviewed project claims, section order, canonical routes, and checksums — including this page.
Durable Object SQLite
One bounded, resumable transcript per conversation session.