A set of tools for enabling people to run their own cloud infrastructures
  • Go 59.6%
  • Vue 12.1%
  • TypeScript 11%
  • Nix 8.8%
  • SCSS 8%
  • Other 0.5%
Find a file
2026-10-23 02:21:28 +00:00
auth Add registration and sign-in 2026-10-23 01:46:03 +00:00
config Add registration and sign-in 2026-10-23 01:46:03 +00:00
db Add registration and sign-in 2026-10-23 01:46:03 +00:00
web Add registration and sign-in 2026-10-23 01:46:03 +00:00
.gitignore Create initial UI pipeline 2026-10-23 00:46:54 +00:00
api_auth.go Add registration and sign-in 2026-10-23 01:46:03 +00:00
flake.lock Initial creation of the deployment target 2026-10-22 21:32:28 +00:00
flake.nix Update vendor hash 2026-10-23 02:21:28 +00:00
go.mod Add registration and sign-in 2026-10-23 01:46:03 +00:00
go.sum Add registration and sign-in 2026-10-23 01:46:03 +00:00
handlers.go Add registration and sign-in 2026-10-23 01:46:03 +00:00
main.go Add registration and sign-in 2026-10-23 01:46:03 +00:00
README.md Add registration and sign-in 2026-10-23 01:46:03 +00:00

sovr.cloud

Tools that make self-hosting ordinary.

sovr.cloud is a subscription service for people who want to run their own infrastructure without depending on a major cloud provider, and without needing an advanced computer science degree to get the basics right.

The premise: many of the things that make self-hosting genuinely hard are small in number, well understood, and tedious to assemble correctly. Each one is a project where the hard part is not the software — it is knowing which software, which configuration, and which of the dozen subtly wrong ways to wire it together. sovr.cloud packages those pieces so a person can point at a box in their closet and get working services out of it.

The tools

These are the problems the service is being built around. Each is a case where the correct solution is known but disproportionately difficult for an individual to discover, implement, and maintain alone.

  • Tunnels that survive ISP port blocks. Many residential connections cannot accept inbound traffic on the ports that matter, or are behind CGNAT entirely. A tunnel gives a self-hosted service a stable public path in without a business circuit or a static IP.

  • Working TLS everywhere, even inside the LAN. Public certificate authorities cannot issue for internal-only names, and a certificate issued for the public name is not valid for an internal address. The usual answer — a wildcard certificate deployed alongside split-horizon DNS — is a set of moving parts that must agree with each other exactly. When they do, a name like service.example.com resolves to the LAN address inside the network and the public address outside it, and the same TLS certificate is valid in both places. No browser warnings, no exceptions, no "just click through it".

  • The joining of the two. Internal names that work from outside the network, external names that keep working from inside it, and certificates that are trusted in both directions.

These are the hard edges. Anything that can be made routine should be routine.

What exists today

The site is a Go HTTP server with a PostgreSQL-backed account system. The product itself is still mostly skeleton, but the deployment model and the pieces below are real and exercised end to end:

  • Environment-only configuration. config.Parse reads the process environment into a Config. Missing or malformed values are reported together at startup, so one restart lists everything a deployment got wrong.
  • Schema ownership. The binary embeds its goose migrations and applies them on startup, so deploying a new build also brings the database to the matching schema.
  • Compile-checked queries. All SQL is built with go-jet from generated table/model packages in db/gen/sovr. Handwritten SQL strings are not used by application code. Changing a column breaks the query at compile time.
  • Users and sessions. A user_ table stores a username, email, and either a bcrypt password hash, a WebAuthn user handle, or both. Sign-up, sign-in, account display, and sign-out all run through the SPA and a JSON API.
  • Passkeys. Registration and assertion use WebAuthn; credentials live in webauthn_credential with the signature counter and backup flags. A user can sign up with a passkey instead of a password, and sign in with it later.
  • A compiled frontend bundle. web/ is a Vue 3 + TypeScript single page application built by Vite from SCSS. The build output is embedded into the Go binary and served from /, with hashed assets cached immutably.
  • NixOS deployment. The flake builds the binary and exports a hardened systemd unit.

Sessions currently live in the process's memory, so restarting the service signs everyone out. A durable session store is a later concern.

Layout

Path Purpose
main.go Server startup, shutdown, CLI commands, embedded bundle
handlers.go Routes, SPA serving, session JSON API
api_auth.go Signup, password login, passkey ceremonies, logout
config/config.go Environment parsing and validation
auth/password.go bcrypt hashing and verification
auth/passkey.go WebAuthn relying party and credential conversion
db/db.go Connection pool, embedded goose migrations, query helpers
db/migrations/*.sql The schema, owned by this application
db/query/public/*.go Typed queries written against generated Jet code
db/gen/sovr/ Generated Jet models and table builders (committed)
db/jet/main.go Regenerates db/gen/sovr from a live database
web/ Vue 3 + TypeScript SPA, Vite config, SCSS
web/src/views/ Home, signup, sign-in, and account pages
web/dist/ Built bundle; embedded into the Go binary, not committed
flake.nix Package build, dev shell, NixOS module

Configuration

Everything is an environment variable. There is no config file.

Variable Default Meaning
HOST 127.0.0.1 Address to bind
PORT 8080 TCP port to bind
POSTGRES_DSN (required) PostgreSQL connection string
COOKIE_SECURE true Mark the session cookie Secure
WEBAUTHN_RP_ID localhost Relying-party ID; production uses sovr.cloud
WEBAUTHN_RP_NAME sovr.cloud Name shown by the passkey manager
WEBAUTHN_RP_ORIGINS http://localhost:5173,http://localhost:8080,… Comma-separated origins allowed to run ceremonies

The WebAuthn defaults are for local development. A production deployment sets WEBAUTHN_RP_ID=sovr.cloud and WEBAUTHN_RP_ORIGINS=https://www.sovr.cloud; the NixOS module does this by default. Passkeys require a secure context: HTTPS, or localhost during development. A page served from 127.0.0.1 is not a valid origin for an RP ID of localhost.

The default bind address is loopback on purpose: TLS and public exposure are handled by a reverse proxy in front of the process, not by the process itself. Set COOKIE_SECURE=false only for local development over plain HTTP.

A Unix-domain-socket database deployment looks like:

POSTGRES_DSN='postgres:///sovr?host=/run/postgresql&sslmode=disable'

Add whatever user and password the socket's authentication method requires.

Running it

With Nix:

nix run

Or build the binary and run it directly:

nix build
POSTGRES_DSN='postgres:///sovr_dev?host=/run/postgresql&sslmode=disable' \
  COOKIE_SECURE=false ./result/bin/sovr

It applies migrations, then listens on 127.0.0.1:8080:

curl http://127.0.0.1:8080/           # the embedded Vue SPA
curl http://127.0.0.1:8080/signup     # sign up with a password or passkey
curl http://127.0.0.1:8080/login      # sign in

nix build compiles the SPA first and embeds the result, so the Nix-built binary is self-contained. A plain go build embeds whatever is currently in web/dist; build that directory first, or the root path answers 503 while the rest of the server still runs.

The sign-up page creates accounts. The CLI remains as a bootstrap path for an operator who needs a password account without the browser:

POSTGRES_DSN='postgres:///sovr_dev?host=/run/postgresql&sslmode=disable' \
  ./result/bin/sovr user add -username alice -password 'correct horse battery staple'

-password is visible in the process list while the command runs, so avoid running it on a shared host.

Development shell

nix develop

It provides Go, Node.js, pnpm, go-jet, goose, gopls, and PostgreSQL client/server tools.

Frontend

web/ is a Vue 3 single page application in TypeScript. Vite compiles it and SCSS compiles into CSS as part of the same build. Output goes to web/dist, which the Go binary embeds with //go:embed.

For UI development, run Vite and the Go server side by side:

# terminal 1
nix develop
POSTGRES_DSN='postgres:///sovr_dev?host=/run/postgresql&sslmode=disable' \
  COOKIE_SECURE=false go run .
# terminal 2
cd web
pnpm install
pnpm dev

Open http://localhost:5173. Vite serves the SPA with hot reload and proxies /api to the Go process on :8080. Use localhost, not 127.0.0.1, when testing passkeys: the default relying-party ID is localhost.

To produce the embedded bundle yourself:

cd web
pnpm install
pnpm build

The build type-checks with vue-tsc first, so a type error fails the build. Hashed files under web/dist/assets are served with a one-year immutable cache; index.html is served with no-cache so a new deployment is picked up on the next load. web/dist is not committed: nix build regenerates it, and web/dist/.gitkeep keeps the embed directory present in a fresh checkout.

Styles live in web/src/styles (_variables.scss for tokens, _mixins.scss for shared treatments, main.scss for the page). Add new partials with @use, not @import.

Database

The application owns its schema. Migrations live in db/migrations and are embedded into the binary; both the server and sovr user add apply pending migrations before doing anything else. Goose takes an advisory lock, so several instances starting together still apply each migration once.

To write a migration, add the next numbered file:

-- +goose Up
ALTER TABLE user_ ADD COLUMN display_name TEXT;

-- +goose Down
ALTER TABLE user_ DROP COLUMN display_name;

Apply migrations manually with goose (optional — the binary does it):

GOOSE_DRIVER=postgres \
GOOSE_DBSTRING="$POSTGRES_DSN" \
goose -dir db/migrations up

Generated queries

db/gen/sovr is generated from the live schema and committed. Do not edit it by hand.

POSTGRES_DSN='postgres:///sovr_dev?host=/run/postgresql&sslmode=disable' \
  go run ./db/jet

Run that after applying a migration that changes a table, then commit the regenerated files. Application queries use the generated columns, for example:

stmt := table.User.
    SELECT(table.User.AllColumns).
    WHERE(table.User.Username.EQ(postgres.String(username)))

The compiler rejects a column that does not exist on table.User. db.Ex accepts either the pool or a transaction, so the same query works in both.

Deployment

The flake exports nixosModules.default:

{
  inputs.sovr.url = "git+https://github.com/…/sovr-cloud";

  imports = [ inputs.sovr.nixosModules.default ];

  services.sovr = {
    enable = true;
    port = 8080;
    postgresDsn = "postgres:///sovr?host=/run/postgresql&sslmode=disable";
  };
}

The service listens on loopback and expects a reverse proxy such as Caddy to terminate TLS and forward to it. Firewall rules, DNS, and certificates belong to that proxy layer, not to this process. The package build compiles the Vue bundle and embeds it, so a deployed binary needs no Node.js at runtime. The module defaults WEBAUTHN_RP_ID to sovr.cloud and WEBAUTHN_RP_ORIGINS to https://www.sovr.cloud; change both together if the site moves. The unit may use an external database; adjust postgresDsn, or remove postgresql.service from its after list for your packaging if you do.

Authentication

The SPA talks to a small JSON API:

  • POST /api/signup — username, email, password; creates the account and starts a session.
  • POST /api/signup/passkey/begin and /finish — the WebAuthn registration ceremony. The user row and credential are written in one transaction after the authenticator succeeds.
  • POST /api/login — username and password.
  • POST /api/login/passkey/begin and /finish — the WebAuthn assertion. The username is required, so the flow is not username-less; the assertion is validated against that account's stored credentials.
  • GET /api/session — {authenticated, username}.
  • POST /api/logout — destroys the session.

/account shows the signed-in username and a sign-out button. Unknown or deleted users are signed out. Authentication rotates the session token.

Passkeys store the credential ID, public key, attestation metadata, signature counter, and backup flags in webauthn_credential. The signature counter and flags are updated after each successful assertion; a clone warning is persisted. A username may have a password, a passkey, or both.

Every mutating API call requires Content-Type: application/json. The session cookie is HttpOnly, SameSite=Lax, and Secure by default.