A malleable, offline-first medium for knowledge work. · Try the live version →
A Roam/Workflowy-like outliner where everything is a block and everything is an extension — notes, daily notes, settings, even the renderers that draw the screen. The app is a thin host that assembles itself from those blocks at runtime, so you can reshape it from inside. It runs in the browser as an installable PWA, is local-first (instant, offline-capable), and syncs across devices via PowerSync + Supabase.
The idea: log in and you get an opinionated knowledge-management tool (think Notion / Roam / Obsidian) you can be productive in immediately — but those defaults are an example, not a prescription. They're assembled from a small set of primitives, so you (or a coding agent on your behalf) can remix them into the tool that fits how your mind works; "Notion" and "Roam" are just different views over the same blocks. The hard, generic parts — sync, collaboration, offline, storage, querying — are solved once and exposed as building blocks, so shaping the app to you means composing components instead of rebuilding infrastructure. (In the spirit of malleable software.)
What's different: one universal blocks table for everything (pages, todos,
flashcards, places, settings, renderer source — set apart only by multi-valued
types + typed properties); the whole app is facet-based extensions you can author
live in-browser (no build step); config, prefs, and UI state live in the
document as blocks (so they sync and version with your notes); and a runtime
bridge lets agents/scripts drive the live app from your terminal.
Features (learn them in the built-in [[Tutorial]]): outlining with fold /
zoom / drag and vim or arrow-key motion · [[wiki links]], block refs ((id)),
inline embeds !((id)), and indexed backlinks · side panels + multi-select ·
QuickFind and workspace-wide find-and-replace · daily notes with quick capture ·
typed properties & user-defined types · first-class places & maps · todos and
SM-2.5 spaced repetition · Roam import, themes, and a touch-first mobile UI.
This repo expects:
- Supabase hosted Postgres for the source database
- Supabase Auth for app sessions (email OTP, with anonymous as a fallback)
- PowerSync Cloud for syncing into local SQLite
Sign-in flow:
- Primary: enter your email, Supabase emails a 6-digit code, type it back in the app
- Fallback: "Continue without an account" button performs an anonymous sign-in (per-device session, can't invite or be invited until you sign in with email)
- Create a local env file from
.env.example. - Log into Supabase:
npx supabase login- Create a hosted project, then link this repo to it:
npx supabase orgs list
npx supabase projects create knowledge-medium --org-id <org-id> --region us-west-1 --db-password <db-password>
npx supabase link --project-ref <project-ref> --password <db-password>- Push the committed schema and auth config:
npx supabase db push
npx supabase config push-
Fill client env vars:
VITE_SUPABASE_URL: Supabase project URLVITE_SUPABASE_ANON_KEY: Supabase publishable / anon key
-
Log into PowerSync and create a cloud instance:
npx powersync@latest login
npx powersync@latest link cloud --create --project-id <powersync-project-id>
npx powersync@latest validate
npx powersync@latest deploy-
Fill the remaining env vars:
PS_DATABASE_URI: Supabase "Direct connection" URI copied from the Supabase Connect dialogVITE_POWERSYNC_URL: PowerSync instance URL from the PowerSync dashboard / connect dialog
-
Start the app:
pnpm devRun the SQL migration test suite with:
pnpm check:dbThis starts a local Supabase Postgres test container and the Storage API,
applies supabase/migrations/*.sql as supabase_admin, and runs every pgTAP
test in supabase/tests. Storage starts first, as on the hosted project, so the
migrations' Storage blocks (the attachments bucket and its policies) run. It
then starts GoTrue and PostgREST against that database and runs the vitest files
named *.supabase.test.ts (through vitest.supabase.config.ts), which drive the
app's own PostgREST and Storage code against it. Finally it removes the
containers and their network so the next run starts clean. It is not part of
pnpm check; the pre-commit gate stays JS-only and does not require a container
runtime. pnpm check:db --hold brings the stack up, prints its CHECK_DB_*
exports and keeps it running until Ctrl-C, so one stack test can run alone.
For Apple Container, install container and socktainer. pnpm check:db
uses the shared runtime helper and starts the Apple container system plus a
managed Socktainer process when DOCKER_HOST is unset:
pnpm check:dbSocktainer needs to be at least 1.0.0. Older releases set the hostname to
<container name>-<uuid> without capping it at Linux's 64 characters. The test
container's name is long enough to go over that limit, so its start fails with
vmexec error ... Invalid argument. Each Socktainer release is built against
one Apple Container version (Socktainer 1.5.x against container 1.5.0), so
upgrade the two together:
container system stop && brew upgrade container socktainer && container system startSocktainer now ships in homebrew-core. If you installed it from the old
socktainer/tap, run brew untap socktainer/tap first, as upstream recommends.
The managed Socktainer logs to
$TMPDIR/knowledge-medium-socktainer-<pid>.log, and its startup version check
reports a mismatch there.
The runtime helper is also available directly:
pnpm db:runtime:appleThat starts Apple Container/Socktainer and prints the Docker host it prepared.
If DOCKER_HOST is already set, pnpm check:db respects it. To force the
default Docker runtime, set CHECK_DB_CONTAINER_RUNTIME=docker.
- The fastest dev path is to use the Supabase direct connection URI as-is in
PS_DATABASE_URI. - For a hardened setup, create a dedicated
powersync_rolewith replication privileges and replace the username/password inPS_DATABASE_URIbefore deploying PowerSync. - Because the starter graph is seeded in the remote database, the app will now wait briefly for the first remote sync before falling back to local example data.
- Add
?safeModeto URL to disable dynamic renderer loading - Only default renderers will be used
- Useful for debugging or when custom renderers are problematic
The app exposes a first-class runtime bridge for coding agents. The browser app connects to a local relay, and agents submit commands to that relay from the terminal. Commands execute inside the live app runtime, with access to the current Repo, active workspace, PowerSync SQLite database, resolved runtime facets, block helpers, and arbitrary JavaScript execution.
Start the app:
pnpm devThen use the CLI from another terminal. If the local relay is not running, the CLI starts it in the background before submitting the command:
pnpm agent connect
pnpm agent ping
pnpm agent sql all "SELECT id, content FROM blocks LIMIT 5"
pnpm agent create-block '{"parentId":"<block-id>","content":"Created by agent"}'
pnpm agent delete-block "<block-id>"
pnpm agent restore-block "<block-id>"
pnpm agent eval 'return { workspaceId: repo.activeWorkspaceId, user: repo.user }'pnpm agent connect prints an app URL, opens the token dialog when that URL is loaded, then waits for the copied token to be pasted back into the terminal. After that one-time pairing, normal pnpm agent ... commands use the stored token automatically.
Available runtime-code bindings include repo, db, runtime, safeMode, sql, block, getBlock, getSubtree, createBlock, updateBlock, moveBlock, deleteBlock, restoreBlock, installExtension, setExtensionEnabled, uninstallExtension, actions, renderers, refreshAppRuntime, React, ReactDOM, window, and document.
For an agent-facing orientation to the data model, run pnpm agent data-model.
By default the bridge uses http://127.0.0.1:8787. The bridge secret is stored in the local config file (~/.config/knowledge-medium/agent-bridge.json by default), so pairing is normally one-time per browser profile and app origin. Run pnpm agent pair-url or foreground the relay with pnpm agent:server to get a bridge-only pairing URL. Override the pairing target with AGENT_RUNTIME_APP_URL, the browser endpoint with VITE_AGENT_RUNTIME_URL, and the CLI endpoint with AGENT_RUNTIME_URL.
The bridge only accepts browser origins from loopback hosts and configured app origins. Add comma-separated entries with AGENT_RUNTIME_ALLOWED_ORIGINS; browser origins do not include URL paths, so GitHub Pages is allowed as https://stvad.github.io. Detailed /health output requires the bridge secret header; the CLI reads the persisted local secret automatically for pnpm agent status.
pnpm dev # start the app (Vite)
pnpm check # the verification gate: typecheck, lint, and tests
pnpm build # production buildSee AGENTS.md for verification, testing, and contribution
conventions, and docs/ for design notes (treat these as dated
intent — the code and tests are authoritative).
This repository is licensed under the Elastic License 2.0. In short: you can use, copy, modify, and self-host the software freely; you may not offer it to third parties as a hosted or managed service that replicates a substantial part of its functionality.
The packages/agent-cli sub-package is licensed separately under the
MIT License so it can be used as an open
integration surface by third-party tooling.
See CONTRIBUTING.md for contribution terms, including
the license grant that preserves the project's ability to relicense future
versions.