Introduction

Newspapers are unusual in how they have throughout modern history recorded daily events and stories that otherwise would have faded into oblivion never to be known by anyone. Well, despite that, newspaper readership has declined considerably over the years and the stories despite being recorded never get read. Atlas bridges this gap by transforming it into an interactive medium with Reactor that increases the instructional value of these records and turns them into extremely valuable and captivating learning materials.

Inspiration

A historical event is three things: a place, a time, and a set of things that were true at that place and time. Every format we use to teach one throws away the first. A paragraph describes a place. A photograph gives you one frame of a place. A documentary walks you along a camera operator's path through a place. None of them let you look left.

For most of the history of media that was a hardware fact rather than a choice. Building a walkable 1969 lunar surface, or the Bornholmer Strasse crossing on the night of 9 November 1989, meant an art team, several months, and a budget only a film studio could carry. So the past stayed flat, and we all agreed to call reading about a place the same thing as knowing it.

Meanwhile the formats that carry history have been quietly losing the room.

Signal Number Source
People who still reach news in print, down from around half in 2013 10% Reuters Institute, Digital News Report 2025
People who say they sometimes or often avoid the news, a record high 40% Reuters Institute, DNR 2025
18 to 24 year olds who avoid the news, up from 30% in 2017 42% Reuters Institute, DNR 2025
People who trust most news most of the time, flat for three years 40% Reuters Institute, DNR 2025
US adults getting news from social and video, now ahead of TV at 50% 54% Reuters Institute, DNR 2025
Share of all web traffic that is automated rather than human 51% Imperva, 2025 Bad Bot Report
US eighth graders scoring below NAEP Basic in US history 40% NAEP, 2022

Read together, those numbers say something blunter than any one of them says alone. Nine in ten people have stopped touching print. Four in ten steer around the news on purpose, and among 18 to 24 year olds it is more than four in ten, up twelve points in eight years. Trust has not moved in three years. Attention has migrated to short video and social feeds, where more than half of the traffic is now machines talking to machines. And in the classroom, 40% of American eighth graders cannot reliably pull a simple idea out of a primary source.

Every fix the industry has tried swaps one passive format for another. Longer video. Shorter video. Vertical video. Newsletters. None of it worked, because the problem was never the resolution of the picture. It was that nothing ever asked the reader to do anything.

The one intervention with a consistent, replicated effect size is participation. Freeman and colleagues pooled 225 studies in PNAS and found that students in traditional lectures were 1.5 times more likely to fail than students in active learning sections, with examination performance up 0.47 standard deviations. Making somebody do something is the only lever in education research that keeps working across two hundred replications.

Two things changed at once this year. Real time world models made the cost of a place collapse from studio months to cents per second. And the archives finished digitising: the British Newspaper Archive has passed 100 million pages, the Library of Congress's Chronicling America holds more than 23 million, and essentially nobody outside a research library opens either.

So the raw material is sitting there, fully sourced and fully licensed, and the technology to stand inside it finally exists. That is the whole reason Atlas exists. Not a better article about the Berlin Wall. Ninety seconds standing at the checkpoint on the night the gate opened, with the source for every detail one keypress away.

What it does

Atlas is a newspaper you can walk into.

Each article in the archive carries an archival photograph and a structured scene brief written against sources. Atlas compiles that brief into a layered prompt, refuses to serve it if any part of the prompt cannot be traced back to a citation, mints a short lived scoped token, and hands the browser a plan it can drive. The reader clicks the play control on the photograph and walks around inside it in first person.

Ten events ship in the archive today: Apollo 11, the night the Berlin Wall opened, the eve of D-Day at Greenham Common, the March on Washington, the midnight of Partition, the Armistice at Compiegne, the San Francisco relief camps of 1906, the Triangle fire mourning march, the Bandung Conference, and the Windrush arrival at Tilbury.

Control Does
W S walk forward and back
A D strafe, or turn on a model with no lateral axis
mouse, or drag look, with pointer lock where the browser grants it
two finger trackpad look, without holding a button
arrow keys look, without a mouse at all
space / C jump / crouch
1 to 9 hold a sourced event from the article
Esc leave the world

The event list set in the left margin, curving away from the selection, with no panel or background behind it

The reading surface has no navigation at all. There is a wordmark, a list of events in the left margin, and a paper in the middle of the screen that only ever moves up and down. Whatever the list lands on is already open, so there is no confirm step and no panel to dismiss. Where a real scan of the original front page exists, it sits inside the same vertical scroll as the story, full width, credited to the library that holds it. On a phone the margin list becomes one horizontal row above the same single column of paper, and inside a world thumb pads replace the keyboard, chosen by (pointer: coarse) rather than by screen width, because a narrow browser window on a laptop is not a phone.

The part we care about most is what the world is not allowed to do:

  • Every world is anchored on a real archival photograph, cropped to the model's native 1664x960 frame, with its licence and credit preserved and shown in the interface.
  • Prompts are compiled from structured briefs, never hand written, with landmarks pinned by explicit count and camera behaviour bound to reader input.
  • A fidelity linter blocks absence phrasing, camera direction that has escaped the camera layer, over budget prompts, and any hold key event that cannot cite a source. The test suite fails if the shipped archive produces so much as a warning.
  • Sensitive events carry a content note naming what the world deliberately does not stage. The Triangle fire world is the mourning march ten days later. The D-Day world is the airfield the evening before.
  • Seeds are fixed, so the same article renders the same world every time, which means a teacher or a judge can check us.
  • The reader is told in the article itself that the world is a reconstruction and not footage.

How we built it

Atlas is a pnpm workspace of seven packages and two apps, written in TypeScript 6, with zod contracts at every boundary so a malformed article fails at the edge instead of halfway down the pipe.

content/articles/*.json     sourced article, archival photo, scene brief
  -> @atlas/schema          parse and validate against the contract
  -> @atlas/scene           compile the layered prompt, lint it for fidelity
  -> @atlas/archive         index the archive, withhold anything unservable
  -> @atlas/world           pick the model, mint a scoped JWT, build the plan
  -> browser
  -> @atlas/runtime         connect, stage, map input to model commands
  -> Reactor                LingBot World 2, WebRTC video at 1664x960
Package Responsibility
@atlas/schema zod contracts for articles, scene briefs, sessions and control state
@atlas/scene compiles a brief into layered prompts, composes them, lints them for fidelity
@atlas/archive loads and validates the local JSON archive, withholds unservable articles
@atlas/world Reactor model registry, command vocabularies, token minting, session planning
@atlas/runtime browser control loop: transport, input, camera pose, chunk clock, touch, React hooks
@atlas/briefs the event brief pipeline that turns research into a scene brief
@atlas/ingest optional side channel: post an article, or draft one from a news URL
apps/web the reader, the product surface
apps/api the archive and world session API, and nothing else

The boundary that decided the architecture sits between build time and request time. Everything above it is deterministic and testable with no network and no GPU, which is why 230 unit tests can assert that all ten shipped events produce a clean, in budget, fully sourced prompt without spending a cent. Everything below it is network and GPU work that can fail, and every failure has a named, readable outcome rather than a black screen.

Three decisions did most of the work.

Prompts are compiled, not concatenated. A world model drifts when its three conditioning signals, the image, the text and the input, disagree with each other. So the compiler assembles layers in a fixed order, base + camera[moving] + movement[moving] + heldEvents + vertical, and recomposes them whenever input state changes, with verbatim camera and movement contracts and positive only phrasing. The rules live in the compiler once. The linter enforces them per article, in CI.

Input is coalesced against the chunk clock. Reactor's set_* commands are persistent state applied at the next chunk boundary, roughly every 250 ms, so sending one per keystroke adds latency and buys nothing. The runtime keeps a desired wire state, diffs it against the last state the model acknowledged, and flushes at most one batch per boundary with a single call in flight. Every state message from the model corrects what the runtime believed was on the wire, so a command the model quietly dropped gets re-sent on the next chunk instead of desyncing the session permanently.

The API key never reaches a browser. REACTOR_API_KEY is read server side only and exchanged for a JWT scoped to one model, one session, and 120 seconds. That JWT is the only credential that ever touches the client, and the cap is enforced in two independent places, so a browser that lied or crashed still cannot hold a GPU past two minutes.

The reader is React 19 on Vite 8 with Geist self hosted through Fontsource. The API is a small Node process with no database. The brief pipeline calls two models through the OpenAI SDK pointed at Groq's OpenAI compatible endpoint, because no single model there does both jobs well: one scans the page image, another fills a strict brief schema, and Tavily searches for corroborating coverage before composition. Model choice is environment config rather than code. Testing is Vitest for the 230 unit tests and Playwright for 8 browser tests across Chromium and WebKit.

Challenges we ran into

Making a generative model behave like a source. This is the entire problem with the idea, and hoping was never going to fix it. Atlas is not a rendering problem wearing a provenance problem's clothes, it is the other way around. The interesting engineering was never "can we generate 1969", it was "can we generate only the 1969 that a source will attest to, and refuse to serve the rest". The answer turned out to be a linter and a hard CI gate rather than better prompt writing, and the gate has to be able to withhold an article entirely, which meant the archive loader needed to report what it withheld and why rather than silently shipping nine events instead of ten.

We built the wrong product first, twice. The original Atlas was a cinematic on rails: a scripted camera path over a 3D newspaper, with Theatre.js driving the timing. It looked good in a screenshot and it taught nothing, because the reader only watched it. If the whole argument is that you can look left, a camera that looks for you defeats the argument. We cut it. The second cut was a gated historical soundscape, which we wanted badly, and removed once we accepted that no source in the archive licenses audio for the specific moments we stage. A sound effect that is not attested is a fabrication with a nicer surface. Deleting two features we had already built was the hardest call of the project and the one that made the rest of it coherent.

A live GPU is expensive in a way a web page never is. At list price LingBot World 2 bills $0.007 per second, which is $25.20 an hour, and a browser tab is trivially easy to forget about. So a world warns at 45 seconds without input and closes at 60, hits a hard ceiling at 120, and closes on pagehide and visibilitychange as well. $0.84 is the ceiling on any single session and there is no path through the code that produces a more expensive one.

Failure needed to be a feature, not an exception. A generated world has many more ways to fail than a web page, and every one of them looks identical to a reader: a black rectangle. So each failure became a state the reader gets told about in plain language. No free GPU at the provider, an exhausted credit balance, a model refusing a command, video that never arrives, all of them produce a distinct readable sentence naming the actual cause. Getting that right meant reading provider error semantics carefully rather than catching everything and shrugging.

Keyboards are not all our keyboard. WASD is a physical shape rather than four letters, so keys are read by position with event.code and land correctly on QWERTY, AZERTY and Dvorak. Where a browser reports no physical key at all, which happens on remote desktops, some virtual keyboards and several IMEs, the binding falls back to event.key rather than going silently dead.

Accomplishments that we're proud of

Ten events, fully sourced, none of them faked. Every photograph and every scan in the archive is public domain or openly licensed and carries its credit in three places: the article JSON, the interface, and the scene brief that references it. Sources include NASA, the US National Archives, Wikimedia Commons, Trove at the National Library of Australia, the Internet Archive, and ZEFYS at the Staatsbibliothek zu Berlin.

Historical accuracy enforced by CI rather than by good intentions. 230 unit tests and 8 browser tests, green, alongside lint, typecheck and a production build. The fidelity linter runs inside that gate, so an unsourced claim cannot reach a reader even if everyone on the team is tired at 4am.

A bounded cost model we can put in front of a school. $0.84 per session worst case, enforced twice, with every guard documented and tested. Three worlds per student is $1.26 of GPU, which sits inside the noise of what a history department already spends per student per lesson on licensed material.

A runtime another frontend can adopt in about fifteen lines. Three React hooks over a transport that knows nothing about our reader. That was a hard requirement from the first day and it shaped most of the package boundaries.

An interface with nothing in it. No feed, no recommendations, no infinite scroll, no engagement loop. A wordmark, a list, and a paper. Getting to that took more deleting than building.

What we learned

The medium was throwing away the dimension that mattered. Reading about a place and standing in a version of it are different cognitive events, and the research on participation says the difference is large and replicable. That reframed Atlas from a visual effects project into an education one.

When three conditioning signals disagree, a world model does not average them, it drifts. Compiling prompts in layers and recomposing on input change fixed more drift than any amount of prompt wordsmithing did.

Persistent state commands punish eagerness. Coalescing input against the chunk clock did more for how real time the worlds feel than any rendering change.

A front page is a primary source and a media product at the same time. The morning after a huge event the omissions are enormous. Wrong times, missing photographs, the part everyone remembers later left out entirely. What a page leaves out turned out to be as teachable as what it prints, and it is the reason the original scans sit in the reader next to the world rather than behind it.

Deleting a finished feature is a design skill. The cinematic and the soundscape were both working, and both were wrong. The project got better every time we took something out.

What's next for Atlas

Finish the walkthrough pass. Every one of the ten worlds needs a human at the keyboard signing off on it, which is the last unchecked box in our spec.

Open the ingest path publicly. @atlas/ingest already accepts an article or drafts one from a news URL, but it stays behind ATLAS_INGEST_ENABLED until the fidelity gate has been tested against articles we did not choose ourselves. An editor still owns the brief and the sourcing, and that should stay true.

Move the archive out of the repository. Nothing in the pipeline assumes ten events. Articles and images move to object storage behind the same loader, with the fidelity gate running at publish time rather than at test time, and token minting becomes a queue, because the binding constraint at scale is GPU availability and a reader told "you are third in line" is happier than one told "no capacity".

Run a classroom pilot measuring what we actually claim. Time spent inside a story and questions asked afterwards, on a real syllabus, against the same lesson delivered as text.

The same morning in several cities at once. Walk the front page of 10 November 1989 as it was reported in East Berlin, then in London, then in New York, and see how differently one night was understood by the people writing it down while it was still happening to them.

Built With

Share this project:

Updates

Submission history