redact-03-local-service, after redact-02-first-ocr
Why can’t the browser just run the model?
QVAC’s engines are native code: compiled programs that use your CPU and GPU directly, plus model files hundreds of megabytes in size. A web page can’t load native code, and you wouldn’t want each browser tab downloading and holding a model of its own. So the model lives in one ordinary Node.js process on your machine, and the page talks to it over HTTP. That process is the local service. It’s the same idea as calling a cloud API, with one difference that matters here: the address is127.0.0.1, your own computer.
The page image goes from one program on your laptop to another, and nowhere else.
More on this design: About the local service.
What makes a boundary safe?
Anything that listens for requests can be sent bad ones, so the service gets a few firm rules from the start.
Both sides share one description of the request and response, written with Zod.
The browser checks what it receives with the same schemas the service uses to check what it’s sent.
1. Get this lesson’s code
Stop the app if it’s running. From the repository root,redact-course, bring in this lesson’s commit and install the new packages:
zod and tsx, so npm ci takes a minute or two.
To see every change, open the comparison on GitHub.
2. Read the contract
Opensrc/features/redact/analysis-contract.ts.
This is the request the browser will send for each page:
level and direction are what you chose on the privacy screen; nothing uses them yet, but they travel with every page from now on.
Each OCR block that comes back must look like this:
3. Read the analyzer interface
Openqvac-service/analyzer.ts:
qvac-service/index.ts plugs in a placeholder:
4. Read the server’s rules
Openqvac-service/server.ts.
The first thing every request meets is the origin check:
Origin header saying which site made the request.
If it’s there and isn’t a localhost page, the service answers 403 origin_forbidden and stops.
Tools like curl send no Origin, which is why you can test from the terminal.
Then the analyse route parses the body against the schema before doing anything:
400.
If the analyzer throws, for any reason, the browser gets 503 analyzer_unavailable instead of a half-made result.
The busy flag shows up in /health while a page is being analysed; lesson 08 uses it to turn away a second request.
readJson also stops reading after 20 MB, so a huge upload can’t fill your memory.
5. Start the service
REDACT_QVAC_PORT if you set it, otherwise 4317.
6. Ask for its health
In a second terminal:ready, but the OCR model is not_loaded and there’s no reasoning model at all.
A running process and a loaded model are different things, and the service reports them separately.
7. Send a bad request
8. Send a well-formed request
9. Pretend to be a website
-H "Origin: http://localhost:3101" instead, and you get the health response back.
Stop the service with Ctrl+C.
If something goes wrong
EADDRINUSEwhen starting: something else is on port 4317, maybe a service from an earlier lesson. Stop it, or start this one withREDACT_QVAC_PORT=4318 npm run qvac:service.curl: (7) Failed to connect: the service isn’t running, or is on a different port.tsx: command not found:npm cididn’t finish; run it again from theredactfolder.
10. Check your work
npm test reports Tests 42 passed (42).
The new tests in server.test.ts start a real server with fake analyzers and check each rule above.
So what just happened?
The browser now has somewhere to send its pages: a small service on your own machine, with clear rules about who may call it and what it accepts. It doesn’t read anything yet, and it says so instead of pretending.Try it yourself
Send a request whoselevel is "secret" instead of "private".
Before you run it, decide which error you expect; then check.
Then send pageNumber: 0 and see the same rule catch it.
Where to next
- Every file in this checkpoint:
redact/atredact-03-local-service - All routes and errors of the finished service: Local service API