Where published sites go: the built-in /p/ hosting, and the local directories, SSH servers and Cloudflare Pages accounts you configure
A space, folder or note can be published as a static documentation site. Every server can host sites itself at /p/. You can add other destinations in config.toml: a directory on the Kribble host, an SSH server, or Cloudflare Pages. How people publish is in Publishing sites. This page is about the server side.
Publishing a space, folder or note, or building it again, needs space admin rights on its space (or ownership of the organisation). Through the API, the CLI and MCP, an access token with the publish or write scope acts for its creator (Organisations and access).
Someone publishes. kribble-web checks the request, stores the site with status queued and returns at once.
kribble-builder looks for queued sites every 10 seconds. It claims one (status building), renders every note to HTML in a temporary directory, and adds a markdown copy of each page, llms.txt, llms-full.txt and kribble-docs.json for AI agents.
It copies the result to the site's target and sets the status.
Up to [builder].workers sites build at once. Publishing a site that is already queued or building doesn't queue it twice.
Status | Meaning |
|---|---|
| Waiting for the builder. A site that stays here means the builder isn't running. |
| Being built now. If the builder stops during a build, it puts the site back to |
| Built and copied to its target. |
| The site couldn't be generated (for example a note that couldn't be loaded). The builder log has the reason. |
| The site was generated, but copying it to an external target failed. The error is stored with the site, and the publish dialog and |
Each claim counts as an attempt. When the builder starts and finds a queued site with 3 or more attempts, it marks it failed instead of building it again, so a site that crashes the builder can't do so forever. Publishing or rebuilding again resets the count. To retry after failed or publish_failed, fix the cause and rebuild: Publish in the web app, or kribble redeploy.
Sites published to the built-in target (kribble) are stored in Kribble's file storage and served by kribble-web:
At https://<server>/p/<organisation>/<slug>/, or /p/~<username>/<slug>/ for a personal vault. The slug is chosen when publishing and must be unique within the organisation.
Public sites are readable by anyone. Their pages are stored under sites/<organisation id>/<slug>/ in the public storage area (<dir>/public, or public_bucket), and their images and other assets load from [storage.s3].public_bucket_url, which must be https://<server>/files-public.
Space members sites are stored in the private area and readable only by members of the space. People who aren't signed in are sent through /login and back. The CLI reads them with its access token (read scope). Access is checked on every request, and removing someone from the space or organisation takes effect at once.
Switching a site between public and space members moves its files to the other area.
Unpublishing deletes its files. Deleting a space or organisation deletes its sites.
There are no custom domains for the built-in target. To serve a site under its own domain, use one of the targets below, or proxy that domain to /p/<organisation>/<slug>/ yourself.
You define external targets in config.toml. Every key is listed in Configuration. They have these rules in common:
name is the identity. Names are unique across all target types. Each site stores the type and name of its target, so renaming or removing a target breaks its sites: their rebuilds end in publish_failed, and unpublishing can't clean up the files.
allowed_orgs decides who sees the target in the publish dialog, kribble targets and MCP. List organisation names (lowercase), or "*" for every organisation. An empty or missing list means nobody, so you can add a target before giving access. A personal vault is allowed by its generated name, like vault-3f9a1c0b7d2e, or by "*". Taking an organisation out of the list stops new sites, but existing ones still rebuild.
External sites are always public. The target can't check Kribble's space membership, so the space-members option is ignored.
Assets are included in the output, so the site works without Kribble.
The target is fixed when the site is first published. To move a site, unpublish it and publish it again to the new target.
Checked at startup. A target without its required keys stops both binaries with an error.
For local and SSH targets, each site goes into a subdirectory of the target's base directory, and that directory is emptied before every build. So a subdirectory is required, and Kribble refuses one that another site on the same target already uses. The publish dialog, kribble deploy and MCP suggest one made from the site's name. Still, never point base_dir or remote_base_dir at a directory holding anything else, and don't nest one site's subdirectory inside another's.
Unpublishing a site on an external target deletes it there: it empties the local or remote subdirectory, or deletes the Cloudflare Pages project.
[[publish_targets.local]] copies each site to <base_dir>/<subdirectory> on the Kribble host, for a web server on the same machine to serve. Files are written under a temporary name and renamed, so readers never see half-written files.
[[publish_targets.local]]
name = "docs-www"
base_dir = "/var/www/docs"
allowed_orgs = ["acme"]
The builder writes the files, and kribble-web deletes them when a site is unpublished. Both run as kribble and need write access to base_dir. With the systemd units from install.sh, allow the path in both, because they make everything outside /opt/kribble read-only:
sudo mkdir -p /var/www/docs && sudo chown kribble:kribble /var/www/docs
sudo systemctl edit kribble-builder # add: [Service] ReadWritePaths=/var/www/docs
sudo systemctl edit kribble-web # the same
sudo systemctl restart kribble-web kribble-builder
In Docker, mount the directory into both containers at the same path as base_dir.
Serve it with your web server, for example nginx:
server {
listen 443 ssl;
server_name docs.example.com;
root /var/www/docs/handbook; # the site's subdirectory
index index.html;
location / {
try_files $uri $uri/ $uri.html =404;
}
}
Add text/markdown md; to the types block in /etc/nginx/mime.types, so browsers show each page's markdown copy instead of downloading it.
[[publish_targets.ssh]] uploads each site over SFTP to <remote_base_dir>/<subdirectory>.
[[publish_targets.ssh]]
name = "docs-prod"
hostname = "docs.example.com"
port = 22
username = "deploy"
private_key_path = "/etc/kribble/keys/docs_deploy"
passphrase = "" # only for a key with a passphrase
known_hosts_path = "/etc/kribble/keys/known_hosts"
remote_base_dir = "/var/www/docs"
allowed_orgs = ["acme"]
To set it up:
sudo mkdir -p /etc/kribble/keys
sudo ssh-keygen -t ed25519 -N "" -C kribble-publish -f /etc/kribble/keys/docs_deploy
sudo sh -c 'ssh-keyscan -t ed25519 docs.example.com > /etc/kribble/keys/known_hosts'
sudo chown -R kribble:kribble /etc/kribble/keys
sudo chmod 600 /etc/kribble/keys/docs_deploy
Then add /etc/kribble/keys/docs_deploy.pub to ~deploy/.ssh/authorized_keys on the remote host, and make sure deploy can write to remote_base_dir and the SSH server allows SFTP. Check the host key you scanned against the remote host's own key before you trust it.
The host key is always checked against known_hosts_path. There is no option to skip it. A target without known_hosts_path starts, but every publish to it fails.
Kribble offers Ed25519, ECDSA and RSA with SHA-2 host keys, not SHA-1 ssh-rsa. The scanned key should be one of those types.
A key with a passphrase needs passphrase. Without it, publishing fails with a message saying so.
The connection times out after 15 seconds.
Keep the keys out of /home. The systemd units hide it from Kribble. In Docker, mount the key directory into both containers.
[[publish_targets.cloudflare_pages]] creates one Cloudflare Pages project per site and uploads it with the Direct Upload API.
[[publish_targets.cloudflare_pages]]
name = "cf-pages"
account_id = "<cloudflare-account-id>"
api_token = "<cloudflare-api-token>"
production_branch = "main"
project_prefix = "kribble"
allowed_orgs = ["acme"]
The API token needs the account permission Cloudflare Pages: Edit. Create it under My Profile → API Tokens in the Cloudflare dashboard, limited to the account. The account ID is on the account's overview page.
Project names are <project_prefix>-<published name>-<slug>, lowercased with other characters replaced by -, and at most 63 characters. The name is fixed when the site is first published, so renaming the space later doesn't rename the project. Kribble creates the project if it doesn't exist.
The address is https://<project>.pages.dev/, which kribble sites and the publish dialog show.
production_branch (default main) is set on the project and used for every deployment, so each build is a production deployment.
Files over 25 MiB can't be uploaded to Pages. A site with one ends in publish_failed.
Custom domains are added to the project in the Cloudflare dashboard (Workers & Pages → the project → Custom domains). Kribble doesn't manage them, and they stay when the site is rebuilt. Unpublishing deletes the project, custom domains included.
People with the kribble CLI can publish without the web app (The kribble CLI):
kribble targets # where sites can go, per organisation
kribble deploy "Handbook" --target local --target-name docs-www --subdir handbook --wait
kribble deploy "Engineering/SDK" --slug sdk --access space_members
kribble sites # every site, its URL and status
kribble redeploy "Handbook" --wait # build again after notes changed
kribble deploy publishes a space, folder or note, or builds it again with its existing settings if it is already published. AI agents do the same over MCP with the list_deploy_targets, deploy_site, redeploy_site and list_sites tools (AI agents). CI scripts can call POST /api/v1/spaces/{spaceId}/publish with a publish-scoped token.