Mount the graph in Rails
Your team's knowledge, served inside your app, behind your auth.
Why mount it
okf server is great on a laptop, but a team usually wants the knowledge graph where the team already is: inside the internal Rails app, behind the same sign-in as everything else. The server was built for exactly that. The page under the UI is a plain Rack app, and its endpoints are mount-relative, so it works identically at / or under any prefix.
Add the gem
# Gemfile
gem "okf"
require "okf" loads only the library: the model, the analyzers, and the on-disk handles. The graph server is a separate, on-demand require, require "okf/server/app", which is exactly how the gem's own CLI loads it. Your app's boot never pays for Rack machinery it does not serve. The architecture page explains why the footprint stays this small.
Mount it in routes
# config/routes.rb
require "okf/server/app" # the server loads on demand, like the CLI does
Rails.application.routes.draw do
knowledge = OKF::Server::App.new(
OKF::Bundle::Folder.load(Rails.root.join(".okf").to_s)
)
mount knowledge => "/knowledge"
end
That is the whole integration. Visit /knowledge and you get the same interactive graph as the public demo: nodes by type, the inspector with rendered Markdown, and the catalog, files, tags, and stats views. The files view is a nested tree of the bundle on disk, with each index.md and log.md sitting as a row at the top of the folder it documents, and an "Indexes only" toggle when those are all you want to see.
Many bundles: mount a hub
OKF::Server::App serves one bundle. To serve several behind one prefix, with an in-page switcher that jumps between them, mount OKF::Server::Hub instead. You hand it an ordered list of OKF::Server::Hub::Bundle structs (slug, folder, title); the first is the one the prefix root opens.
# config/routes.rb
require "okf/server/hub"
Rails.application.routes.draw do
bundles = [
OKF::Server::Hub::Bundle.new(
"handbook",
OKF::Bundle::Folder.load(Rails.root.join(".okf").to_s),
"Handbook"
),
# ...more bundles...
]
mount OKF::Server::Hub.new(bundles) => "/knowledge"
end
Each bundle is served at /knowledge/b/<slug>/, and /knowledge redirects to the default. A plain Hub.new(bundles) is read-only: the registry-editing routes (POST /registry/*) answer only when you pass both an explicit writable: true and a registry:, and neither the hub nor the app ever writes to your bundle's Markdown either way. Mounting it exposes reading only, which is what you want inside someone else's app.
Preserve the trailing slash (the hub's one mount contract)
The hub serves each graph at /knowledge/b/<slug>/, with the trailing slash, and 301-redirects the slashless /knowledge/b/<slug> onto it. That slash is load-bearing, not cosmetic: the page's fetch endpoints are relative (../../search, ../<sibling>/), so they resolve correctly only when the document itself lives at .../b/<slug>/.
Here is the trap. A normalizing host router, Rails' mount included, strips the trailing slash from the mounted app's PATH_INFO before the hub ever sees it. So a browser request for /knowledge/b/handbook/ reaches the hub looking slashless, the hub 301s to add the slash, the router strips it again, and the two spin into an infinite redirect loop (ERR_TOO_MANY_REDIRECTS).
The hub cannot fix this from standard Rack: it has no way to tell "the visitor really typed no slash" (301 is correct) from "the router stripped the slash the visitor sent" (301 is wrong). Only the host still holds that signal, so the host has to restore the slash before delegating. In Rails, wrap the hub in a small middleware:
class OKFMountAdapter
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
original = request.original_fullpath.to_s.split("?", 2).first
path_info = env["PATH_INFO"].to_s
# Append only when the browser's real path ended in "/", so leaf
# endpoints like /node and /search are left untouched.
if original.end_with?("/") && !path_info.end_with?("/")
env["PATH_INFO"] = "#{path_info}/"
end
@app.call(env)
end
# Keep `rails routes` and the dev error page readable. A hub's default
# inspect walks every loaded bundle, concept, and body, which is
# megabytes of output; a terse one keeps the route table usable.
def inspect = "#<OKFMountAdapter app=OKF::Server::Hub>"
end
mount OKFMountAdapter.new(OKF::Server::Hub.new(bundles)) => "/knowledge"
That wrapper is also the natural place to enforce auth: return a 302 to your sign-in route, or a 404, before you call @app.call(env), since a mounted Rack app bypasses your controllers' before_actions. The auth patterns below apply to the hub as much as to the single-bundle app.
Put your auth in front
The graph inherits whatever stands in front of it, which is the point of mounting instead of running a second service. With Devise:
authenticate :user do
mount knowledge => "/knowledge"
end
Or, framework-free, wrap it in basic auth:
protected_graph = Rack::Builder.new do
use Rack::Auth::Basic, "Knowledge" do |user, pass|
ActiveSupport::SecurityUtils.secure_compare(pass, ENV.fetch("KNOWLEDGE_PASS"))
end
run knowledge
end
mount protected_graph => "/knowledge"
Anything Rack understands works: Devise, Warden scopes, IP allowlists, your SSO middleware.
No Rails? Plain Rack works too
# config.ru
require "okf"
require "okf/server/app"
run OKF::Server::App.new(OKF::Bundle::Folder.load(".okf"))
rackup serves it with whatever server you already run (Puma, Falcon). The built-in WEBrick runner behind okf server is just a convenience wrapper around this same app.
What updates live, and what does not
Concept bodies are fetched from disk on every click, and the Files view re-reads log.md every time it is opened, so editing a concept or appending a log entry shows up next time with no restart. The bundle's structure (which files exist, how they link, the authored index maps) is read when Folder.load runs, so adding or removing concepts needs an app restart or a fresh folder load, same as any boot-time configuration.
One boundary worth respecting in a mounted setup: the page renders bundle content through DOMPurify and escapes everything it inlines, but it still loads its viewer libraries from a CDN and renders whatever links the bundle carries. Serve bundles you trust; the graph server page has the full trust write-up.