In lesson 02 a script read the chat image from the terminal. But Redact is a web app: the page you upload lives in the browser, and the browser can’t run the OCR model. So before the model can help, we need something that sits between the two. Checkpoint: 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 is 127.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:
This lesson adds zod and tsx, so npm ci takes a minute or two. To see every change, open the comparison on GitHub.

2. Read the contract

Open src/features/redact/analysis-contract.ts. This is the request the browser will send for each page:
One page at a time, as a PNG encoded as base64 text, with its size in pixels. 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:
That’s lesson 02’s output, written down as a rule: text, four finite numbers for the box, and a confidence between 0 and 1.

3. Read the analyzer interface

Open qvac-service/analyzer.ts:
The service doesn’t know about QVAC at all. It only knows that something can report its state, analyse a page, and shut down. That lets us test the service with fakes, and swap in the real model next lesson without touching the server. At this checkpoint, qvac-service/index.ts plugs in a placeholder:
It’s honest: the OCR model isn’t loaded, and asking it to analyse a page fails.

4. Read the server’s rules

Open qvac-service/server.ts. The first thing every request meets is the origin check:
Browsers send an 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:
Bad input gets 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

You should see:
Leave it running. The port comes from REDACT_QVAC_PORT if you set it, otherwise 4317.

6. Ask for its health

In a second terminal:
The service is 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

An empty body never reaches the analyzer.

8. Send a well-formed request

This one passes the schema and reaches the placeholder, which throws. Instead of inventing OCR results, the service says it’s unavailable.

9. Pretend to be a website

Try -H "Origin: http://localhost:3101" instead, and you get the health response back. Stop the service with Ctrl+C.

If something goes wrong

  • EADDRINUSE when starting: something else is on port 4317, maybe a service from an earlier lesson. Stop it, or start this one with REDACT_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 ci didn’t finish; run it again from the redact folder.

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 whose level 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

Next: Send uploaded pages to local OCR (lesson 04)