- Rust 98.8%
- JavaScript 1%
- CSS 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .sqlx | ||
| .woodpecker | ||
| assets | ||
| crates | ||
| docs | ||
| migrations | ||
| .editorconfig | ||
| .gitignore | ||
| ARCHITECTURE.md | ||
| bacon.toml | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CLAUDE.md | ||
| compose.yml | ||
| Containerfile | ||
| CONTEXT.md | ||
| LICENSE.md | ||
| mise.toml | ||
| README.md | ||
| renovate.json | ||
| tarpaulin.toml | ||
Vernier
A self-hostable blogging platform with IndieWeb support.
Vernier is a small multi-user blog host built around IndieWeb features, like Webmentions, Micropub, and an IndieAuth provider. Accounts are invite-only and sign-in is by passkey — there are no passwords.
Table of Contents
Security
Passkeys (WebAuthn) are the sole login factor; there is no password to phish or
leak. WebAuthn requires a secure context, and Vernier sets Secure session
cookies, so in production it must be served over HTTPS — terminate TLS at a
reverse proxy in front of it. Accounts are operator-provisioned by invite;
users cannot sign themselves up.
Found a vulnerability? Please contact the maintainer privately via her contact page rather than opening a public issue.
Install
Dependencies
Vernier needs, at runtime:
- SQLite — the datastore and the background-job queue, a single local file (no separate database service). It is created and migrated automatically on startup.
- An SMTP server — used to send email-address confirmations and passkey-recovery links.
To build from source you also need a Rust toolchain (this project uses
mise to manage it).
From source
git clone https://code.cosmicrose.dev/rosa/vernier.git
cd vernier
cargo install --path crates/server
This installs the vernier-server binary.
Container
A container image is built from the Containerfile and can be
run with Podman or Docker. A compose.yml is included for local
development; it brings up MailCrab (a catch-all SMTP inbox) and a Jaeger tracing
collector (SQLite needs no service of its own).
docker compose up -d
Usage
Vernier is configured entirely through environment variables and takes no arguments. Point it at a database and an SMTP server, give it its public URL, and run it:
VERNIER_BASE_URL=https://blog.example.com \
VERNIER_DATABASE_URL=sqlite:///var/lib/vernier/vernier.db \
VERNIER_SMTP_HOST=smtp.example.com \
VERNIER_SMTP_SENDER="Vernier <[email protected]>" \
vernier-server
The server binds to 0.0.0.0:3000 by default, so it should then be reachable at
http://localhost:3000 (put a TLS-terminating reverse proxy in front of it for
any real deployment — see Security).
Configuration
All settings are read from VERNIER_-prefixed environment variables.
| Variable | Required | Default | Description |
|---|---|---|---|
VERNIER_BASE_URL |
yes | — | Public origin the instance is served from, e.g. https://blog.example.com. Used to build permalinks and IndieAuth identities. |
VERNIER_DATABASE_URL |
yes | — | SQLite connection string, e.g. sqlite:///var/lib/vernier/vernier.db (the file is created if missing). |
VERNIER_SMTP_HOST |
yes | — | SMTP server hostname. |
VERNIER_SMTP_SENDER |
yes | — | From address for outgoing mail, e.g. Vernier <[email protected]>. |
VERNIER_BIND_ADDRESS |
no | 0.0.0.0:3000 |
Address and port to listen on. |
VERNIER_SMTP_PORT |
no | lettre default | SMTP server port. |
VERNIER_SMTP_USERNAME |
no | — | SMTP username, if the relay requires auth. |
VERNIER_SMTP_PASSWORD |
no | — | SMTP password. |
VERNIER_SMTP_TLS |
no | true |
Whether to connect to the SMTP server over TLS. |
VERNIER_ENABLE_BACKGROUND_JOBS |
no | true |
Run the in-process job workers (Webmentions, mail). |
VERNIER_LOG |
no | — | Tracing filter, e.g. info or vernier=debug. |
Trace export is opt-in: set the standard OTEL_EXPORTER_OTLP_ENDPOINT (or
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) variable to ship spans to an OTLP
collector such as Jaeger. When neither is set, Vernier logs to stdout only and
needs no collector running.
First-run setup
A fresh instance has no accounts. On startup, when no Operator exists yet, Vernier logs a one-time setup URL:
WARN No operators exist yet. Open this one-time setup URL to create the first operator. setup_url=https://blog.example.com/setup?token=…
Open that URL, choose a username, and enroll your first passkey. That makes you the instance's first Operator. The setup token stops working the moment the first Operator is established.
Inviting other users
Operators invite additional people from the Invites page (/invites). Each
invite is a single-use link you send to the invitee through any channel; opening
it lets them pick a username and enrol a passkey to create their (non-Operator)
account. Outstanding invites can be revoked before they are redeemed.
Once you have an account you can post from the web editor (/new/post) or from
any Micropub client pointed at your Profile URL.
Backup
The whole instance — accounts, posts, and media — lives in the single SQLite
database file, so a backup is one file. Take it with SQLite's online backup, not
cp: the running server keeps the database in WAL mode, where a plain file copy
can omit committed transactions still held in the -wal sidecar, or capture a
torn file mid-checkpoint. This command snapshots the live database consistently
and is safe to run from cron:
sqlite3 /var/lib/vernier/vernier.db ".backup '/backups/vernier.db'"
To restore, stop the server and copy the snapshot back into place (a plain copy is fine here, because nothing is writing):
cp /backups/vernier.db /var/lib/vernier/vernier.db
Maintainer
This project is maintained by Rosa Richter. For ways to contact her, see her contact page.
Contributing
Questions and contributions are absolutely welcome! Please create an issue for bugs, feature requests, or questions.
This project follows the standard-readme specification.
License
AGPL-3.0-or-later © 2026 Rosa Richter
This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License along with this program. If not, see https://www.gnu.org/licenses/.