Nulldown turns Markdown into deterministic structure.
Markdown is the shared language of people and agents. Nulldown makes it addressable, replayable, queryable, attributable, composable, and renderable, so one source can power documents, shared state, agent memory, targeted retrieval, workflows, and interfaces.
flowchart LR
Markdown["Markdown"] --> Nulldown["Nulldown"]
Nulldown --> State["Deterministic structure"]
State --> Memory["Agent memory"]
State --> Retrieval["Targeted retrieval"]
State --> UI["Document-native UI"]
State --> Trust["Explicit trust modes"]
Most systems repeatedly translate the same work between documents, prompts, databases, memory, and component state. Nulldown keeps those uses connected to readable Markdown and inspectable state.
Install the Bun-native CLI. Install Bun first if it is not already on your PATH.
bun install -g @thenullnode/nulldown
nd --version
printf '%s\n' '# Hello from Nulldown' '' 'This is a disposable sample.' | nd create - --json
nd get <id-from-create> --rawnd create --json returns the canonical id and a url. Use the id in later CLI commands and open the url to view the drop. This sends plaintext to https://nulldown.app; anyone with the returned URL can access it, so use only non-sensitive sample content.
Choose the surface that fits your work:
| Need | Start with |
|---|---|
| Read and author documents | nulldown.app |
| Automate documents and branches | CLI and API guide |
| Connect an agent | MCP server |
| Run a local API | Self-hosting |
| Understand the model | Nulldown documentation |
For public discovery, see the search API reference. Its backend controller and repository live in functions/api/_lib/search/.
Portable contracts and codecs live under shared/. Public package imports are defined by the export map in package.json and can differ from source filenames.
| Area | Current capability |
|---|---|
| Deterministic state | Ordered branch diff events, parent-linked snapshots, checkpoints, replay, and promotion. |
| Retrieval and memory | Structural document/runtime queries, source references, priority overlays, and optional NullMem facts, procedures, capabilities, and freshness signals. |
| Interfaces | Native Nulldown composition, nullplug runtime contracts, runtime facts, and policy-controlled proposed mutations. |
| Trust | Public plaintext, client-sealed, provider-assisted, and self-hosted workflows with different explicit trust properties. |
| Account continuity | OpenAuth can bind a current V1 account and store a browser-encrypted key package for recovery of known private links on another signed-in browser. |
| Deployment | Cloudflare Pages/R2/D1 plus a self-hostable Bun API backend using filesystem blobs and SQLite metadata. |
Browser drop storage and Nullplug invocation use separate capabilities. DropProviderPortRegistry routes local and remote drop operations, while BrowserNullplugClient owns editor-session invocation and branch fact submission. Backend composition is exposed independently through NulldownServerRuntime from @thenullnode/nulldown/server/runtime.
Remote manifests declare the versioned invocation media type application/vnd.nulldown.nullplug.invoke+json;version=1. Provider invocation rechecks the endpoint allowlist, narrows capabilities to the manifest permissions, enforces a timeout and response-size limit, and rejects non-conforming responses. It never imports code from manifest URLs.
Creating and reading plaintext drops does not require an account session. Resolving branches, promoting changes, and protected diff writes do. Set ND_TOKEN to an account session token before using those operations. ND_ACCOUNT_ID is only for local development against a server that explicitly enables its insecure account header.
Install the CLI:
export ND_TOKEN='<account-session-token>'
nd branch resolve <rootId> --json
nd branch content <rootId> <branchId> --json
nd branch query <rootId> <branchId> --query "important section" --top 3 --jsonAuthorize the CLI through the signed-in browser by opening the printed verification URL and entering the printed authorization code. The refreshable credential is kept in the default private config directory:
nd auth login
nd auth status
nd auth logoutAn authoring-capable login seals authenticated create and update content in
the CLI before it is sent to the API. Use --visibility private|unlisted|public
to select sharing behavior; it defaults to unlisted. Private drops are
vault-only, while unlisted and public drops use provider escrow for recovery.
The CLI reads the public VITE_PROVIDER_ENCRYPTION_PUBLIC_JWK setting for that
escrow path and never needs a provider private key. Older credentials must run
nd auth login again before account-owned authoring. Use --legacy-plaintext
only for intentional compatibility: it stores plaintext and does not enter
Remote Library.
MCP integrations can seal account-owned drops through the stable root export:
import {
sealDropForAuthoring,
type AccountEncryptionMaterial,
type DelegateSigningMaterial,
type ProviderEncryptionMaterial,
type SealDropForAuthoringInput,
} from "@thenullnode/nulldown/drop/authoring";Use --no-browser to print the verification URL and authorization code
without opening it. Set
ND_AUTH_FILE or pass --auth-file to select another credential file. Direct
--token and ND_TOKEN values still take precedence over the stored CLI
credential.
When retrying nd diff apply after an ambiguous network failure, reuse both the
original event identity and creation time. A successful response includes the
durable event receipt; do not retry with a new identity until the original outcome
is known. For a generated diff replace, save the generated envelope and retry it
with diff event or diff batch; replacement re-computes its operations from live
branch content and is not an exact replay surface.
nd diff apply <rootId> --branch <branchId> --event-id retry-1 --created-at 1770000000000 --insert '0:retry' --jsonRun against a local or preview API:
nd --base=http://127.0.0.1:8788 get <id> --jsonThe 0.0.9 core and MCP prerelease is available under the next tag. MCP
requires core >=0.0.9 <0.1.0 for the strategy-read and NullMem contracts
below. Release verification installs both local tarballs before publication.
Use the separate MCP package to let agents retrieve structure, manage branch diffs, and work with NullMem without shelling out:
bun install -g @thenullnode/nulldown-mcp@nextnulldown-mcp is a stdio server configured by an MCP client, not an interactive terminal program. Configure ND_BASE_URL, ND_TOKEN, and ND_CLIENT_ID in the MCP client environment as needed. ND_ACCOUNT_ID is a development-only alternative accepted only when the target API explicitly enables its insecure account header; an invalid bearer credential never falls back to it. Read/query tools support bounded compact responses; expand exact branch content only when a decision needs it. See the MCP package README.
The canonical conceptual documentation lives in Nulldown:
- Documentation index
- Why Nulldown: deterministic structure for Markdown
- State model
- Agents, retrieval, and memory
- Documents as interfaces
- Privacy and trust boundaries
- Build with Nulldown
- Status and direction
The local docs/ directory contains source-coupled API and operational references.
strategy_get (SDK: client.readStrategy) uses an explicit branchId without
reading the root. Otherwise it reads the root once and follows plaintext payload
metadata strategyRef: { kind: "branch", rootDropId: "<canonical same root>", branchId: "<explicit branch>" }.
Both ids must be nonempty trimmed strings. Short input ids are checked against the
canonical id returned by the root read. Explicit branch responses must match the requested canonical root or its
six-character short alias. Without a reference it stays a labeled root
read; query, snapshotId and top require an explicit or metadata-selected branch.
Reads never resolve/create branches, follow references recursively, or fall back
after invalid/cross-root references or branch errors. Routing is not authorization.
Envelope metadata is not followed and no secrets are decrypted. Existing drops are
not migrated: publishing a branch and revision-safely updating its original root's
metadata are separate explicit actions; preserve the root content and other metadata.
Output defaults to 800 approximate tokens (maxTokens, 100-8000), bounded to
maxTokens * 4 serialized characters even with format: "full" or preview: false.
Root/branch/snapshot identity, partial/truncated flags and requery guidance survive
payload truncation. getDrop / drop_get remain the raw root read interfaces.
New committed branch snapshots carry an authoritative sourceContentHash in
snapshot JSON. Document queries can reuse a matching current-version projection
without replaying content. Missing or stale projections reconstruct and validate
the source before repair; inconsistent authoritative hashes fail explicitly.
Legacy snapshots and mutable snapshot zero still reconstruct on every query,
without writing hashes back. Runtime fact freshness and per-query priority reads
remain independent of document projection reuse. No database migration is required.
Resolved queries and updates check root plaintext-read permissions before using cached or regenerated heaps. Private and account-vault-only envelopes require the root owner's authenticated session. Runtime projections additionally require the branch owner or writer; rebuild requests reject client-supplied runtime fact arrays and use facts persisted through the state/submit endpoints.
An authenticated remote branch can render a built-in approval nullplug in the editor:
```approval(id="release-42")
Approve the production release?
```The id is required and should remain stable. Submitting the form stores an immutable, actor-attributed ui.response fact and rebuilds the branch runtime-reference heap. Agents can retrieve the decision with MCP branch_query using resolver nulldown.resolved.runtime-refs, kind ui.response, and primitive id release-42. Approval responses never apply document diffs or mint runtime authority by themselves.
Run the API locally with filesystem blob storage and SQLite metadata:
nd serve --host 127.0.0.1 --port 8788 --data-dir .nulldown-dataThe local SQLite runner records successful migration files transactionally, so
restarts do not repeat account-schema alterations. Its startup migrationsApplied
list contains only files applied during that invocation.
For databases created before the migration ledger, existing DDL is adopted only
after matching its stored schema definition. Missing statements still execute;
incompatible definitions fail and roll back that migration rather than being marked applied.
Run the same API in Docker with /data as the persistent volume:
docker build -t nulldown .
docker run --rm -p 8788:8788 -v nulldown-data:/data nulldownThe local server supports core drop, branch, diff, resolved-query, priority, and NullMem routes. It is a self-hostable API backend, not a full packaged replacement for every hosted route or the web application.
Install dependencies:
bun installThe root-only @thenullnode/nulldown: file:. override links MCP to this checkout.
It does not replace the MCP package's consumer dependency range; installed-pair
verification supplies both exact tarballs.
Run the Vite development server:
bun run devRun Cloudflare Pages development with Functions:
bun run pages:devRun focused verification:
bun run test
bun run build
bun run cli:build
bun run package:check-cli
bun run package:check-mcpRun the local checks before submitting changes:
bun run format:check
bun run lint
bun run typecheck
bun run test --runInBandbun run typecheck checks the web app, Functions, tooling, MCP, shared code,
configuration, OpenAuth, and tests as separate TypeScript surfaces. Run the
individual typecheck:<surface> scripts when you need diagnostics for one area.
Repository rules live in AGENTS.md. Nulldown-hosted plans, documentation, and agent memory are updated with branch diffs and verified through resolved queries. The local docs/ directory retains source-coupled references and migration material; the hosted documentation graph is the public conceptual source of truth.
MIT