TeachBack

How TeachBack is built

TeachBack is a Hebrew-first learning tool where the student explains a topic out loud and the AI only asks questions. This is the engineering side: the decisions, the architecture that enforces them, how the work is checked, and what is deliberately not finished.

New here? The English overview explains what the product is in about a minute, and the worked example shows a full session end to end with nothing to sign up for.

The shape of it

Client
Vite and React, a single-page app. Hebrew-first and right to left, with an English mode.
Server
Express, in the same container as the built client. One deployable unit, so there is no version skew between the two.
Voice
A real-time model session over a WebSocket the server owns. The browser never holds a provider credential.
Data
Postgres through Supabase, with authentication and row-level rules in the same place as the data.
Public site
Static HTML on a CDN with no build step. It has no dependency on the app, so it stays readable when the app is down.

Six decisions worth explaining

Two of these are running in production and can be checked from outside today. The other four are implemented and in review, not deployed. Each one says which it is, because a decision described in the past tense reads as shipped, and four of these are not.

1. The deployment declares what it is allowed to do, and refuses to start otherwise

Implemented, in review. Not deployed.

A single variable states the mode: serve the static sample, or make real model calls. In production the server refuses to boot when it is unset.

Because the health check is memory-only and always answers 200. An unset mode would deploy green while voice, evaluation and upload were all quietly dead, and neither the uptime monitor nor the platform health check could see it. Failing to start is loud. Starting wrong is not.

2. Exactly one place constructs the model client

Implemented, in review. Not deployed: the deployed server still constructs four.

Four independent SDK clients became one gateway. Every call goes through it, with its timeout, its cancellation signal and its output ceiling.

Because a limit that lives in four places is four limits, and the weakest one is the real one. Consolidating it made a single grep the proof: one construction site, or the change did not land.

3. Routes that are out of scope are removed, not defended

Implemented, in review. Not deployed.

Seven route prefixes answer 410 Gone, from a declaration of what the deployment no longer offers. The check runs before any body parser or file handler.

Because a route you do not serve cannot be exploited, and the cheapest place to stop a request is before anything reads its body. 410 rather than 404 is deliberate: it says removed on purpose, not missing by accident.

4. Every route states its own request ceiling

Implemented, in review. Not deployed: the deployed server still mounts one global 50MB parser.

One global 50MB JSON parser became a 64kb default, with a larger allowance only on the one route that needs a transcript, plus explicit caps on transcript length.

Because a single global ceiling is set by whichever route needs the most, and every other route silently inherits it. The route that accepts a paragraph should not accept fifty megabytes.

5. The public site publishes by allowlist, not by blocklist

In production since 17 September 2026.

The static site serves its repository directly, so its ignore file names what may be published and denies the rest.

Because a blocklist silently re-exposes every new file, and a repository that gains internal notes and planning documents over time will eventually publish one. An allowlist fails the safe way: something new is invisible until someone says otherwise.

6. The client build and the server build do not share a directory

In production since 20 September 2026.

The client compiled into dist/ and the server into dist/server/, and dist/ is the directory the app serves statically. Every compiled backend file was therefore downloadable from the public origin. The two outputs are now siblings and the static root points at the client one explicitly.

Because this one was not theoretical. The route handlers, the environment module and the server entry point each returned real JavaScript with a 200. It was found by asking what the running container actually serves rather than what the build script intends, and the fix is confirmed the same way: those paths now return the client shell, byte for byte identical to a path that never existed. A status code alone could not have shown this, because the app answers unknown paths with its own page.

How the work is checked

Three habits do most of the work here, and all three come from getting it wrong first.

What this does not claim

There are no usage results on this page. The learning research the product is built on is real and cited on the overview, but it studies retrieval practice and learning by teaching as methods. None of it is a study of this product, and there is no measured cohort here to report. Numbers about how well TeachBack works would have to come from real sessions, and that evidence does not exist yet.

See it rather than read about it

The worked example Open the app

The worked example needs no account and no microphone. The app needs both, and is Hebrew-first. There is a language switch inside the app, but not on the sign-in screen you land on first.