Extended usage #
Multi-Site Mode #
Multi-site mode allows running multiple independent sites from a single lichen.page server instance. Sites are routed based on the HTTP Host header, and a dashboard provides centralized management.
Quickstart #
cargo run --features "mdbook" -- server run --multi
This starts the server in multi-site mode with default settings. Visit http://localhost:9000/ to access the dashboard.
Local Development #
For local multi-site development, use .local or .localhost domains which are routed to 127.0.0.1 via /etc/hosts or dnsmasq.
On nixos here is an example configuration to make a wildcard subdomain route to 127.0.0.1 with dnsmasq:
services.dnsmasq = {
enable = true;
settings = {
# answer *.lichen.local with 127.0.0.1
address = "/.lichen.local/127.0.0.1";
};
};
On mac .localhost may automatically resolve to 127.0.0.1, but on linux you need to specify this somehow.
Then create a lichen.toml in your lichen root directory for local development which uses this domain as the dashboard_domain. The default location of the lichen root when run in multi-site mode is ./lichen-root,
so the lichen.toml should be at ./lichen-root/lichen.toml.
server_port = 9000
development_mode = true
hot_reload_templates = true
dashboard_domain = "lichen.local"
development_mode- generateshttp://URLs instead ofhttps://hot_reload_templates- reloads server templates on each request for faster iteration
You can then access the dashboard at http://lichen.local:9000.
And create new sites using *.lichen.local subdomains (e.g. sitea.lichen.local, siteb.lichen.local).
You can then view these sites at http://sitea.lichen.local:9000/ and http://siteb.lichen.local:9000/.
In a live deployment things would work similarly, but DNS records need to be wired up to your server.
In production, Caddy can be used as a reverse proxy for TLS termination. See the deploy configs in references/lichen-deploy/ for examples.
If you also want support for hugo, then ensure a hugo binary is on your path and run with all features:
cargo run --all-features -- server run --multi
Directory Structure #
Multi-site mode uses a root directory containing all sites and shared user data:
lichen-root/
├── lichen.toml # server configuration
├── users/ # shared user authentication
│ ├── alice.toml
│ └── bob.toml
├── site-a.com/ # site directories named by domain
│ ├── settings.toml
│ ├── content/
│ └── dist/
└── site-b.com/
├── settings.toml
├── content/
└── dist/
Configuration #
Settings are loaded from lichen.toml in the root directory. Configuration sources are applied in this order (highest precedence first):
- CLI arguments (
--root-dir,--settings) - Environment variables (
LM_ROOT_DIR,LM_SETTINGS_PATH,LM_*) lichen.tomlfile- Default values
The root directory defaults to ./lichen-root. The settings path, if provided via CLI or env var, is resolved relative to the root directory.
Configuration Options #
| Setting | Default | Description |
|---|---|---|
server_port |
9000 |
Port the server listens on |
use_auth |
false |
Require authentication for protected routes |
dashboard_domain |
none | Domain that serves the dashboard (e.g., dashboard.example.com) |
admin_user |
"admin" |
Default username when auth is disabled |
development_mode |
false |
Use http:// URLs instead of https:// |
hot_reload_templates |
false |
Reload server templates on each request |
auth_providers |
["file"] |
List of auth providers (e.g. "file", "atproto", "oidc") |
public_url |
auto-generated | Public URL for the server (defaults to http(s)://localhost:{port}) |
default_storage_limit |
none | Default storage limit per user (e.g. "500MB", "1GB") |
CLI Commands #
The following hosting commands use the --multi flag:
# start the server
cargo run -- server run --multi
# build all sites
cargo run -- server build --multi
# build a specific site
cargo run -- server build --multi --domain example.com
# export a site to zip
cargo run -- server export --multi --domain example.com
# manage users
cargo run -- server users --multi add alice
cargo run -- server users --multi set-password alice
CLI Options #
--root-dir <DIR>- Override the root directory--settings <FILE>- Override the settings file path
Domain-Based Routing #
When dashboard_domain is configured, the server routes requests based on the Host header:
- Requests to
dashboard_domaingo to the management dashboard - Requests to site domains (e.g.,
site-a.com) serve that site's content - Requests to unknown domains return 404
When dashboard_domain is not set, unrecognized domains fall back to the dashboard.
Authentication #
When use_auth = true, users must log in to access:
- The dashboard
- CMS routes for any site
Users are stored in the shared users/ directory. Session cookies are configured to work across subdomains when dashboard_domain is set.
Site Ownership #
Each site tracks its creator. Only the site owner can access that site's CMS routes. This is enforced by the ACL guard middleware.
Invite Codes #
By default, anyone with an ATProto account can sign up. To restrict signups, add an [invite_codes] section to lichen.toml:
[invite_codes]
codes = [
{ code = "welcome-2026", max_uses = 10 },
{ code = "open-invite", max_uses = 0 }, # 0 = unlimited
]
New users are prompted for a code on their first login. Existing users can log in freely without one. Codes can also be generated from the command line:
# random code, unlimited uses
cargo run -- server invites create --multi
# custom code with a use limit
cargo run -- server invites create --multi --code "my-code" --max-uses 5
When no [invite_codes] section is present, signups are open.
Static Site Generators & Features #
Multi-site mode can scaffold new domains with different SSGs. The available generators depend on the Cargo features you build with:
lichen(default, always available)hugo(requires--features hugoat build/run time)mdbook(requires--features mdbook)
When you enable additional features, the dashboard's Create site form exposes those options in the SSG dropdown. The generated settings.toml stores the chosen ssg so future builds use the correct pipeline. Example:
cargo run --package lichen-cli --features "hugo mdbook" -- server run --multi
Without a feature enabled, its option is hidden and API requests using that ssg value will fail.
Publishing Targets #
Sites can publish their built output to external hosting platforms. Configure a [publish] section in the site's settings.toml and click "Publish" in the CMS, or enable auto_publish = true to publish on every save. Supported targets: GitHub Pages, Codeberg Pages, and wisp.place (not yet implemented).
By default, clicking Publish also pushes any unpushed git commits to the remote. To skip the git push and only run other publishing targets (e.g. standard.site), set push_on_publish = false in the [git] section:
[git]
auto_push = true
push_on_publish = false
standard.site #
When built with the atproto feature, lichen can publish site content as standard.site records on the user's ATProto PDS.
For ATProto users, a [standard_site] section is auto-scaffolded in settings.toml when a site is created. You can also configure it manually:
[standard_site]
enabled = true
name = "My Site"
description = "A short description"
[standard_site.frontmatter]
title = "title"
description = "description"
publish_date = "date"
updated_at = "updated"
draft = "draft"
tags = "tags"
cover_image = "cover_image"
The frontmatter mapping tells lichen which frontmatter field names in your markdown files correspond to standard.site document fields. Defaults are provided per SSG (e.g., Hugo uses lastmod for updated_at).
On publish, lichen creates a site.standard.publication record for the site and a site.standard.document record for each non-draft post.
Title extraction — if no title is set in frontmatter, lichen falls back to the first markdown heading (# ... through ######), then the first HTML heading (<h1> through <h6>), then the filename.
Cover image — if no cover_image is set in frontmatter, lichen looks for the first local image in the body (markdown  or HTML <img src="path">). Remote URLs (http://...) are skipped. The image is uploaded as a blob to the PDS and attached to the document record.
textContent — the document body is converted to plain text (markdown and HTML formatting stripped). Link URLs are preserved in text (url) format so they are not lost.
The "Sync to PDS" button on the site settings page triggers a publish manually.
Filtering which posts are published
include_paths / exclude_paths — filter by path prefix. Leading slashes are optional. If no include is set, then all markdown files which do not have draft: true in their fronmatter are included.
[standard_site]
enabled = true
include_paths = ["posts/"] # only publish posts under posts/
exclude_paths = ["posts/drafts/"] # but skip posts/drafts/
When both are set, include_paths is applied first, then exclude_paths.
require_standard_site_gate — requires explicit opt-in per post. Only posts with standard_site: true in their frontmatter are published; all others (including posts with the field absent) are skipped:
[standard_site]
enabled = true
require_standard_site_gate = true
---
title: "My Post"
standard_site: true
---
When the gate is not set to true, the standard_site field, can also be used to specifically filter a post out.
| value | gate off | gate on |
|---|---|---|
| absent (default) | published | not published |
standard_site: true |
published | published |
standard_site: false |
not published | not published |
Setting standard_site: false in a post's frontmatter always excludes it, regardless of any other setting.
Full Command-line interface #
Lichen installs one executable. remote interacts with another server (clone, pull and
sync also modify local files). server runs or administers local hosting; offline commands
do not require a server process to be running.
lichen remote auth login | logout | status
lichen remote sites list | create | delete | info
lichen remote clone <domain> [local-dir]
lichen remote push [domain] [--all]
lichen remote pull [domain] [--all]
lichen remote sync
lichen remote debug pds-get-sites <handle>
lichen remote debug pds-get-repos <handle>
lichen remote debug pds-get-publications <handle>
lichen remote debug pds-get-documents <handle> [--site <publication>]
lichen remote debug pds-delete-publication <handle> <rkey>
lichen remote debug pds-delete-site <handle> <domain>
lichen server run [--port <PORT>] [--open]
lichen server init [--ssg <SSG>]
lichen server build [--domain <DOMAIN>]
lichen server export [--domain <DOMAIN>]
lichen server users add <username> [--password <PASSWORD>]
lichen server users set-password <username> [--password <PASSWORD>]
lichen server invites create --multi [--code <CODE>] [--max-uses <COUNT>]
lichen update [--local]
Use --help on any command for its arguments. Remote site creation accepts --ssg;
deletion accepts --delete-git. Login accepts --server, --handle, --password,
--token, and --invite-code; OAuth is the default in builds with ATProto support.
Without that feature, use password or token login; the PDS debug group is unavailable.
Do not combine a named domain with push/pull --all.
--json is supported by remote sites list/create/info and clone; other commands reject it.
Logs go to stderr. Client credentials remain in ~/.lichen/cli-config.toml, with
LM_SERVER_URL and LM_API_TOKEN overrides. LM_ROOT_DIR relocates both this client
data and the local manager's root. Server user files are separate from client credentials.
Server modes #
| Invocation | Target and configuration |
|---|---|
lichen server run |
Local manager at ~/.lichen, loopback-only, port 9000; no hosting settings file |
lichen server run --site ./site |
Standalone site and its settings.toml |
lichen server run --multi |
Hosting root ./lichen-root and its lichen.toml |
lichen server run --multi --root-dir ./hosting |
Explicit hosting root |
--site and --multi are mutually exclusive. --root-dir requires --multi.
For run, --settings <FILE> requires an explicit --site or --multi.
The local manager never becomes publicly exposed because hosting settings are inherited.
Its port precedence is --port, LM_SERVER_PORT, then 9000; hosted modes retain their
configured port unless --port is supplied. Port 0 selects an available port.
The server prints its UI URL; --open opens it only after initialization succeeds.
Explicit public_url values are preserved (for example, behind a reverse proxy); otherwise
the port in generated URLs follows the actual listener.
Offline init/build/export/users commands default to the standalone ./site, not the local
manager root. Give --site to select another directory, or --multi for hosting.
Multi-site build selects all sites unless --domain is given; export requires --domain.
Init is standalone-only; invites require --multi. Server --site always means a path,
not a domain. Passwords are prompted when omitted; an invite max-uses of 0 means unlimited.
For example:
lichen remote auth login --server dash.lichen.page
lichen remote clone example.com
lichen server run --open
lichen server build --multi --domain example.com
lichen server export --site ./site
The CLI crate owns the executable, while the server library remains reusable by the desktop app.
The normal build enables Git, ATProto and shell support. Use
cargo build -p lichen-cli --no-default-features for a minimal server; add features explicitly
(e.g. --features hugo,mdbook). Optional iroh behavior is described in
SITE-IROH.md.
Optional Features #
Beyond the SSG features (lichen, hugo, mdbook), the unified executable has a few other optional compilation features:
git #
Enables git version history and smart HTTP endpoints. Sites become git repositories with automatic commits on each save. Clients can clone, push, and pull via standard git over HTTPS.
atproto #
Enables OAuth login with an ATProto handle. User data is automatically backed up to their PDS via git-remote-pds.
oidc #
Generic OpenID Connect authentication provider. Allows login via any OIDC-compatible identity provider (e.g. Keycloak, Authentik).
iroh #
Note: the iroh feature was thoroughly tested in lichen 0.2, but has not been thoroughly tested tested in lichen 0.3. If people have a desire to use this, then I think it could be brought back with some polishing, and contributions are welcome, but for now it is in standby unless there is a strong desire to use it.
The iroh feature provides peer-to-peer sync via iroh for real-time collaboration without a server. peers exchange git pack files via iroh blobs, using a conflict-free merge driver to avoid conflicts, and using lowest sha wins as a tiebreaker so that git HEADs converge once their filetrees are identical. See docs/SITE-IROH.md for details.
shell #
In-browser terminal using xterm.js. Spawns a sandboxed PTY (via bubblewrap) accessible over WebSocket.
Deployment #
Docker images are published on Docker Hub:
| Image | Description |
|---|---|
notplants/lichen-full |
All features enabled (recommended) |
notplants/lichen-min |
Lichen SSG only, no external dependencies |
The Dockerfile is here.
For deployment guides (Docker Compose, Ansible, Helm), see the lichen-deploy repo.