orfloat

ORF-R-2026-006 · Preview Project

Niotebook: an open-source CS50 companion where the lecture, a real in-browser editor, and a tutor that knows your exact place in the video share one canvas

Can the lecture, the editor, and the help a self-taught CS student needs live on one surface, with a tutor grounded in the exact moment of the video rather than guessing, and every language running in the browser with nothing on a server?

Finding

Yes, and the proof is on screen: the lecture plays beside a real editor and terminal, and when the learner asks what the professor is explaining right now, the tutor answers from the transcript at that timestamp. Every language runs in the browser behind its own isolation primitive, and the learner's API key is encrypted inside a server boundary that never returns it.

A student teaching themselves computer science from an open course lives in a maze of tabs. The lecture plays in one. The editor is in another. A generic chatbot, which has no idea what the lecture just said, sits in a third. Notes go in a fourth. The thread of the lesson breaks on every switch. Niotebook is the argument that it does not have to be that way: the lecture, a real editor and terminal, and a tutor that knows exactly where the learner is in the video, on one surface. The recording below is one continuous take.

a walkthrough of the recorded demo

[one continuous screen recording. the niotebook wordmark, then the workspace,
 open on CS50x 2026, Lecture 2: Arrays. one canvas holds the lecture video, a
 code editor with a terminal, and the Assistant. the lecture source and its
 CC BY-NC-SA 4.0 license sit in the footer throughout.]

[code pane: scores.c, in C, the lecture playing beside it.]

  #include <stdio.h>
  int main(void)
  {
      int score1 = 72;
      int score2 = 73;
      int score3 = 33;
      printf("Average: %f\n", (score1 + score2 + score3) / 3.0);
  }

[terminal: the learner compiles and runs it, in the browser, no server.]

  $ make scores
  $ ./scores
  Average: 59.333333

[the Assistant pane opens, marked Live, headed "Lecture 2 / main.c". the
 learner asks a question about the exact moment of the video.]

learner: what is the professor explaining right now?
nio:     The professor is currently explaining how computers handle
         floating-point arithmetic and how variables are represented in
         memory. Specifically, around the 1:09:45 mark, the lecture covers:
         1. Integer vs. Floating Point Division. Dividing two integers
            performs integer division and discards the remainder, resulting
            in 0. To get a precise decimal, at least one value must be a
            float, which promotes the whole calculation to floating-point.
         2. Type Casting. Putting a data type in parentheses, like (float)3,
            tells the computer to treat an integer as a float.
         3. Memory Layout. An integer or a float typically takes up 4 bytes
            of memory, visualized as blocks on a grid.
         Next step: would you like to try writing a small program in main.c
         that calculates the average of three scores to see this promotion
         or casting in action?

[the layout switches to the three-pane preset, video, code, and Assistant side
 by side. a second file, main.c, runs in the browser and prints to the
 terminal.]

  printf("Hello,CS50\n");
  $ Hello,CS50
Figure 1. the recorded demo, one continuous take: a CS50 lecture plays beside an in-browser C editor and terminal, and the Assistant answers “what is the professor explaining right now?” grounded in the exact moment of the video, naming the integer-versus-float division, the casting, and the memory layout the lecture is on. nothing here runs on a server.

The overhang, one learner at a time

The lab has written before about the capability overhang: the widening gap between what the frontier can already do and what most people get from it. The argument there was that the gap does not close with more tools. It closes with embedding, with the work moved inside the actual workflow instead of bolted on beside it.

A self-taught learner has an overhang of their own. The lecture is good, the editor is real, the help is one prompt away, and yet they sit in different tabs, and the distance between them is where attention leaks and momentum dies. What a learner could absorb and what they actually do are two different curves, and the space between them is the understanding left on the table.

niotebook is that same embedding move, applied to one person learning. It does not add a cleverer chatbot to the pile. It puts the lecture, the editor, and a tutor that knows where you are into one place, so the help arrives in the same thought as the confusion. The claim is not that the model is smarter. It is that the workflow stops leaking, and over a course the two learners pull apart.

closing the learner’s capability overhangmasteryhighlowtime and effort over a coursewith niotebookwithout niotebookthe overhangniotebook closes it
closing the learner's capability overhang (schematic, not a measurement)
  axis x   time and effort over a course
  axis y   mastery, low to high
  with niotebook      starts low, rises steeply and accelerates
  without niotebook   starts low, rises slowly, stays low (the maze of tabs)
  the widening band between the two curves = the learner's capability overhang
  niotebook closes it by putting the lecture, the editor, and a grounded tutor in one place
  this is the shape of the argument, not a measured outcome
Figure 2. the learner’s capability overhang, drawn as the shape of the argument: the same student reaches higher mastery with the lecture, a real editor, and a grounded tutor in one place than in a maze of tabs, and the gap widens over a course. it echoes the lab’s note on the capability overhang, where the answer is embedding rather than more tools. this is a schematic, not a measurement: there is no learning-outcomes study here and the axes carry no units. the felt proof is the demo above, and the honest limits are below.

That picture is the thesis, not a measurement. We have not run a learning-outcomes study, and the axes carry no units. The evidence for niotebook is the working software above and the public repository below, not a curve we drew. The curve only names the gap the rest of this piece is built to close.

One canvas, not four tabs

The whole product is a single synchronized canvas. The lecture video, the code editor with its terminal, and the tutor sit in one workspace that the learner reshapes on the fly: one pane, two panes, or all three, by a keystroke. The point is not the layout. The point is that the three things a learner is doing, watching, writing, and asking, stop being three places and become one thought.

Underneath, the synchrony is deliberate engineering, not a coincidence of being on the same page. The video’s current time is broadcast through a small external store the panes subscribe to, so a tick of the playhead reaches the tutor without re-rendering the editor the learner is typing in. The lecture moves, the tutor’s sense of where the lesson is moves with it, and the code the learner is mid-keystroke on does not flicker. The canvas is the thesis the rest of the piece defends: keep the lesson, the runtime, and the help in the same place, and the friction that scatters a self-taught learner’s attention goes away.

A tutor that is grounded, not guessing

The tutor is called Nio, and the thing that makes it useful is also the thing most chatbots cannot do: it answers from the lecture the learner is actually watching. In the demo, the question is “what is the professor explaining right now?” and the answer names floating-point division, type casting, and memory layout, because that is what the lecture is on at that timestamp. It is not recalling a general fact about C. It is reading the transcript window around the playhead.

That grounding is a pipeline, and the load-bearing part of it is a cascade the server runs to resolve the lecture window itself, never trusting the client to supply it. The browser sends the request with the learner’s place in the video; the server authenticates it, rate-limits it, and neutralizes prompt-injection attempts in the message; then it resolves the transcript four ways in order, and the first that succeeds wins.

one question, four layers: the Nio AI tutor request pipelinethe browseruseChatThread builds the request, POST plus Clerk JWTNioChatRequestthe route: /api/niovalidate, then Clerk and Convex authvalidateauthrate limit, then scrubConvex rate limit (429 plus Retry-After),prompt-injection neutralized429neutralizethe four-tier transcript cascadeserver re-resolves the lecture window, never trusts the client1 client lines2 Convex segments3 SRT file4 YouTubefirst non-empty wins; all empty degrades to lesson meta plus codebuildNioContext (domain)assembles transcript window, current code, last error, recent chatbudget 11,776 charsdeterministic trim: drop history, truncate transcript, truncate codeSSE stream, BYOK failoverstreams over Server-Sent Events, fails over across providersGeminiOpenAIAnthropic10s first-token timeoutidempotent Convex persistthe grounded contextHash is stored with the reply
one chat message, four layers (the Nio request pipeline), top to bottom

1. the browser: useChatThread builds the request, POST plus Clerk JWT [NioChatRequest]
   v
2. the route, /api/nio: validate, then Clerk and Convex auth [validate] [auth]
   v
3. rate limit, then scrub: Convex rate limit (429 plus Retry-After), prompt-injection neutralized [429] [neutralize]
   v
4. the four-tier transcript cascade (server re-resolves the lecture window, never trusts the client):
     1 client lines -> 2 Convex segments -> 3 SRT file -> 4 YouTube
     first non-empty wins; all empty degrades to lesson meta plus code
   v
5. buildNioContext (domain): assembles transcript window, current code, last error, recent chat [budget 11,776 chars]
     deterministic trim: drop history, truncate transcript, truncate code
   v
6. SSE stream, BYOK failover: streams over Server-Sent Events, fails over across providers [Gemini] [OpenAI] [Anthropic], 10s first-token timeout
   v
7. idempotent Convex persist: the grounded contextHash is stored with the reply
Figure 3. one chat message, from the browser through validation, rate-limiting, the four-tier transcript cascade, the domain context build under a fixed character budget, and BYOK streaming, to an idempotent persist.

The discipline shows in two details. The transcript is handed to the model labelled as untrusted context, so a lecture that quotes an instruction cannot hijack the tutor. And the assembled context is held to a fixed character budget, shed in a fixed order when it overflows: the oldest chat history goes first, then the transcript is trimmed, then the code, and the current code and the lecture window are the last things to be cut. History is the cheap thing to lose. Where the learner is, and what they wrote, are protected.

The pedagogy is encoded too. Nio is a teaching assistant, not an answer key: it is told to refuse complete solutions to graded work, to never invent a transcript or a timestamp it was not given, and to say plainly when the lecture window is empty rather than fill the silence. A good CS section leader’s restraint, written down.

A real workspace, run in the browser

The editor is not a toy. It runs seven languages, JavaScript, Python, C, HTML and CSS, SQL, and R, and it runs all of them in the browser. There is no server that executes the learner’s code, which means there is no remote-code-execution surface to defend. The interesting part is that there is no single sandbox doing the work. Each language is isolated by the primitive that actually fits it.

seven languages, four wallsseven languages, four wallseach language isolated by a different primitivethe browser tab, no server, no RCECSP allowlist (the outer wall)JavaScriptnew Function in a blob: iframeopaque-origin iframeremove frameHTML / CSSsandboxed iframe (srcdoc)opaque-origin iframereplace frameCJSCPP interpreter in a Web WorkerWeb Workerworker.terminate()PythonPyodide WASM, main threadWASM memoryAtomics SIGINTSQLsql.js WASM, persistent DBWASM memorysynchronousRWebR WASM workerWASM memorywebr.destroy()Python (sandbox)Wasmer WASIX, editor-sandboxCOOP/COEP iframebridge timeoutshared floorevery executor enforces a timeout;output is streamed live
seven languages, four walls (in-browser code execution, no server, no RCE)

outer wall:  the browser tab, no server, no RCE
inner wall:  CSP allowlist
  JavaScript     new Function in a blob: iframe   [opaque-origin iframe] [remove frame]
  HTML / CSS     sandboxed iframe (srcdoc)        [opaque-origin iframe] [replace frame]
  C              JSCPP interpreter in a Web Worker [Web Worker] [worker.terminate()]
  Python         Pyodide WASM, main thread        [WASM memory] [Atomics SIGINT]
  SQL            sql.js WASM, persistent DB        [WASM memory] [synchronous]
  R              WebR WASM worker                  [WASM memory] [webr.destroy()]
  Python (sandbox) Wasmer WASIX, editor-sandbox    [COOP/COEP iframe] [bridge timeout]
shared floor: every executor enforces a timeout; output is streamed live
Figure 4. seven languages run in the browser with no server, each sealed by a different primitive: opaque-origin iframes for JavaScript and HTML/CSS, a Web Worker for C, WASM linear memory for Python, SQL, and R, and a COOP/COEP sandbox for the Wasmer path.

The honesty in the engineering is worth naming, because it is the kind of thing a marketing page would smooth over. C does not run on a compiler. It runs on JSCPP, an interpreter, inside a Web Worker, and the Wasmer path that does compile is an experimental terminal-only alternate, not the default. Each language gets the stop button that suits it: a JavaScript run that overstays is killed by removing its iframe, a C run is killed by terminating its worker, a Python run is interrupted by a shared-memory signal where the headers allow it. The shape is deliberately heterogeneous because the browser does not offer one clean way to run everything, and pretending it does would be the lie.

Your key, encrypted, never handed back

The tutor is bring-your-own-key. A learner can use every other feature without an API key at all, and if they want Nio, they paste their own key for Gemini, OpenAI, or Anthropic. The promise the architecture makes about that key is precise: once saved, the plaintext is never returned to the browser, by any code path.

the key the browser never gets backthe browseruser pastes a providerAPI key in settingsConvex actionSHA-256 derives a 32-bytenon-extractable key, a 12-byteIV per key, AES-256-GCMencrypt, plaintext transientthe boundaryat rest in Convexbase64 ciphertext plusits IV (userApiKeys)listHintsreturns a last-4 hintthe model providerdecrypt only inside the relayaction, HTTPS to the provider,never echoed to the clientkeyHint: last 4plaintext keyciphertext + IVtransient plaintextat request timethe decrypted keyno returnthe hint only, back to the browser
the bring-your-own-key vault (the plaintext key is never returned to the browser)

  the browser            user pastes a provider API key in settings
    | plaintext key
    v
  Convex action (the boundary): SHA-256 derives a 32-byte non-extractable key;
    a random 12-byte IV per key; AES-256-GCM encrypt; plaintext is transient
    | ciphertext + IV
    v
  at rest in Convex      base64 ciphertext plus its IV (userApiKeys)

  back to the browser:   listHints returns a last-4 hint only [keyHint: last 4]
  the decrypted key:     never returned [no return]

  at request time:       decrypt only inside the relay action [transient plaintext],
                         HTTPS to the model provider, never echoed to the client
Figure 5. the bring-your-own-key vault: a provider key is encrypted inside a Convex action with AES-256-GCM and a per-key IV, only a last-4 hint is ever returned, and the plaintext is decrypted solely inside the server-side relay, never handed back to the browser.

The guarantee is structural, not a matter of remembering to be careful. The key is encrypted inside a Convex action with AES-256-GCM under a key derived by SHA-256, with a fresh random initialization vector per key. What rests in the database is ciphertext. The only thing the client can ask for back is a four-character hint, enough to recognize which key is saved, useless to anyone who steals it. The plaintext exists for a moment, inside the server-side action that relays the call to the provider, and nowhere else. This is the bring-your-own-key promise made literal: the learner keeps control, their usage is their own and transparent, and there is no mystery layer between them and the model.

The file tree that lives in your browser

A workspace needs files, and niotebook’s files live in the browser too. There is a virtual filesystem, an in-memory tree with the ordinary ceilings of a real one, that the editor and terminal read and write, and that persists to the browser’s own IndexedDB so a lesson survives a refresh and a return. There is no file store on a server.

  editor + terminal          VirtualFS                IndexedDB
  (React + Zustand)          in-memory Map tree       niotebook-vfs
        |                         |                         |
        |  write / rename         |                         |
        |------------------------>|  1 MB per file          |
        |                         |  50 MB per project      |
        |                         |                         |
        |                         |  snapshot()             |
        |                         |---- save, debounced --->|  keyed by lessonId
        |                         |                         |
        |                         |<------ restore on load -|
        |                         |                         |
  on a write failure the store stays in memory; nothing is lost mid-session
Figure 6. the server-less virtual filesystem: the editor and terminal write an in-memory tree with per-file and per-project ceilings, a debounced snapshot persists it to the browser’s IndexedDB keyed by lesson, and a failed write degrades to memory rather than losing the session.

The discipline underneath

None of this is held together by good intentions. The codebase is split into four layers with the boundaries enforced in continuous integration, not just documented. The domain layer is pure: a check in CI fails the build if it imports React, Convex, or anything with a side effect, and the same pure modules are the single source of business logic shared by both the web app and the backend, so there is no second, drifting copy. The backend forbids the any type by a script that fails the build. The result is a system where the rules about how it is allowed to be built are themselves tested.

We will not pretend the contract is perfect, because the repository does not. A handful of infrastructure files still hold thin React hooks the layer rules say they should not, a real gap between the contract and the code, left visible in the open rather than quietly papered over. A lab that publishes its own delta is more believable than one that claims none.

Where this honestly stands

This is an open beta, and it has rough edges we would rather name than have you find. The editor reads the filesystem when it opens a file and writes when it saves, but it does not yet listen for changes made underneath it, so a file rewritten by the terminal while it is open in a tab updates the tree without updating the open editor until the tab is reopened. Python runs on the browser’s main thread, which can briefly freeze the tab on a heavy program where the headers needed for a worker are not present. An external adversarial review of the codebase, run earlier in its life, graded it in the middle of the range while crediting its layering and its consistent typing, and we keep that review in view rather than out of it.

There is a licensing nuance that is not a footnote but a design principle. The niotebook software is MIT licensed: take it, run it, change it. The CS50 course content it draws on, the transcripts and the lecture metadata, is licensed by Harvard under Creative Commons Attribution-NonCommercial-ShareAlike. Non-commercial. That single word is why the open, free, bring-your-own-key shape is not a phase to be grown out of but the honest fit: the project cannot and does not commercialize the courseware, so it does not try to. The constraint and the design agree.

What transfers

The lessons here are not about CS50, and not even about education. Grounding beats generation: an assistant that reads the transcript at the learner’s timestamp is more useful than a cleverer model guessing, and the engineering that matters is the cascade that resolves the right context, not the prompt. Isolation belongs at the boundary you actually control: there is no single sandbox for seven languages, so each gets the primitive that fits, and the system is more honest for refusing to pretend otherwise. And a secret should be encrypted at the boundary that holds it, inside the server action, never handed back to the client that supplied it. Build a learning tool that way and the proof is the learner asking what the professor just said and getting a true answer.

If your operation has a body of knowledge people need to learn against, and a frontier worth grounding an assistant in, this is what embedding AI into that can look like when the guardrails are real. Start a conversation, and we will scope it.

The honest state of AI in learning

It would be easy to oversell this. The category is loud right now, and most of the noise is a promise that an AI will do the learning for you: paste the assignment, take the answer, move on. That is not a tutor. It is an answer machine, and it tends to hollow out the very struggle that turns watching into understanding.

niotebook is built against that grain on purpose. The tutor refuses complete solutions to graded work, grounds every answer in the lecture the learner is actually on rather than a general recollection, and hands back the next step instead of the finished one. The bet is that the useful place for a model in education is beside the work, not in place of it. We think that bet is right. We do not yet know that it is, and we are not going to pretend a drawn curve settles it.

What is honestly true today is narrow and worth stating plainly. AI can keep a learner from getting stuck on the wrong thing at the wrong moment, and it can do that grounded in where they are. Whether that compounds into people who learn more, and hold it longer, is a question only real use over real courses will answer, and the answer will not come from the people selling the tools. So we are building this one in the open, the repository and its history there to be read, because the education sector does not need another confident story about AI. It needs tools that keep the learner in the work, shown honestly enough that someone can check.

References

The systems this preview is built on, each linked to its primary documentation, and the repository itself: