Lantern runs untrusted and semi-trusted learning apps through one governed LMS integration boundary.
It handles LTI login and launch, Deep Linking, reviewed runtime delivery, grade publishing, evidence, and admin tools so app packages never need raw LMS credentials, direct database access, or direct grade-write power.
If you want to evaluate Lantern as a JS/TS teaching tool, start here instead of with the full platform install:
- Install Node.js 26 LTS and run
npm ci. - Run
npm run app:preview -- examples/apps/typescript-ladder-game. - Open the localhost URL printed by the command.
This path does not need LMS setup or a Cloudflare account. It runs the same browser authoring seam Lantern uses for local package preview, so you can judge the app model first.
When you want the full local admin and review flow, use the built-in local bootstrap path:
npm run local:init
npm run local:bootstrap
npm run local:startThen open the localhost URL printed by the Cloudflare Vite dev server.
For arbitrary reviewed packages, use /admin/packages/import. Reference apps
remain available at /admin/packages/reference.
What these commands do:
local:initwrites.env.local, generates a local development signing key, and creates Lantern's local artifact directorieslocal:bootstrapapplies local D1 migrations through cflocal:startruns Lantern through cf with cloudflare.config.ts
Lantern app authoring is intentionally narrow:
- Scaffold one reviewed package from a curated starter.
- Edit HTML, CSS, JavaScript, and JSON content.
- Validate the package.
- Execute preview assertions through Lantern's local browser seam.
- Open a live preview URL only when that helps.
- Import the same package into Lantern for review and inventory.
Start with:
- AUTHORING.md
- AUTHORING_FOR_LLMS.md
- GENERATED_APP_CONTRACT.md
- BROWSER_AUTOGRADER_COOKBOOK.md
- examples/apps/template/README.md
- examples/apps/web-checkup/README.md
- examples/apps/typescript-ladder-game/README.md
Common path:
npm run app:new -- /tmp/my-lantern-app --starter=simple-activity --app-id=my-app --title="My App"
npm run app:dev -- /tmp/my-lantern-appapp:dev starts one local loop with a stable preview URL. It reruns package
validation and preview assertions after file saves, shows blocked package
diagnostics clearly, and reloads the blocked preview page after the next valid
save.
Use --starter=browser-autograder when the reviewed package needs
grading/specs/*.js plus evidence/example-output.json.
- running untrusted or semi-trusted learner-facing apps through one LMS boundary
- packaging browser-first learning activities as signed, versioned app packages
- keeping LMS credentials and service calls in Lantern instead of in app code
- serving reviewed browser assets through a read-only Dynamic Worker envelope
- supporting Deep Linking without ad hoc placement setup
- publishing grades and roster checks from the server
- giving admins one place to inspect deployments, retries, and audit records
For the plain-language trust promise behind this model, see TRUST_MODEL.md.
For the exact boundary App Writer-generated packages must stay inside, see GENERATED_APP_CONTRACT.md.
- a general app hosting platform
- an LMS replacement
- a pile of compatibility shims for every LMS quirk
- a way to hand raw LMS tokens or D1 database access to generated apps
- a way to let generated apps write grades or call arbitrary outbound services
- a backend framework for arbitrary server code inside app packages
Lantern's answer to institution-built and AI-built learning tools is not to give every tool its own LMS integration. App code runs as reviewed, browser-first packages behind Lantern's gateway. Lantern owns the LTI boundary, runtime session, storage bridge, grading flow, evidence trail, and audit record.
On Cloudflare, that model maps to one narrow platform path:
- Cloudflare Workers validate launches, create runtime sessions, broker capabilities, and publish grades.
- Cloudflare D1 stores trusted product state such as reviewed packages, placements, attempts, runtime sessions, and audit records.
- Cloudflare R2 stores immutable reviewed package artifacts and evidence bytes.
- Worker Loader / Dynamic Workers serve approved browser assets from a reviewed package identity without receiving LMS tokens, a D1 binding, or generic outbound capability.
This keeps the blast radius of app code small: reviewed packages can render the learner experience and request only the capabilities Lantern exposes, while credentials, persistence, grade writes, and durable audit evidence stay inside the trusted platform boundary.
At a high level:
- An app package is imported, reviewed, and versioned.
- Lantern stores one exact LMS setup record for each supported deployment.
- The LMS launches Lantern over LTI 1.3.
- Lantern validates the launch, creates a runtime session, and loads the app package.
- Lantern, not the app, owns Deep Linking return, placement creation, grade publication, and audit records.
This keeps the design simple:
- apps get launch context, content, and the features Lantern exposes to them
- apps do not get raw LMS access tokens
- apps do not get arbitrary outbound HTTP
- apps do not get direct grade-write power
- apps do not get direct D1 database access
Lantern implements LTI against real LMS behavior, not just the clean examples from the spec.
Examples of realities Lantern already accounts for include:
- exact launch verification against the saved LMS setup
- one JWKS refetch during key rotation races
- Deep Linking replay protection
- sparse or partial AGS line item responses
- one token refresh retry on LMS service
401s - proxy-aware public origin resolution for Cloudflare Workers and other reverse proxy deployments
- audit events when Lantern has to take a recovery path
The goal is still one correct path. Lantern only adds recovery behavior where real LMS behavior requires it and where the behavior stays clear.
Lantern does not need a browser cookie to track LTI launch state.
Instead, Lantern treats state as a one-time server-side record:
- On login initiation, Lantern generates a random
stateandnonce. - Lantern stores them with the expected LMS identity and launch facts:
issuer,client_id,deployment_id,target_link_uri, expiry, andused_at. - Lantern sends the random
stateandnonceto the LMS authorization flow. - On the signed launch back, Lantern loads the saved record by
state. - Lantern verifies that the launch JWT matches the saved record, especially
nonce,deployment_id, andtarget_link_uri. - Lantern atomically marks the state as used so it cannot replay.
This means the browser does not need to hold the real launch state in a third-party cookie. If the LMS is embedding Lantern in an iframe and the browser blocks third-party cookies, the launch can still be correlated because the real state lives in Lantern's D1 database.
Lantern's canonical full-app runtime is Cloudflare Workers with D1, R2, and Worker Loader bindings.
For public LTI URLs, Lantern resolves its public origin in this order:
Forwarded/X-Forwarded-*request headersAPP_ORIGIN- the raw request origin
This keeps LTI config, login redirects, dynamic registration callbacks, and Deep Linking responses consistent behind Cloudflare domains, routes, and previews.
The repo treats the Workers entrypoint at src/worker.ts as the
canonical runtime path. The local app wrapper in src/test_helpers/app.ts exists
for injected tests and package preview tooling; full persistence runs through
the Worker DB binding. The typed deployment configuration lives in
cloudflare.config.ts, with the browser service defined in
cloudflare.platform.ts.
Current Workers boundaries:
- Lantern still owns the top-level reviewed runtime session document, signed bootstrap injection, JSON content bridge, local-state/event/evidence/score mutations, and audit boundary
- on Workers, immutable reviewed entrypoint HTML, reviewed static assets, and
reviewed browser-grader files launch through a reviewed-identity-keyed Dynamic
Worker envelope created from a Worker Loader binding named
LOADER - that Dynamic Worker envelope is browser-first and read-only: it serves only
reviewed immutable bytes, receives no D1 database, LMS, or generic outbound
capability, and sets
globalOutbound: null - reviewed package snapshots are also written to and reloaded from that same
PACKAGE_ARTIFACTSbucket on Workers - arbitrary reviewed package imports and curated reference app imports follow
the same governed snapshot flow and land under
var/packages/<app-id>/<version>/... - repository-backed Worker routes use D1 through a binding named
DB - curated reference package imports on Workers read source files from
PACKAGE_ARTIFACTSunderreference-packages/<app-id>/source - the Cloudflare configuration enables
nodejs_compatand Smart Placement because Lantern makes multiple D1 queries per request - the gateway configuration declares the
LOADERbinding used to cache Dynamic Workers by reviewed runtime identity instead of by session id
The R2 bucket should contain two key layouts:
- reviewed snapshots under
var/packages/<app-id>/<version>/..., for examplevar/packages/<app-id>/<version>/dist/index.html - curated reference package sources under
reference-packages/<app-id>/source/..., for examplereference-packages/chapter-4-asteroids/source/manifest.json
At import time Lantern persists the immutable snapshot, derives the reviewed
artifact digest from stored files, and signs the reviewed runtime contract that
runtime launch later verifies. On Workers, Lantern uses that reviewed identity
to key the Dynamic Worker envelope, so the same approved package bytes can be
reused across sessions without turning reviewed packages into arbitrary
server-side apps. The Worker app path no longer depends on local disk for demo
package import or reviewed runtime delivery. Provision the curated reference
package source files into PACKAGE_ARTIFACTS during Cloudflare bootstrap or
release with:
npm run reference:sync -- --bucket=replace-with-package-artifact-bucketThis command uses the installed cf r2 objects put CLI. Authenticate with
cf auth login or provide a Cloudflare API token before syncing remotely.
Local syncs use .cloudflare/state, the same directory as the dev server.
npm run build uses cf build and the Cloudflare Vite plugin to build the gateway,
the browser service, and the Python container. Build Output is written under
.cloudflare/output/v0/ and is not checked in. npm run check:worker verifies the
build and required bindings, and rejects Deno-only references in Worker modules.
npm run check:deploy validates both deployable Workers without API requests.
Use npm run deploy when ready to publish: it builds once, deploys the browser
service explicitly, then deploys the gateway and its Python container.
To run the Worker locally:
- set Worker vars such as
APP_ORIGIN,BETTER_AUTH_SECRET, andLTI_TOOL_PRIVATE_JWKin.env.local - keep the
LOADERWorker Loader binding present; Lantern uses it for the governed Dynamic Worker envelope around immutable reviewed runtime delivery - keep the
DBD1 binding present; it is the Worker persistence binding - apply D1 migrations from
src/db/d1_migrationsbefore using repository-backed Worker routes
Google admin sign-in is optional. To enable it, create a Google OAuth web client with these redirect URIs:
https://lantern.appboundary.com/api/auth/callback/googlehttp://localhost:8417/api/auth/callback/google
Then configure both GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET as Worker
secrets, or put both values in local .env.local. Setting only one value fails
startup clearly.
- if you want the demo import path to work on Workers, provision the curated
reference sources into the same bucket first with
npm run reference:sync -- --bucket=<bucket-name>ornpm run reference:sync -- --bucket=<bucket-name> --local --persist-to=<dir>before calling/admin/packages/import-reference - then run
npm run local:start
The reviewed runtime contract remains the same across local Node.js preview and the
Cloudflare-native Worker path. The browser still sees one Lantern-owned session
document plus the same GatewayBootstrap and GatewayApp bridge. What changes
on Workers is only the immutable delivery substrate for reviewed browser-first
package bytes. This is a governed reviewed-runtime envelope, not a generic
code-hosting path for arbitrary uploaded server logic.
- Authoring guide: AUTHORING.md
- LLM authoring guide: AUTHORING_FOR_LLMS.md
- Browser autograder cookbook: BROWSER_AUTOGRADER_COOKBOOK.md
- App package contract: APP_PACKAGE_SPEC.md
- Manifest schema: schemas/app-manifest.schema.json
- SDK contract: sdk/app-sdk.ts
- Sample app packages: examples/apps/chapter-4-asteroids/README.md and examples/apps/quick-study/README.md These are governed reference apps, not a generic app gallery.
- Browser autograder demos: examples/apps/web-checkup/README.md and examples/apps/typescript-ladder-game/README.md
- Browser-autograder starter reference: examples/apps/template/README.md
- Public UI design contract: DESIGN.md
- Canonical Workers entrypoint: src/worker.ts
- Test/tooling app wrapper: src/test_helpers/app.ts
- Node.js 26 LTS and TypeScript 7
- Hono
- Cloudflare Workers
- Cloudflare D1
- Cloudflare R2
- Cloudflare Worker Loader / Dynamic Workers
- hand-written SQL
- server-rendered HTML
- plain CSS tokens
Use Node.js 26.13.0 and npm 12.2.1, then install the locked dependencies with
npm ci. The Node version is recorded in .node-version.
Vite+ runs formatting, linting, TypeScript checks, and tests. App Writer uses the
installed TypeScript 7 compiler for local source compilation; it does not fetch
compiler libraries at runtime. Package and rubric validators are precompiled from
schemas/ so Workers never compile schemas with new Function. Run
npm run schemas:generate after changing a schema; validation checks for stale output. Cloudflare's Vite and Vitest plugins use the same pinned Miniflare 5 runtime.
Node tests run in the node Vite+ project; D1, RPC, and Browser Rendering tests run
inside workerd in the workers project. Cloudflare's test pool owns their runtime
and storage lifecycle.
Cloudflare Puppeteer is pinned to 1.5.0-rc.0 because it removes the vulnerable
legacy browser-download dependency tree. CI runs the dependency audit to catch
new advisories. Miniflare also embeds an older downloader that npm's audit cannot
see. The install step replaces that bundled copy with pinned @puppeteer/browsers
3.2.6, removes its unused legacy dependencies, uses current Chrome, and keeps the
Chrome sandbox enabled in CI. The patch verifies the upstream and patched file
hashes and fails on unexpected changes; review it when upgrading Miniflare.
The current cf beta also imports two removed Miniflare helpers. A second
hash-checked install patch redirects those imports to Cloudflare's maintained
workers-utils entrypoints. Review that patch when upgrading cf.
Run npm run browser:install after installing dependencies to download the pinned
Chrome version. Local preview assertions execute generated JavaScript in Chrome's
sandboxed renderer, with no Node objects or inherited host environment. Lantern
handles preview requests and blocks external requests. No DOM emulator executes
app code in the host process.
CI installs Chrome into a fresh cache and runs both local preview regression tests
and npm run check:browser, which exercises the production App Writer preview handler in the Workers test
pool with a real Browser Rendering binding. Linux hosts must allow Chrome's sandbox user
namespaces; CI enables them explicitly instead of disabling the browser sandbox.
Fast path:
npm run local:init
npm run local:bootstrap
npm run local:startOpen the localhost URL printed by the Cloudflare Vite dev server.
Generated local environment:
.env.local: written bynpm run local:init- the Vite dev server listens on port
8417 APP_ORIGIN: defaults tohttp://localhost:8417APP_RUNTIME_ORIGIN: defaults tohttp://localhost:8417so local preview and reviewed runtime stay on one originBETTER_AUTH_SECRET: generated automatically for local admin sessionsLTI_STATE_SECRET: generated automatically for local developmentLTI_TOOL_PRIVATE_JWK: generated automatically for local development
Manual minimum environment if you do not use local:init:
LTI_STATE_SECRET: one stable secret with at least 32 UTF-8 bytes used for LTI OIDC state signingLTI_TOOL_PRIVATE_JWK: one RSA private JWK used to publish Lantern's public JWKS and sign LTI assertionsAPP_ORIGIN: recommended for stable local config and required anywhere Lantern must publish fixed public URLs without proxy headersAPP_RUNTIME_ORIGIN: required anywhere Lantern must publish runtime URLs for attempts and app sessionsBETTER_AUTH_SECRET: one stable secret with at least 32 UTF-8 bytes used for admin auth cookies and sessions
Common commands:
npm run local:initnpm run local:migratenpm run local:bootstrapnpm run local:startnpm run local:devnpm run app:new -- --list-startersnpm run app:new -- <output-root> --starter=<id> --app-id=<app-id> --title="<title>"npm run app:dev -- <package-root>npm run app:validate -- <package-root>npm run app:test-preview -- <package-root>npm run app:preview -- <package-root>npm run reference:sync -- --bucket=<bucket-name>npm run fmtnpm run fmt:checknpm run lintnpm run checknpm run check:workernpm run testnpm run validate
Default local port: 8417
src/lti/: LTI login, launch validation, Deep Linking, service calls, and config publishingsrc/package_review/: package import, review persistence, placements, runtime session persistence, and audit eventssrc/runtime/: runtime and grade publishing codesrc/admin/: server-rendered admin pagessdk/: app-facing contractexamples/: sample app packagesschemas/: public package schema
This repo is the public code and docs. It intentionally focuses on:
- runtime interfaces
- schemas
- SDKs
- sample apps
- public implementation code
Private strategy, security notes, infra specifics, pricing, and other internal operational material stay out of this repo.