Wenlan
Wenlan is a source-backed AI knowledge base and LLM wiki for AI work: organize documents and decisions into pages you can find, inspect, and review.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
Website and API for Roam Publish (publish Roam Research pages to the web)
翻訳待ち
Loading the full README. The saved excerpt is shown below.
Server and website for Roam Publish, a Roam Research extension that publishes
pages and blocks to the web.
For how the server and extension work together (the contract, shared invariants, trust boundary, and how to ship
changes across both), see roam-publish-docs.
Without RESEND_API_KEY, verification and reset emails are printed to the server console.
Edit src/db/app-schema.ts (or re-run bunx auth@latest generate --config src/lib/auth.ts --output src/db/auth-schema.ts
after changing better-auth plugins), then bun run db:generate and commit the migration in drizzle/.Read the full README ↗Server and website for Roam Publish, a Roam Research extension that publishes pages and blocks to the web.
For how the server and extension work together (the contract, shared invariants, trust boundary, and how to ship changes across both), see roam-publish-docs.
Stack: Bun · Next.js (App Router) · shadcn/ui (Blueprint-styled) · better-auth (+ api-key plugin) · Drizzle · Postgres · Resend.
bun install
cp .env.example .env.local # fill in BETTER_AUTH_SECRET and APPEND_TOKEN_KEY (openssl rand -base64 32 each)
bun run db:migrate
bun dev
Without RESEND_API_KEY, verification and reset emails are printed to the server console, except in production,
where they aren't sent and an error is logged instead (their links must never reach the host's logs).
Emails are Resend templates, defined in src/lib/email-templates.ts with the layout in src/lib/email-layout.ts.
There's nothing to set up in Resend: the first time each one is sent, the server publishes it as a template whose
alias ends in a hash of its content (rp-verify-email-1a2b…), so a changed email is a new template and staging and
production never overwrite each other's. If Resend's templates fail, the same email is sent as plain HTML instead.
Old rp-* templates can be deleted from the Resend dashboard once nothing sends them.
Admins read and answer email sent to the domain at /admin/inbox. It lists what Resend's receiving API holds (so
receiving must be turned on for the domain in Resend) and needs no other setup; replies go out from EMAIL_FROM with
MODERATION_REPLY_TO as their Reply-To, and are logged in the inbox_reply table.
bun run test
Unit tests (tests/unit) need nothing. Integration tests (tests/integration) run against a real Postgres: they
create and migrate TEST_DATABASE_URL (default postgres://postgres@localhost:5433/roam_publish_test) and empty it
before each test, so never point it at a database you care about. Next's request APIs (headers, cookies, after,
cache revalidation) are stubbed in tests/helpers/preload.ts; Roam's Append API and email are never called.
A test.failing is a known bug: it passes while the bug is there, and fails once it's fixed, so swap it for test.
CI runs typecheck, lint and the tests on every PR.
develop first. Railway's staging environment (the shadow database) deploys it. Its pre-deploy step (bun run staging:check) runs the full test suite against a throwaway roam_publish_test database on the staging Postgres, then migrates staging. Wait for that deploy to succeed.develop into main.develop is always the staging branch for main.Edit src/db/app-schema.ts (or re-run bunx auth@latest generate --config src/lib/auth.ts --output src/db/auth-schema.ts
after changing better-auth plugins), then bun run db:generate and commit the migration in drizzle/.
Tags and search text are derived from each page's tree on publish (src/lib/tags.ts). After deploying migration
0018_search_and_tags, or after changing how tags are extracted, run railway run bun run search:backfill once so
existing pages get them.
/discover/feed.xml: always on, the newest pages on Discover./{graph}/feed.xml: off by default; owners turn it on in graph settings. Needs the front page on and open to everyone./c/{slug}/feed.xml: off by default; owners turn it on in collection settings. Needs the collection page open to everyone.Feed readers send no cookies, so feeds only ever list pages open to everyone (see src/lib/feeds.ts).
Graph owners' append-only tokens are stored AES-256-GCM encrypted with APPEND_TOKEN_KEY (src/lib/append-token.ts)
and only used to append the change log under shortlink blocks. Without the key, nothing is stored and the change log
is off.
Rotating the key (e.g. it leaked, but the database didn't):
APPEND_TOKEN_KEY_PREVIOUS to the old key and APPEND_TOKEN_KEY to a new one (openssl rand -base64 32),
then deploy. Tokens encrypted with either key keep working.railway run bun run tokens:rotate to re-encrypt every token with the new key.APPEND_TOKEN_KEY_PREVIOUS and deploy.If the key and the database may both have leaked, treat the stored tokens as exposed: set a new key, deploy, and
run railway run bun run tokens:revoke-all. Every owner gets a dashboard banner asking for a new token; tell them to
revoke the old one in Roam (Settings → Graph → API tokens). An append-only token can only add blocks to its own graph.
Changing the key without tokens:rotate doesn't break anything: tokens the server can no longer read are marked
invalid the next time they're needed, and owners are asked for a new one.
A page can be encrypted when every place it's shown is Password (src/lib/encryption.ts, rules in
src/lib/encryption-rules.ts). Its tree is encrypted with a content key, and that key is sealed to each password's
X25519 public key; each private key is stored wrapped under scrypt(password). Readers' browsers unlock with a proof
derived from the password and decrypt the page themselves (src/lib/reader-crypto.ts, src/server/actions/unlock.ts),
so rendering never decrypts on the server.
encryption_version 1): the server encrypts a plain tree on arrival (sealNewContent). Extensions before
0.2.0, Roam without X25519, and the dashboard's encrypt switches make these.GET /api/ext/publications/:rootUid/seal for the public keys, encrypts in Roam and
sends only the cipher and sealed keys (src/lib/e2e-publish.ts). The server never sees the text.This is the beginning of the README. Read the rest at the original. Read the full README ↗