# 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.
```sh
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
```sh
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
```sh
cargo install semora-server # or, from a checkout: cargo build --release -p semora-server
```
Cargo builds from source, so on Linux it needs a C compiler and linker as well as Rust: install `build-essential` on Debian or Ubuntu, `gcc` on Fedora, or `base-devel` on Arch.
Then follow the comments in [`deploy/semora.service`](deploy/semora.service); put [`deploy/Caddyfile`](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`](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). On Linux, install a C compiler first, as for the server: `build-essential` on Debian or Ubuntu. Then:
```sh
semora login --server https://semora.example.com # prints a link; allow it in any browser
# (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:
```sh
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)
```
`.semora` names the server by a random id, not by its address, so a public repository doesn't give the address away.
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`](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, in the top menu, 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
```sh
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`](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`](docs/VENDOR.md). The design lives in [`docs/spec/semora-design-spec.md`](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`).
The server's thumbnail renderer, takumi, includes jpeg-encoder, which is based in part on the work of the Independent JPEG Group. Copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding. The IJG-derived parts are provided "AS IS", with no warranty of any kind.
The server's About page (`/about`) shows Semora's licence and the licences of what it includes. The Rust crates' texts are in `crates/semora-server/licenses/rust-dependencies.txt`, written by `python3 scripts/rust-licenses.py`. Run it after any change to `Cargo.lock`; the Check build fails if the file is out of date.