An Obsidian-style linked knowledge base for PhoenixKit, backed by real markdown files.
Admins open Knowledge Base in the sidebar, create a vault, and write
markdown notes in it. Notes live in a folder tree, carry tags, and link to one
another with [[wiki-links]]; each note shows what links back to it, and the
vault keeps a running list of everything linked to but not yet written.
Files are the source of truth
A vault is a directory. A folder is a directory. A note is a <Title>.md
file — the filename is the title, exactly as in Obsidian. So rg, git,
pandoc, vim and Obsidian itself all work on the tree directly, and nothing
you write is trapped in a database.
<root>/system/<vault-slug>/… organisation vaults
<root>/users/<email-slug>/<vault-slug>/… personal vaultsPostgres is an index of those files: cached content for full-text search, parsed front matter, tags, and the whole link graph. Delete the tables and a rescan rebuilds every bit of it from the markdown.
Keeping the index honest, without a watcher
There is no filesystem watcher. Edits made through the web UI index
themselves; edits made anywhere else are picked up by Rescan — the button
on a vault, or mix phoenix_kit_knowledge_base.rescan from a shell or a cron
job. A rescan compares (mtime, size) first and only hashes what looks
changed, and it recognises a moved file by content hash so an external mv
keeps the note's uuid, its permalink and every backlink into it.
Two things follow from having no watcher, and both are handled rather than hoped about:
- The web UI never clobbers an external edit. Every save first compares the file's hash with the indexed one and refuses if they differ, offering to reload or to overwrite deliberately.
- Titles are not unique. The filesystem allows
research/Ideas.mdandarchive/Ideas.md, so the index describes that rather than rejecting it.[[Ideas]]resolves to a note in the same folder first, then the shortest path;[[research/Ideas]]says exactly which one you meant.
Two browsers, two people
Every vault page subscribes to a PubSub topic for that vault, so a note added,
renamed, saved or deleted anywhere in the running application — the Rescan
button included — reaches every open session. mix phoenix_kit_knowledge_base.rescan is the exception: it runs in its own OS
process, with no PubSub, so open pages see its changes on their next
navigation. Run PhoenixKitKnowledgeBase.Rescan.run/1 on the live node (a
release's rpc) when they should refresh at once.
A note being read is refreshed in place; a note being edited raises a
banner instead, because the buffer may hold unsaved keystrokes.
Concurrent edits to the same note are detected, not merged: the second writer is stopped by the stale-write guard and given an explicit choice between reloading and overwriting. Two people on different notes in a shared vault is the case this supports well; two people in the same note at the same time wants a CRDT, which this is not.
Vaults
A vault is the namespace links resolve in — [[Foo]] never crosses a vault
boundary.
- System vaults are org-wide, created by anyone holding
knowledge_base.manage_system_vaults, who are implicitly owners of all of them. - Personal vaults belong to one person. Nobody else sees a personal vault in the app — not even an org Owner — unless its owner adds them as a member.
Both share one membership model (owner / editor / viewer), and
PhoenixKitKnowledgeBase.Vaults is the single place that decides any of it.
Note the limit of that, plainly: membership is enforced in the application. The notes are files, so anyone with shell access to the server can read every vault. That is inherent to storing notes as files, not a gap to be closed here.
Zero host setup
The editor is Leaf — the same one PhoenixKit
uses elsewhere — in its Obsidian-style hybrid live-preview mode, with [[
and # inline-suggestion triggers. Rendering goes through core's MDEx +
sanitizer, so this module ships no JavaScript and asks the host for nothing
beyond the dependency and a root path.
Note how Leaf's own bundle arrives: core's vendored phoenix_kit.js
registers a placeholder hook that lazy-loads leaf.js from jsDelivr on first
mount. That is core's mechanism, but it means the browser must be able to
reach the CDN and a strict script-src CSP must allow it. Loading a vendored
leaf.js before app.js short-circuits the loader. css_sources/0 is declared so a host running core's Tailwind source
compiler picks up the classes used here.
Installation
# host mix.exs
{:phoenix_kit_knowledge_base, "~> 0.1"}
# host config
config :phoenix_kit_knowledge_base,
root: Path.expand("../priv/knowledge_base", __DIR__)Then mix deps.get and mix phoenix_kit.update (creates the five
phoenix_kit_kb_* index tables). Enable it on the admin Modules page.
To adopt notes you already have, move or symlink the directory into place under the root, create a vault with the matching slug, and press Rescan.
Summary
Functions
The registry and dynamic supervisor behind live editing.
Tailwind scans this app's templates for the classes the knowledge base UI uses.
Whether the module is on.
Config for the admin Modules page.
The phoenix_kit_kb_* table coordinator (run by mix phoenix_kit.update).
The base knowledge_base permission opens the section; the sub-permission
decides who may create and administer system vaults.
Multi-page routing (vaults, workspace, members) — see PhoenixKitKnowledgeBase.Routes.
Functions
The registry and dynamic supervisor behind live editing.
Core starts these under its own supervisor, so the host needs no setup. They
start whether or not this module is enabled — children/0 is collected
statically, before anything can ask the database — but an empty registry and
an idle DynamicSupervisor cost nothing until somebody opens a note.
Tailwind scans this app's templates for the classes the knowledge base UI uses.
Whether the module is on.
Rescues and returns false because this is called before migrations have
necessarily run — a settings table that doesn't exist yet must read as
"disabled", not crash host startup.
Config for the admin Modules page.
Only enabled?/0 — this runs on every render of that page, and counting
vaults or notes would add a query per page load for a number nobody reads
there.
The phoenix_kit_kb_* table coordinator (run by mix phoenix_kit.update).
The base knowledge_base permission opens the section; the sub-permission
decides who may create and administer system vaults.
Note what the sub-permission deliberately does not grant: access to other
people's personal vaults. See PhoenixKitKnowledgeBase.Vaults.
Multi-page routing (vaults, workspace, members) — see PhoenixKitKnowledgeBase.Routes.