# gunnar-ui

<img src="crates/gunnar-ui-web/assets/gunnar.webp" alt="gunnar" width="320">

The gunnar console: a passkey-guarded web UI (Leptos, WebAssembly) and its
backend-for-frontend, which speaks sshsig-signed gRPC to a gunnar appliance.

* Anonymous browsing of public repositories.
* Mirrors from GitHub, Codeberg, Forgejo and GitLab.
* Accounts, keys, groups and grants.
* Usage and paid signup through a monetize server, compiled in by the `metered` feature; the open build has no money code.
* The exact same code is hosted at https://gunnar.rs

## Building

```
git clone ssh://git.gunnar.rs:2222/nordisk/gunnar-ui
cd gunnar-ui
cargo build --release -p gunnar-ui-server
```

The console itself is a WebAssembly bundle, built by trunk, which is required:

```
cd crates/gunnar-ui-web
trunk build --release
```

This page documents **gunnar-ui 1.5.0** — the version `gunnar-ui-server
--version` prints.

License: AGPL-3.0-only, © 2026 Vetra Automation AB and Ignalina ApS (Rickard Ernst Björn Lundin). Commercial
terms: `COMMERCIAL-LICENSING.md`.

## Routes

The console's pages, routed in the browser. A visitor has no session; signed in
is any passkey session; admin is the role a session carries.

| page | who may open it |
|---|---|
| `/repos` | visitor |
| `/repo/{namespace}/{name}` | visitor |
| `/repo/{namespace}/{name}/files` · `/graph` · `/diff` · `/blame` · `/search` · `/chain` | visitor |
| `/repo/{namespace}/{name}/pulls` · `/pulls/{n}` · `/issues` · `/issues/{n}` | visitor |
| `/` | signed in |
| `/storage` · `/usage` · `/usage/{namespace}` · `/events` | signed in |
| `/account` | signed in |
| `/access` | signed in; every mutation on it is admin |
| `/mirrors` · `/mirrors/import` | signed in; every mutation on it is admin |

The server's own routes, read off `serve.rs`. Everything under `/api/` that is
not listed as a visitor's is refused without a session; the role floor is
decided by method and prefix in one place (`auth::middleware::role_floor`).

| route group | who may open it |
|---|---|
| `/health` · `/login` · `/login/repos` · `/auth/status` · `/auth/logout` | visitor |
| `/auth/passkey/login/*` · `/auth/passkey/register/*` | visitor |
| `/welcome` · `/auth/signup/status` (a `metered` build that sells) | visitor |
| `/signup` — the ONE sign-up page: `?plan=private` a new private Gunnar, `?plan=10gb` an account here (or, signed in, growing it); `/private` redirects to it · `/auth/private/ladder` · `/auth/private/{quote,start,status}` · `/auth/signup/{ladder,name,start,rename}` (a `metered` build that sells) | visitor; a signed-in buyer's order is theirs (the session's name, locked) |
| `GET /api/repos` | visitor |
| `GET /api/repos/{namespace}/{name}` and its `refs` · `head` · `log` · `browse` · `diff` · `blame` · `search` · `object` · `chain` leaves | visitor |
| every other `GET`/`HEAD` under `/api/`, including `/api/events` | signed in |
| `/api/account/keys` — a person's own keys | signed in |
| every other mutation under `/api/` | signed in, operator |
| `/api/principals` · `/api/keys` · `/api/grants` · `/api/namespaces` · `/api/accounts` · `/api/forges` · `/api/mirrors`, mutating | admin |
| `/api/namespaces/{name}/increase` (a `metered` build holding a monetize bearer) | signed in to read, admin to spend |
| `/auth/passkey/invites` · `/auth/passkey/credentials` | signed in, a fresh ceremony to mint or revoke; gunnar decides who may invite whom and whose passkeys a person may see |
| `/auth/sessions/revoke-all` | admin |

## Passkeys, invites and sessions live in gunnar

The console keeps no passkey, no invite and no key of anybody's. gunnar
issues the WebAuthn challenge, verifies every login against the passkey it
stores on the person's principal (`Access.WebLogin`), and enrols a short-lived
session key the console minted for that login; everything the session does is
signed with that key, as the person, and gunnar's grants decide. Logging out
revokes the session key. Registration (`Access.WebRegister`) redeems an invite
gunnar minted (`Access.CreateInvite`, asked as the person who invites), or is
the operator's first passkey through the installer's break-glass token. A
passkey this console registered before 2026-10-04 moves to gunnar when the
console starts. The console's own store keeps cookie sessions, preferences and
caches only.

## Thanks

* Its inventors: Vetra Automation AB and Ignalina ApS (Danskjävlarna).
* Greg Johnston and Ben Wishovich — Leptos.
* Anthony Dodd and Jens Reimann — Trunk.

## Sponsors

None so far — we would love to have one. Contact gunnar@gunnar.rs.
