How this site works

This site is a small full-stack system. All my content compiles into a SQLite database when the site builds, and you can query it three ways: ask a question in plain English and a model writes the SQL, write SQL yourself in the console, or use the public JSON API. This page explains how each part works, what it costs, and what broke along the way.

Tables
14
Model calls a month, at most
2,000
Tokens a question sends
4,034
A month at the cap, at most
$13.19
Eval questions on every push
59

Architecture

ArchitectureAt build time, the site's Markdown and YAML become a SQLite database and JSON endpoints. Your browser loads the database with sql.js when you need it. Your question goes to /api/ask, which sends it with the schema to a language model, checks the SQL that comes back by preparing it against the real database, and returns the SQL to your browser to run. The model never runs a query and never sees the results.Markdown and YAMLat build timeSQLite databaseJSON endpointswhen you need ityour browsersql.jsyour question/api/askpreparing it againstthe real databasethe schemaSQLlanguage modelThe model never runs a queryand never sees the results.ArchitectureAt build time, the site's Markdown and YAML become a SQLite database and JSON endpoints. Your browser loads the database with sql.js when you need it. Your question goes to /api/ask, which sends it with the schema to a language model, checks the SQL that comes back by preparing it against the real database, and returns the SQL to your browser to run. The model never runs a query and never sees the results.Markdown and YAMLat build timeSQLitedatabaseJSONendpointswhen you need ityour browsersql.jsyour question/api/askpreparing it againstthe real databasethe schemaSQLlanguage modelThe model never runs a queryand never sees the results.

If you run curl on my home page over https, you get my resume as plain text instead of the page. A lot of developers live in the terminal, so I wanted the site to answer them there too. wget, HTTPie and PowerShell get it as well.

curl -A curl/8.0 https://alexkachur.com

The first lines of what comes back:

ALEX KACHUR
Full-stack & AI developer
Toronto, Canada · alexkachur98@gmail.com · github.com/AlexKachur98 · linkedin.com/in/alex-kachur

I build web apps with an AI doing real work inside them.
Final-year Software Engineering Technology student at Centennial College.
Available full-time from May 2027.
...

Build-time database

All my content starts as Markdown and YAML, plus a few values read from the code itself, like what the site stores and for how long. When the site builds, scripts/build-db.ts reads it and writes one SQLite database with 14 tables, plus a readable SQL dump you can open below. The same build renders the JSON endpoints and writes my plain-text resume, both from that database. If a row is missing its id or points at something that does not exist, the build fails instead of quietly dropping it. A test builds the database twice from the same content and checks the two are identical byte for byte. Same content, same database, so the fingerprint in the address only changes when the file's bytes do.

/data/portfolio.sql

The ask pipeline

Here is what happens when you ask a question. First the question gets checked, and one under 3 or over 200 characters, after trimming, stops there. Then there is a kill switch I can flip without touching the code: one setting and a redeploy. Then a rate limit of 10 questions a minute per address, counted under a scrambled form of your address, and sending a question to me counts against the same limit. Next is the cache: if the same question was asked recently, you get that answer and no model is called, even in a month when the budget is used up. If not, the monthly cap gets checked, and a model call is reserved before it is made, so retries and failures count too. Only then does the question go to the model, together with the schema, a one-line description of each fact, the pages and headings of the page sections, and 11 worked examples. Apart from those labels, the CHECK lists and example values in the schema, and the examples, it never sees what is in the database, or any results. The SQL it sends back gets validated, and if SQLite rejects it with time left for a second call, the model gets one retry with the error. The SQL that passes comes back to your browser, and sql.js runs it there, on the same database you can download. If the site cannot answer, you can choose to send me the question, and only then is it kept, for 90 days.

The model behind /api/ask is one environment variable, and Anthropic publishes each model's earliest retirement date, so a swap is a change of that variable and the default in the code, a re-recorded eval, and the prices on this page.

What the validator rejects and why

I do not trust the SQL the model writes, and I do not try to guess whether it is safe with a long list of rules. The first pass is simple: the SQL must be one SELECT or WITH statement with no comments, and a word that never belongs in a read-only query, like INSERT, DROP or PRAGMA, gets it rejected right away. Then the real check. The SQL is prepared against the actual database, which means SQLite parses and compiles it without running it. Bad syntax, tables that do not exist and columns that do not exist all fail right there. That is what I mean when I say the validator is the database: SQLite already knows exactly what is valid, so I let it decide. And your browser runs it on its own copy of the database, with SQLite's query_only flag set before every statement, so nothing that runs there can change it.

If you find a hole in any of this, SECURITY.md in the repository says how to tell me.

Cost and the cap

Every question that reaches the model costs a little money, and a public text box is an easy thing to abuse. So the site has a hard monthly cap of 2,000 model calls. When it is reached, the Ask box tells you this month's budget is used up, and everything else keeps working: the examples, the console and the whole database still run in your browser, because they never needed the model. Cached answers do not count against the cap.

One question sends the model about 4,034 tokens: the schema, the fact keys with their descriptions, the section pages and headings, the worked examples, the question and the shape of the answer. That is the largest first call in the recorded eval. The answer is capped at 512 tokens. At Anthropic's published price for Claude Haiku 4.5, $1 per million input tokens and $5 per million output tokens as of 2026-09-25, a month at the cap costs at most about $13.19, with every answer at its full length. A retry sends the failed answer back with the error, so a month with many retries costs a little more.

The prompt is not cached. Anthropic's prompt cache needs at least 4,096 tokens on Claude Haiku 4.5, checked the same day, and this prompt is under that, so a cache marker would do nothing. It would not pay anyway: a cached prompt lasts five minutes unless another question refreshes it, a cache write costs a quarter more than the prompt it saves, and this site sees far fewer than one question every five minutes.

Caching

Answers are cached in Redis for 30 days, refusals for 1 day. The key is built from the prompt version, a fingerprint of the database schema and a hash of your question after it is normalized, so the moment a schema change or a bump of the prompt version goes live, old answers stop matching. The cache stores the SQL and its one-line explanation, never the question itself or who asked, though the SQL and the explanation can repeat words from the question. The full list of what the site keeps is on the API page. The numbers in the footer come from /api/stats. The CDN keeps its answer for 60 seconds and may serve it for 300 more while it fetches a fresh one, so the footer can be up to that far behind. That is fine for a counter.

Performance

Nothing heavy loads until you ask for it. sql.js and the database only download once you click or tab into the Ask box or the console, so the first visit stays fast, even on a phone. The cost is that the database may still be loading when the model answers, so your first question can take a moment longer, which is why the Ask box shows its status the second you click.

Fast is a claim, so here are the numbers. Lighthouse 13.5.0 on 2026-09-25, mobile preset, median of five runs in headless Chrome, on the live site: the home page scores 100 for performance, 100 for accessibility, 100 for best practices and 100 for SEO, with a layout shift of 0. This page scores 100, 100, 100 and 100 in the same order, with a layout shift of 0. The Uraz Hoops case study, the third page measured, scores 100, 100, 100 and 100.

What broke

My eval replays 59 recorded questions on every push. One of them, "What is Alex studying and where?", had been failing since the first recording, and CI stayed green the whole time, because the replay only failed when a recording went stale, not when an answer was wrong. A test that cannot fail is not testing anything. When I dug in, there were two causes. My school only existed inside a sentence, so no query could return it, and the model read "where" as the city, because my own worked example taught it that. My first fix accepted the model's reading: I changed the expectation to the city and a course, added a school fact and a separate question that asks for the school outright, and changed the replay so a wrong answer now fails CI. About 38 hours later I reversed the first part. Someone asking where I study wants the school, so the test had been right and the prompt wrong. I gave every fact a one-line description the model sees, added a worked example for questions about studying, and put the school back in the expectation. If I started again, I would write the eval expectations and the data model together.

The database file was cached by browsers for a long time, but its address never changed. So after a deploy, a returning visitor could keep querying the old database without knowing it. I caught it before it hit anyone. Now the address carries a fingerprint of the file's contents: new data gets a new address and the browser fetches it, and unchanged data stays cached. The readable dump's link was the exception: it went out at the fixed address for about nine hours on 2026-09-22, on the vercel.app deployment, four hours before the domain was connected.

What I left out on purpose

My first portfolio was over the top with animations and decorations. I thought the wow factor was what mattered. After researching what employers and senior developers actually look for, I realized it is all about function: showing what I can build. So this one has nothing that animates on its own, no glow and no decoration that does not do a job.