semora-cli 0.5.0

Semora CLI: publish Markdown and HTML, and hear back from the page
Documentation

Semora

A self-hosted pastebin for HTML and Markdown with a back-channel to the agent that wrote it.

Any agent with a shell publishes a file or a folder with one command and gets a URL. You open it in your browser, read it with your favourite theme, and answer from the page: comments anchored to text, drawings sent as screenshots, messages, and events the page itself sends. The agent receives them with semora wait, fixes the document, pushes again, and your open tab reloads.

semora push plan.md --wait     # publish, then block until you answer in the browser

One Rust server binary (semora-server) and one Rust CLI (semora). SQLite plus a content-addressed blob folder. No Node, no CDN: every browser library is embedded. The gallery shows a thumbnail of every document, drawn by the server itself (no headless browser) in a separate, time-limited process.

Run the server

Docker

docker build -t semora .
docker run -d --name semora -p 127.0.0.1:8080:8080 -v semora-data:/data \
  -e SEMORA_PUBLIC_URL=https://semora.example.com semora
docker logs semora   # prints the first-run setup token

Binary + systemd

cargo install semora-server   # or, from a checkout: cargo build --release -p semora-server

Then follow the comments in deploy/semora.service; put deploy/Caddyfile (or any reverse proxy) in front for TLS. Websockets pass through.

First run

Open the public URL. Every page redirects to /setup until you create the admin (your email and a password) with the setup token printed in the server log (only someone with console access can finish setup). Then optionally add a passkey or an authenticator app, and choose the site title and whether sign-up is open.

Configuration

Environment variables or semora.toml (see crates/semora-server/semora.toml.example):

Variable Default Meaning
SEMORA_PUBLIC_URL http://localhost:<bind port> Public address (no path)
SEMORA_BIND 127.0.0.1:8080 Listen address
SEMORA_DATA_DIR data Database, blobs and secret.key
SEMORA_RP_ID public host WebAuthn relying-party id
SEMORA_MAX_UPLOAD_MB 25 Largest bundle
SEMORA_MAX_FILES 2000 Most files in a bundle
SEMORA_KEEP_REVISIONS 20 Revisions kept per document
SEMORA_TRUST_PROXY false Take client IPs from the last X-Forwarded-For entry (one reverse proxy)
SEMORA_CONTENT_CONNECT_SRC 'self' connect-src for document pages
SEMORA_THUMBNAIL_RENDERER this binary Program that draws gallery thumbnails, or off
SEMORA_LOG info Log filter

Use HTTPS in production: the session cookie is Secure and passkeys need a secure context (http://localhost counts).

Backup: copy the data folder (stop the server, or use SQLite's online backup for semora.db).

Back up semora.db before you upgrade. A new version may change the database, and an older one will not open it again.

Themes: eleven Markdown themes are built in (GitHub auto/light/dark, Air, Modest, Retro, Splendor, Sakura, Tufte, Newsprint, Night). Drop your-theme.css into the data folder's themes/ directory and restart to add one; a file named like a built-in theme replaces its stylesheet. Rendered Markdown sits in <article class="markdown-body" id="write">.

Mail

Semora sends email for sign-up, invites, password resets and email changes. It works without a mail server: sign-up and "forgot password" are hidden, and the admin page gives you invite and reset links to copy and send yourself. "Create an invite link" on the admin page makes a link that is not tied to an email address: it works once, for 7 days, and whoever opens it gives their own address when they sign up.

Variable Default Meaning
SEMORA_SMTP_HOST unset (mail off) SMTP server
SEMORA_SMTP_PORT 587, or 465 with tls SMTP port
SEMORA_SMTP_TLS starttls starttls, tls (TLS from the start) or none (only for a relay on the same machine)
SEMORA_SMTP_USER unset SMTP user name
SEMORA_SMTP_PASSWORD unset SMTP password; environment only, never in semora.toml
SEMORA_MAIL_FROM required with a host Sender, e.g. Semora <semora@example.com>

Links in emails use SEMORA_PUBLIC_URL, so set it. The admin page's Mail card sends you a test email.

Use it

Install the CLI with cargo install semora-cli (or cargo install --path crates/semora-cli from a checkout), then:

semora login --server https://semora.example.com   # or set SEMORA_URL + SEMORA_API_KEY
semora push notes.md
semora wait <id>

Inside a git checkout, make it a project so every worktree and machine updates the same documents:

semora project init            # creates the project and writes .semora; commit it
semora push docs/plan.md       # prints a stable link: …/p/<project>/docs/plan.md
semora project ls              # your projects (the web app lists them under Projects)

On another machine or worktree with the committed .semora, semora pull copies the project's documents into the folder, and semora status lists what changed here and what is newer on the server. A push of a file you pulled stops (exit code 5) if someone pushed it since, so no edit is lost without notice.

The owner can arrange a project's tree with rules, from the project page's Arrange section or with semora project map; links and pushes always use the real path.

The full guide for people and agents is served at /docs (and /docs.md), and printed by semora skill, which you can paste into any agent's instructions. Source: crates/semora-proto/guide.md.

After an update, each person sees what changed since they last looked, in one pop-up on their next page. The Changelog page, linked from the gallery, has every release.

A project's owner can invite people at three levels. A Viewer reads, a Reviewer also sends comments, drawings and messages, and an Editor also publishes and receives feedback with semora wait. Manage collaborators on the project page. An admin can also let project owners invite people who have no account yet (the switch is on the admin page and is off by default).

Security model

  • Document bytes are served under /c/<signed-token>/… into an iframe with sandbox and no allow-same-origin, and every /c/ response repeats the sandbox in its CSP. Uploaded HTML runs at an opaque origin: it cannot read Semora's cookies, storage or pages.
  • The session cookie is __Host-, HttpOnly, Secure, SameSite=Strict. Cookie-authenticated API calls and websocket upgrades must come from Semora's own origin; forms and writes carry CSRF tokens.
  • Content URLs carry an HMAC token (document, revision, user, one-hour expiry); the viewer mints a fresh one on every navigation.
  • Passwords use Argon2id; session tokens, recovery codes and emailed links are stored hashed. API keys are stored hashed (that is what is checked) and also encrypted, so you can copy your own keys from the account page; TOTP secrets are encrypted the same way, with a key from the data folder.
  • Every request is checked against the caller's level in the project, on every route, socket and long poll. People outside a project get "not found". Removing someone takes effect at once, including pages they have open and agents that are waiting.
  • Every response is noindex, and robots.txt disallows everything.

Develop

cargo test --workspace      # server, CLI and protocol tests (in-process servers, real websockets)
cargo run -p semora-server

Crates: semora-proto (shared types), semora-server (Axum, SQLite, templates, embedded assets), semora-cli (the CLI, which installs the semora command). The server compiles in CHANGELOG.md (through the link crates/semora-server/CHANGELOG.md): a line that ends in <!-- whats-new --> also goes in the pop-up. Vendored browser libraries and themes are listed in docs/VENDOR.md. The design lives in docs/spec/semora-design-spec.md.

License

MIT. Vendored libraries keep their own licenses (see crates/semora-server/assets/vendor/LICENSES.txt and themes/LICENSES.txt).