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.
A self-hostable workspace where an infinite canvas, your pages and your issue tracker are three views of one record. Promote a sticky note on this page and watch it turn up in the table and the sprint
ProductivityTypeScript · PLpgSQLGitHub @namlifurkanOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
A self-hostable workspace: pages, databases, issues and an infinite canvas over one set of records. Canvas 2D renderer, Yjs CRDT, local-first. AGPL-3.0.
Translation pending
Loading the full README. The saved excerpt is shown below.
A workspace you can run yourself: pages, databases, issues and an infinite canvas over one set of
records. Canvas 2D renderer + Yjs CRDT, local-first, self-hostable.
An alternative to paying three subscriptions for one team's work. All code, design and branding
are our own; no visual identity or asset is copied from any commercial product.
Whiteboard, task board and document are three views of one workspace, not three products. The
canvas is one of those views, not the product. It is meant to be used daily by a real team,
self-hosted, without a per-seat bill.Read the full README ↗A workspace you can run yourself: pages, databases, issues and an infinite canvas over one set of records. Canvas 2D renderer + Yjs CRDT, local-first, self-hostable.
An alternative to paying three subscriptions for one team's work. All code, design and branding are our own; no visual identity or asset is copied from any commercial product.
Self-hosting · HTTP API · MCP server · Agents · Keyboard · Reproducing · Contributing
| Canvas | Infinite board, 32 shapes, connectors, frames, comments, nine templates, frame export at the size you drew it, Miro and PDF import |
| Pages | Block editor, sub-pages, backlinks, @ mentions, version history, trash, export to md/html/pdf |
| Databases | Six views — table, list, board, gallery, calendar, timeline — and 20 column types |
| Issues | TUV-12 numbers, estimates, labels, sub-issues, blocking relations, seven statuses |
| Projects and cycles | Two-week cycles with a burn line, projects with a roadmap read from their issues |
| Forms | Answers land straight in a database |
| Workspace | ⌘K search across bodies, inbox, activity, what changed since you last looked, page permissions, publish to /p/<slug> with an end date, calendar over every date the workspace already knows, backup |
| Outside | HTTP API, webhooks, MCP server, Notion, Excel and CSV import |
Whiteboard, task board and document are three views of one workspace, not three products. The canvas is one of those views, not the product. It is meant to be used daily by a real team, self-hosted, without a per-seat bill.
One thing here does not exist elsewhere: Hand off to AI. A board is reduced to a semantic graph — frames become sections, connectors become directed edges, comments attach to the nearest item, code blocks stay fenced code — and exported as a prompt, Markdown or JSON that a coding agent can actually act on. Spatial layout is resolved into reading order, so the output is not a screenshot but a brief.
The loop closes from either end. Paste an agent's Markdown into Build a board from a brief, or let the agent post the brief itself over the MCP server and open the board it names: headings become frames, bullets become stickies, fenced code becomes code blocks and a mermaid flow becomes connectors.
Every board is a room in the URL: /b/team-board. The grid icon in the top bar opens the
board list — create, search, switch, delete. The registry lives in localStorage and older
rooms are recovered from indexedDB.databases(), so a board you visited once is never lost to
a forgotten link. The list is per-browser: share the URL for someone else to open a board.
Your camera is remembered per board, so a refresh puts you back where you were.
Tuval is local-first and needs no backend. Add a backend and boards leave the browser: accounts, a board list shared across devices, images in object storage, and every update kept server side.
The backend is a Postgres with PostgREST, GoTrue and storage-api in front of it. Rent them from
supabase.com or run the same open-source containers yourself — deploy/compose.yml brings them
up and docs/self-hosting.md walks through it. Nothing in the app knows
the difference: it is one URL and one key.
Create a project at supabase.com, or cd deploy && docker compose up -d.
Apply supabase/migrations. Either link the project once and let
the CLI track what is applied:
npx supabase link --project-ref <your-project-ref>
npx supabase db push
or paste the files into the SQL editor in filename order. Every migration is idempotent, so re-running one is harmless.
Put the project URL and anon key in .env.local:
VITE_SUPABASE_URL=https://xxxx.supabase.co
VITE_SUPABASE_ANON_KEY=eyJhbGciOi...
Tell the install it is yours:
update public.tuval_settings set self_hosted = true where id = 1;
Skip this and a fresh workspace caps at 3 seats and 1 GB with the HTTP API switched off — the limits that exist because somebody pays for the hosted service. There is no somebody else on your install. The whole story.
Sign-in is an emailed link, GitHub, Google or Apple. A link proves the address once and then a password can be set from the account menu, so the second visit does not need the inbox. Without the keys every control disappears and nothing changes: the board list stays local and images stay inline.
Sharing is by email. The owner invites an address from the Share menu; the invite waits in
board_invites until that address signs in, at which point it becomes a membership. So you can
invite someone who has never opened Tuval. Roles are editor and viewer, enforced by row
level security rather than by the interface.
A board can also be opened to a whole email domain, so a team never has to be invited one by one. The domain is never typed in: it is read from the owner's own verified address, which is also why no DNS ownership check is needed.
Two knobs belong to whoever runs the instance, not to Tuval, and they live in
public.tuval_settings:
-- allow opening a board to any domain, not only your own
update tuval_settings set restrict_to_own_domain = false;
-- refuse domains outright, for example shared mailbox providers
update tuval_settings set blocked_domains = '{gmail.com,outlook.com,yahoo.com}';
Both are enforced by a trigger, so a client that talks to the API directly obeys them too. The defaults are conservative: own domain only, nothing blocked.
This is the beginning of the README. Read the rest at the original. Read the full README ↗