Product design and AI engineering · Internal tool
Role
Product design, product management, architecture, and AI direction
Stack
Vanilla JS SPA, Vercel Edge Functions, Claude API, WebCrypto sessions, Normalidad VF
Built
Nine weeks, June to August 2026 · 55 commits
Access
Internal, gated to @starkfuture.com accounts · Private repo

The landing screen: one search across every knowledge base article, LED table, flow chart, and PDF.
The problem
The Stark VARG is a software-defined motorcycle. It has firmware, a companion phone app, a battery and charging system with its own failure language, diagnostic tools, and four model variants that behave differently from each other. Supporting it after the sale means holding a lot of specific, safety-relevant detail in your head.
That detail existed, it just wasn’t anywhere you could get to fast. Diagnostic procedures sat in one PDF, warranty criteria in another, LED meanings in a third, tool guides somewhere else, and a good chunk of it lived only in the memory of whoever had seen the fault before. So two agents could answer the same question differently depending on which file they opened, and advice written for an MX 1.0 got applied to an EX because nothing tied guidance to a specific bike. New staff had no front door at all.
The bet I made going in: agents don’t want to read a manual, they want a trustworthy answer to the question in front of them, with the source attached so they can check it before they touch a high-voltage connector.
What I decided before writing a prompt
No framework, no build step, no database. The whole app is one index.html. That ruled out a lot of things I might have wanted later, and it bought the one thing that mattered more: anyone who inherits this can open the file and read it. A React app with a bundler would have been a maintenance liability the moment I stopped touching it.
Grounded answers only, with citations, or no AI at all. A chatbot that improvises a torque value in a workshop is worse than no chatbot. Every factual claim carries a source tag that resolves to a real article or PDF in the portal.
Auth on day one, not later. The Claude key and the session check went into the first commit alongside the UI, because a tool holding internal service documentation does not get to defer that.
Support and diagnosis only. I researched what KTM, Zero, and LiveWire give their dealers and wrote an explicit list of what we would not build: warranty claims, parts ordering, unit registration, customer self-service, and rebuilding the diagnostic tools that already work. Every one of those is somebody else’s system.
Architecture
A static single-page app plus four Edge functions. The browser gets HTML, CSS, JS, and the knowledge base inline. Anything that needs a secret runs on Vercel.
Single-file vanilla SPA
No build step means no toolchain rot, and the whole app is greppable by whoever comes next.
Vercel Edge Functions
Four small handlers were all I needed, and Edge gives me WebCrypto without pulling in a crypto dependency.
WebCrypto HMAC session cookies
Signed sessions with zero dependencies, so there is no auth library to keep patched.
Claude API, proxied server-side
The key stays in an env var, and api/chat re-checks the session on every call.
Knowledge base as JS consts
50 articles live in the file they are served from, which is fine at this size and is the biggest thing I would change.
Normalidad VF, self-hosted
One variable font file drives both weight and width, so headings render on first paint instead of after a font request.
Streaming through the proxy, not around it. The obvious shortcut is to call Anthropic from the browser and skip the server hop, but that puts the API key in the page. So api/chat.js sits in the middle and pipes the SSE body straight back to the client without buffering. The response streams token by token and the key never leaves the server.
Two builds from one file, on purpose. The hosted build ends with a real auth block. The local one ends with hideLogin(); initApp(); so I can open it straight off disk with no server running. They are otherwise identical, and the rule I set for myself is that a diff on the two should show nothing but that 27-line tail.
1 · Browser
index.html serves the UI and 50 KB of articles inline.
2 · Edge / login.js
Checks domain and password, returns an HMAC-signed cookie.
3 · Edge / chat.js
Verifies the cookie on every call, then proxies to Claude.
4 · Claude API
Cached system block in, SSE stream back to the browser.
How I directed the AI
The system prompt was the spec. Most of my thinking went into a prompt builder rather than into code. buildSystemPrompt() assembles the portal map, all 50 articles, the LED and warning tables, the flow charts, and the extracted PDF text into roughly 30,000 tokens, then wraps it in rules I wrote by hand. Three of them, verbatim:
Answer only from the portal content below. Do not invent procedures, torque values, pin numbers, part names, or DTC codes. If something is not covered, say so plainly and recommend escalating to Stark Technical Support.
Preserve safety-critical details exactly: PPE, discharge wait times, HV connector timing, and the 10-attempt limit. Never soften or skip them.
The parts manuals are drawings: their part rows and torque values are not in this prompt, so name the manual and section and tell the user to read the table in the PDF. Never guess a part number or a torque figure.
Those are not prompt-engineering tricks, they are product decisions about what the tool is allowed to say.
One concern per commit, one screen at a time. The build went feature by feature, which is why the history reads like Add Diagrams subcategory, then Rename Diagrams to Parts Manuals, then Fix sidebar section detection. Small enough that when something broke I knew exactly what did it.
Written architecture notes, kept current. I maintain a working doc that says which files exist, why there are two of them, how the citation resolver works, and which PDF extractions are known-bad and must not be regenerated automatically. Every session starts from that instead of the model re-deriving the project.
Context discipline. The parts manuals are about 344,000 characters of drawing tables. They deliberately stay out of the prompt.

Model choice (Sonnet, Opus, Haiku) runs server-side; the key never reaches the browser.

Access is gated to Stark Future accounts, checked on the server.
What I caught and changed
Directing a model this way means most of the work is review. A sample of what it generated, why it was wrong, and what shipped instead:
A sign-in gate that ran in the browser
Hiding the login overlay client-side is theater. Open devtools and you are in, and the knowledge base was already in the page.
Moved the domain and password checks into api/login, and made api/chat re-verify the signed cookie on every request.
=== on the password, and a session cookie missing flags
String comparison leaks timing, and a cookie without HttpOnly and Secure is one XSS away from being someone else’s session.
Wrote safeEqual as a constant-time compare, and set HttpOnly, Secure, SameSite=Lax with a 7-day expiry.
Email domain treated as identity
Ending in @starkfuture.com is an attestation, not proof. It is a filter, not a lock.
Kept the check but moved it server-side, and wrote in the README that the password is the real lock and SSO is the fix.
Parts manual data extracted from the PDFs
The QTY and NM columns do not separate reliably from the text layer. A wrong torque figure on a motorcycle is a safety problem, not a bug.
Cut all ~1,160 part rows from the prompt. The manuals carry a section index only, and the prompt forbids stating any part number or torque value.
A chat dock that posted its raw transcript
The Messages API rejected it: consecutive same-role turns, empty content, transcripts starting on an assistant message.
Wrote a server-side normalizer that drops junk roles, collapses same-role turns, caps at 24 turns, and requires ending on a user message.
Every request re-reading the full system block
The knowledge base is identical on every call and about 30k tokens, so the dock paid full price for the same text repeatedly.
Marked the system block cache_control: ephemeral.
The auth mistakes bother me least in hindsight, because that is exactly the class of thing you expect and exactly why you review. The parts-manual one is the one I think about, because the extraction looked fine. Fault codes came out split across columns, obviously broken once you check them against the rendered page, and completely invisible if you do not.
Design
The design work was most of the job, and the piece I would point at is the wait.
An AI answer takes a few seconds. The default is a spinner, which tells the user nothing and quietly asks them to trust the output. Instead the answer view runs a four-step checklist that advances and completes visibly: reading your question and the symptoms, searching the technical and service docs, checking safety notes and model differences, writing the answer with sources. It is honest about what the system is doing, and it does the real work of the feature, which is convincing a technician that the answer is worth acting on.
The other decision that carried weight is that every article is tagged to the models it applies to, and the knowledge base has a model filter across all four variants. That directly attacks the failure mode where the right fix gets applied to the wrong bike.
Underneath: a dark gold-on-black system on CSS custom properties, a self-hosted variable display face driven on both weight and width, an 8px spacing scale I standardized everything to in a single pass, and a left sidebar that reshapes to whichever section you are in and highlights the exact document you are reading.

Instead of a spinner, the answer view runs a visible four-step checklist while it works.

Every factual claim carries a numbered source that resolves to a real article or PDF.

The dock knows which screen you are on and keeps the thread.
Where it landed
Deployed on Vercel, behind sign-in, holding 50 articles across 12 categories, 5 diagnostic flow charts, and a document library of parts manuals, wiring diagrams, firmware notes, and the DTC reference. Two AI surfaces run off the same prompt builder: the full-page search on the landing screen, and a chat dock that knows which screen you are on and resolves “this step” from what is open in front of you.
I do not have adoption numbers yet and will not invent any. The thing I would most like to know is which questions the assistant fails to answer, because that list is the best possible map of what is missing from the knowledge base. It is not instrumented yet.

Diagnostic decision-tree flow charts.

LED and warning indicator reference.

Internal tools and processes.

Bike documentation hub.
What I would do differently
Content should never have lived in the HTML. It was the right call for the first ten articles and the wrong one by the fiftieth. Adding an article means editing a 3,886-line file and redeploying, with no version history per article and no review step. Getting content into structured files is the change that unblocks almost everything else on my list.
A shared password was a launch decision that is overdue. It gives no per-person identity, which means no audit trail, no roles, and nothing VIN-specific or dealer-specific later. SSO restricted to the domain only touches the login function, so the cost of the delay is the features it blocks, not the migration.
I would instrument the AI search from day one. Not for a dashboard, for the gap list. Two months of what agents asked that we could not answer would have been the highest-value document on the project, and I have none of it.