A simple blogging website
  • Rust 98.8%
  • JavaScript 1%
  • CSS 0.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
renovate-bot f6ee235b74
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
ci/woodpecker/push/clippy Pipeline was successful
ci/woodpecker/push/test Pipeline was successful
chore(deps): update rust crate cookie to v0.18.2
2026-08-16 00:07:40 +00:00
.sqlx feat: Media gains the domain ops its Library surface reads and writes 2026-08-12 12:21:41 -06:00
.woodpecker ci: remove image job, fix later 2026-08-09 13:03:10 -06:00
assets feat: the post editor attaches a Media picked from the Library 2026-08-12 13:57:05 -06:00
crates fix: a post editor submission survives the fields nobody filled in 2026-08-12 15:57:51 -06:00
docs docs: remove nonexistent worker crate from domain.md crate list 2026-08-11 21:52:48 -06:00
migrations feat: Media captures its upload filename as a required display label 2026-08-12 00:25:59 -06:00
.editorconfig feat: Add Postgres database 2026-06-06 10:33:13 -06:00
.gitignore docs: defer media processing to a background job (#165) 2026-08-10 00:09:02 +00:00
ARCHITECTURE.md docs: add Design Goals, Invariants, and Ports to ARCHITECTURE 2026-08-08 17:24:51 -06:00
bacon.toml refactor: replace tailwind with plain css 2026-06-05 11:55:30 -06:00
Cargo.lock chore(deps): update rust crate cookie to v0.18.2 2026-08-16 00:07:40 +00:00
Cargo.toml docs: defer media processing to a background job (#165) 2026-08-10 00:09:02 +00:00
CLAUDE.md docs: defer media processing to a background job (#165) 2026-08-10 00:09:02 +00:00
compose.yml refactor: replace Postgres with SQLite 2026-08-03 12:51:34 -06:00
Containerfile refactor: replace Postgres with SQLite 2026-08-03 12:51:34 -06:00
CONTEXT.md refactor: listing a Media's referencing Posts is owner-scoped 2026-08-12 12:31:26 -06:00
LICENSE.md docs: Update README, add LICENSE 2026-08-02 20:34:38 -06:00
mise.toml fix: a post editor submission survives the fields nobody filled in 2026-08-12 15:57:51 -06:00
README.md fix: make OTLP span export opt-in 2026-08-10 19:30:20 -06:00
renovate.json Update renovate.json 2026-05-27 16:04:07 +00:00
tarpaulin.toml build(coverage): Pin tarpaulin to the LLVM engine via tarpaulin.toml 2026-07-05 13:53:49 -06:00

Vernier

status-badge standard-readme compliant

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/.