# 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
```
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)):
| `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` | Use `X-Forwarded-For` for client IPs |
| `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`).
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.
| `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:
```sh
semora login --server https://semora.example.com # or set SEMORA_URL + SEMORA_API_KEY
semora push notes.md
semora wait <id>
```
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).
## 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 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). 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`).