salt.md runs as a single Go executable that bundles a web interface, a SQLite database, and an MCP endpoint for AI assistants. Users get a block-based editor for writing pages, which can also become structured databases with multiple visual layouts like kanban boards and calendars. Several people can edit simultaneously with live cursors, and changes merge automatically even after offline work. AI tools such as Claude or Cursor connect through the Model Context Protocol and operate on the same data with the same access rules. The package has no external service requirements — no separate database, cache, or search engine — so deployment is copying one file or running one container. Backups are a single directory copy. Authentication supports passwords with TOTP, plus optional Google or Microsoft OAuth. The interface works as a progressive web app and there are native desktop wrappers for macOS and Linux. Everything is licensed AGPL-3.0 with no commercial tier holding back features.
salt.md scores 50/100 on selfhost.directory (how scoring works) and ranks #20 of 42 in Knowledge Management. It has 173 GitHub stars, +2 in the last 30 days.
It shipped 11 stable releases in the past year. The latest, v1.1.5, came out on 29 October 2026. New versions land roughly every 4 days.
Setup is easy: most people get it running in one sitting. An official Docker image is available. It is AGPL-3.0-licensed and written mostly in Typescript.
Its closest alternative on selfhost.directory is Memos (86/100, 64 175 stars). It scores higher overall, so compare the two before you pick. See salt.md vs Memos side by side.
Features
Activity Log
Backup and Restore
Block Editor
Databases with typed properties
Desktop AppBeta release
Full-Text Search
MCP server for AI agents
Multi-language Support
Multiple database views
OAuth sign-in (Google, Microsoft)
Public Sharing
Realtime Collaboration
REST API
Two-factor authentication (TOTP)
salt.md README
Docs, databases and realtime editing for your team. The same workspace is open
to Claude, ChatGPT and Cursor, on exactly the access you grant them.
One binary, on your own server.
curl -fsSL https://raw.githubusercontent.com/saltmd/salt.md/main/install.sh | sh
One command downloads the binary for your platform, installs it, starts it and
prints the address to open. On a Linux server it sets itself up as a systemd
service, and running it again upgrades it. No curl on the machine?
wget -qO- … | sh does the same.
Or with Docker:
docker run -d -p 8420:8420 -v salt-data:/data ghcr.io/saltmd/salt.md:latest
There is nothing else to install: no database server, no cache, no object
store. Reverse proxies, TLS and backups are in the
self-hosting guide.
People and agents in the same place
An agent is asked to organise the launch notes. A page appears, a database is
created, the rows fill in. A person opens the same board a second later and
carries on. Same pages, same permissions, same history.
Connect Claude, ChatGPT, Codex, Cursor, Gemini CLI or any other MCP client to
/mcp and it works on the workspace you use:
Pages: create, edit, move, duplicate, trash and restore
Databases: create them, change the schema, add and query rows, set up views
Search with the same permission checks a person gets
Bulk imports from any JSON source, without the content passing through the agent
Comments, and an append-only note trail on every page
Presence: the page shows which agent is working on it, before you start typing
And a list it cannot touch. No agent can create or delete accounts, issue
tokens, take or restore a backup, change instance settings or decide who is in
a workspace, and the server tells every agent that connects. A credential
carries its person's access and never more, every workspace decides what agents
may do there, and the activity log keeps their changes apart from yours.
Search finds pages for two or more words again (#107, thanks @ymbroxe). As soon as one word of a search had a second spelling (a shortened stem, or ss for a sharp s), the query was one the search index refuses, and the refusal came back as "no results". Most searches of two words found nothing, German ones above all.
Line breaks in a text property survive a click (#108, thanks @ymbroxe). Clicking into a value and leaving it saved the value without its line breaks, even without typing. The value is edited in a field that keeps them now, Enter still saves, Shift+Enter adds a line, and leaving it unchanged writes nothing.
salt backup says when files are missing (#109, thanks @ymbroxe). A file under files/ that could not be read ended the backup early, and it still reported success with exit status 0. Now everything readable is archived, the unreadable files are named, and the command exits 1; a failed write removes the half-written archive.
A board keeps its columns when its grouping property is hidden (#112, thanks @ymbroxe).
On a phone, the tab of the open view is scrolled into sight (#113, thanks @ymbroxe).
Less English in a German interface (#114, thanks @ymbroxe): a duplicate is named in the language of whoever makes it, and the editor's own texts (the hint on an empty line, its menus) follow the interface's language.
A container with exactly 2 GB is no longer told it has no memory limit (#117, thanks @ymbroxe).
A long workspace menu stays inside the window (#99, thanks @gezerui888, fixes #41).
An embedded collection survives an agent rewriting the page (#100, thanks @luisfernandosoni).
A workspace rules proposal shows up without a reload (#101, thanks @luisfernandosoni).
Ids spell out accented letters (#102, thanks @luisfernandosoni): an option called "En Revisión" gets the id en-revision, not en-revisin, and an agent re-sending a list of options by name keeps their ids and colours.
New
A new row starts with the values its view's filters fix (#105, thanks @mungle, #91): made in a view filtered to "Status is Done", it is Done, and it no longer vanishes from the view it was made in.
Signed-in agents are listed in the account (#115, thanks @ymbroxe): your name, API tokens, Signed-in connections shows each connection with its access and last use, and ends it with one click. The Claude instructions in the connect dialog follow Anthropic's current menus.
Microsoft sign-in can be limited to one organisation (#110, thanks @ymbroxe): a Tenant field under Instance settings, Access, for single-tenant app registrations.
An instance can stop giving new accounts a personal workspace (thanks @primizor).
For agents: tools say whether they read or write, so clients in plan mode call the reading ones without asking (#103, thanks @thisispvb); get_page reads a page's note trail with include_notes (#118); a comment names the agent that wrote it (#116); list keeps to workspace_id for pages and templates (#111); every writing tool declares idempotency_key (#119). All thanks to @ymbroxe, except #103.
Upgrading
Nothing to do beyond replacing the binary or pulling the image. Two columns are added to the comments table on the first start.
Read-only tokens and templates. A read-only API token could save any page its account may write as a template over MCP (save_as_template): the tool was missing from the list of writing tools the token scope is checked against. That list now names the reading tools instead, so a tool missing from it is refused to a read-only token rather than let through, and a test runs every tool in the catalogue against a read-only token. A template saved from a private page stayed private to its owner, so nothing became visible to other people. It was reported privately, with a reproduction and a patch: thank you.
Fixes
You choose whether a signed-in agent may write (#83, thanks @rocktavious). The consent screen showed the access a client asked for and offered no way to change it, so a client that asks for nothing in particular, like Claude Code's claude mcp add --transport http, only ever got read access. The screen offers read or read and write now, starting on what the client asked for, and the client is told what it got.
A filter "is" on a number or a checkbox finds its rows (#95, thanks @neatlittlemacapps). "Qty is 5" found only a 5 written as text, and "Done is Checked" or "is Unchecked" found nothing, in views and over the API alike. Numbers now compare as numbers, so 5 and 5.0 are one value, and a box nobody ever touched counts as unchecked, as it does on screen.
The formatting toolbar's lists open where they can be seen (#94, thanks @neatlittlemacapps). The block type and colour lists were cut off at the toolbar's edge, so the type of a block could not be changed from the toolbar. The toolbar still scrolls sideways on a phone.
A new row no longer stores the English word "Untitled" as its title. New in a collection wrote that word into the row in every language, so it turned up in search and exports as if somebody had typed it. A new row now has no title, like a new page, and wherever a title is missing the interface says Untitled in the reader's language. In some places (the sidebar, the library, links, gallery and timeline cards) it used to say so in English. Rows made before keep their title.
The wiki said New opens the new row. It has not done that since 1.1.0: you stay in the view, and a click opens the row (noticed in #92, thanks @neatlittlemacapps).
Upgrading
Nothing to do beyond replacing the binary or pulling the image.
Please upgrade if agents or API tokens reach your instance.
Credentials and workspaces, everywhere. 1.1.2 made API tokens and signed-in agents keep to their workspaces (a token narrowed to some, a workspace closed to agents or open to signed-in agents only) on every tool and route that names a page. A full pass over everything else found the places that name something else, and all of them follow the same rule now:
Agent tools: set_properties with a list of updates, deleting, resolving and reopening comments by id, the pages linking in through get_links, moving a page into another workspace, and the retry key, which handed a narrower token a wider one's cached answer.
REST: deleting a comment, the file lists, the activity log, the workspace list, the skill bundle, the change feed, importing a workspace and using a blueprint.
The calendar key, emergency access and a workspace's rule for agents need a browser sign-in now. The calendar key opens a feed of every workspace of the account without a sign-in, so a token, read-only included, could hand it out.
A page moved to another workspace took neither its files nor its history along in the file list and the activity log; both follow the page now. The file index is rebuilt once on start.
Agent tools that name no page logged the new page's title in the account's default workspace; they log where the work was done.
The calendar feed named any collection or workspace by id; it names only what its subscriber may read.
Shared collections and ids. A shared collection printed the ids of people and of related rows, a private row's included. The new reader (below) shows neither.
New
Shared pages read like the app (#35 and #40, thanks @grootbro). A shared page is rendered by the server in the app's own reading layout, with its images and files, still without the app and with no script but its own. A shared collection is a real read-only table: other people's private rows stay out and are not counted, and 50,000 rows come out at about 0.2 MB. Printing shows every row, in light even when you read in dark. The HTML download carries the page's pictures, so it opens anywhere.
Relative days in filters (#47, thanks @d-perelygin). A date condition can say today, yesterday, tomorrow or a distance from them (today-1w, tomorrow+3m), and a saved view keeps meaning the current day, the viewer's day rather than the server's. A value that is not a day yet waits instead of filtering; agents get an error naming it.
Tabs remember their view and scroll (#62, thanks @d-perelygin). A plain click on a collection row opens it in place and Cmd/Ctrl adds a tab, as everywhere else; Open rows in new tab in the account menu makes every click open a tab.
Tick boxes on board cards (from #74, thanks @d-perelygin): a checkbox on a card ticks without opening the card.
Fixes
A database an agent creates with its own schema no longer opens on an empty board grouped by a property it does not have, and a board grouped by a text or date column is refused instead of showing one column with every row in it (from #72, thanks @d-perelygin).
Property changes an agent makes to several rows at once are in the activity log, so they can be taken back like single ones.
A stray web/tests/node_modules link that 1.1.2 carried is gone.
A column resize that the browser cancels on a touch screen no longer leaves text unselectable until a reload.
Upgrading
Nothing to do beyond replacing the binary or pulling the image. One column is added to the accounts table (the row-opening choice); an older binary ignores it. The file index is rebuilt once on the first start, which takes a moment on an instance with many files.
If a script of yours reads GET /api/ics or the emergency-access list with an API token, or changes a workspace's rule for agents with one, it now gets 403 with session_required; those are a person's decisions. A token narrowed to particular workspaces sees only those in GET /api/workspaces and the skill bundle.
What's Changed
Remember tab view and scroll; open collection rows in new tab by @d-perelygin in #62
Improve public document reading, sharing and portable HTML downloads by @grootbro in #35
Render shared collections as native read-only tables by @grootbro in #40
Smart dates in collection filters: today/yesterday/tomorrow/today-N(d/w/m/y) by @d-perelygin in #47
Please upgrade. Several of these matter for any instance that is reachable from the internet, and most for one behind the built-in Cloudflare tunnel.
Client addresses behind a proxy. Behind a proxy that appends to X-Forwarded-For (Cloudflare, so also the built-in tunnel, and nginx in its usual setup) salt.md read the first entry, which the caller writes. Anybody could name their own address: a fresh one for every password guess past the sign-in limit, or somebody else's, to use up that address's budget of wrong tokens, which refuses every token from there, valid ones included. Only the entry the proxy wrote counts now.
Links in the print view and on shared pages. A link inside the text went out as typed, so a link written as javascript: ran on one click with the session of whoever had the page open. Every link is checked now, and both documents carry a Content-Security-Policy that admits only their own script.
Workspace scope and agent rules. A token limited to one workspace could create pages in another, instantiate its templates, export it and take back changes in it. A workspace set to No agents or Only signed-in connections still took writes from tokens over REST, and an unrestricted token could read and write its pages over MCP by id. A viewer's import, and MCP create_page / create_database without a workspace, could land in a workspace the viewer may only read (thanks to @mungle for #78). Writing now goes through the same gates as reading.
Open editors after access ends. An open editor kept receiving every edit after its reader lost the page: made private, removed from the workspace, signed out. Access is checked again every 15 seconds and the editor closes.
A page nobody could open. Anybody with write access could make a page unopenable in the editor for everyone, with a few hundred tiny edits and by ignoring the request to compact them. Such a page opens again.
Share passwords could be guessed as fast as requests arrived. Guesses are limited per link and address, on the form and on the JSON route.
Go 1.27.2. 1.1.1 was built with Go 1.25, with ten reachable standard-library advisories that are fixed only in 1.26.9 and 1.27.2. Binaries and the image are built with Go 1.27.2 now; govulncheck reports nothing reachable. The frontend lockfile is refreshed within its ranges (npm audit: 27 findings to 4, none of them shipped to the browser in a way that matters, see below).
Fixes
An open tab could undo an agent's write (#79). Typing in the second before an agent rewrote a page, a tab whose connection dropped during the rewrite, or a tab loading the page at that moment could put the old text back, with the agent's text in no revision. Every write from outside the editor now starts a new document generation; a stale copy is refused and kept in the page history instead. Thanks to @d-perelygin for the report.
Sharing (#68). Opening the globe menu published a page on the spot, so looking at a page's sharing state shared it, and checking whether Stop sharing had worked published it again under a new address. The menu now only shows the state; a link exists after Create link or New link. Changing the expiry or password waits for New link instead of silently replacing the link you just copied, a refused Stop sharing says so, and public pages are sent with Cache-Control: no-store. The library shelf that lists pages visible to the workspace is called Workspace instead of Shared.
An open collection kept its first copy of the schema and sent it back with every view change, so a property an agent had added meanwhile disappeared when somebody resized a column. Only the changed half is sent now, and open views pick up schema changes as they happen.
A saved filter without a value no longer takes a collection view down (#71).
Moving a page to the trash returns to its parent and closes its tab (from #77).
New
Agent keys from listed addresses only.Instance settings → Access → Agent keys: an opt-in list of addresses and networks. With it on, API tokens and signed-in agents are accepted only from there, over MCP and REST; browser sign-ins are not affected. Add Anthropic (Claude) fills in the range Anthropic publishes for Claude's connectors. Claude Code running on your own computer calls from your own address.
cloudflared stays current. salt.md now updates the cloudflared it downloaded itself: daily, checked against the SHA256 GitHub records for the file, and a running tunnel restarts on the new version in a few seconds. The settings show the version and offer Check now. The tunnel token no longer appears in the process list.
The library and the graph open pages with the modifier rule (#71).
Upgrading
Nothing to do beyond replacing the binary or pulling the image; there is no data migration. If you run salt.md behind two proxies in a row, let the inner one resolve the visitor's address (see Reaching it from outside in the wiki), or every visitor arrives as the outer proxy.
What is left in npm audit needs Vite 8 or Mermaid 12: the development server, and a low-rated KaTeX issue inside Mermaid. Both are separate decisions.
What's Changed
Small fixes: config normalize, header passthrough, modifier-click follow-ups by @d-perelygin in #71
Enforce workspace content permissions consistently by @mungle in #78
Please upgrade if you share pages publicly or keep private rows in shared collections.
Private rows. A row marked private in a collection was hidden from other members' row list, but could still be read through the Markdown export, get_page over MCP, the calendar feed and a publicly shared collection. All four now follow the same rule as the row list: other people's private rows stay out, and a visitor without an account sees none.
Comments on shared pages. A publicly shared page carried every comment on the page, with its author, and the workspace name in its HTML source, hidden only by CSS. Anything the page does not show is no longer sent.
Diagrams. A diagram's stored picture was written into the print view as markup, and the sanitizer did not know every SVG event handler. Somebody with write access to a page could run script for whoever opened its print view or its shared link. Diagrams are now embedded as images, where no script runs.
Also in this release
Español. The interface is available in Spanish (#32). Only the German catalog has to be complete for a build now; other languages may trail and show English until they are topped up.
Sharing. A globe marks pages that are shared publicly (#61). Opening the share menu on a page that is already shared no longer replaces its link; it says so and offers New link and Stop sharing.
Collections. URL properties can be edited, cleared and opened separately (#39). Filter groups combine conditions with OR in the REST API and in query_rows (#57).
Editor. A block that lists the page's sub-pages (#50). A modifier click opens board cards, notes and sub-pages in a second tab (#52).
Self-hosting. An unknown command-line argument no longer starts a second server (#45). A guide for rootless Podman with a Quadlet service (#42). The OAuth client registration answers with its JSON content type (#31).
Wording. The last German strings in the interface are translated (#48), and the interface and server messages read without em dashes.
What's Changed
Reject unknown CLI commands instead of starting the server by @dimitree2k in #45
Localize hardcoded German strings (form submit, invite form) by @d-perelygin in #48
Filter groups on the server (REST filter_group, MCP filter_groups) by @d-perelygin in #57
Mark publicly shared pages with a globe badge by @d-perelygin in #61
Obsidian is local files and excellent for writing alone; its Bases plugin even gives table and card views. What it does not do is several people editing one document at once, or an agent reaching the vault over MCP.
How does it compare to Docmost, Outline or AppFlowy?
Docmost and Outline each want PostgreSQL and Redis beside the app; AppFlowy Cloud's own compose file brings up eleven containers. salt.md is one process with SQLite compiled in.
Does collaboration need a sync server?
No. The Yjs relay is part of the same binary. Edits merge through CRDTs, so two people editing the same paragraph offline converge when they reconnect.
Can I get my content back out?
Yes, in three ways: Markdown export for a single page or a whole workspace as a ZIP, a native workspace archive that moves everything to another instance including databases and files, and the SQLite file itself.
Where is my data?
In one directory you choose, containing a SQLite file and your uploads. Nothing is sent anywhere. There is no telemetry and no phone-home.
What do I need to run it?
A machine that can run a single binary. No database server, no runtime, no dependencies: SQLite is compiled in and the frontend is embedded.
Is salt.md open source?
Yes, under the AGPL-3.0. Use it anywhere, including at work, with no fee and no separate licence. If you change it and let others use your version over a network, you publish those changes.
0 comments