Note
This project is developed with the assistance of AI code generation tools. AI-generated code is reviewed and tested before being merged, but if you encounter any issues, please feel free to open an issue or submit a pull request.
2026-02-25_07-57-39_x265.mp4
Web admin dashboard for Continuwuity Matrix homeservers.
GitHub · GitHub Container Registry · Docker Hub
Continuwuity only supports admin commands via messages in a special admin room. uwu-admin provides a proper web UI by connecting to the homeserver as a bot account, sending admin commands to the admin room, and displaying the results.
Caution
uwu-admin has full admin control over your homeserver. It is intended for private or internal use and should not be exposed to the public internet. If you do need remote access, place it behind a TLS-terminating reverse proxy with additional access controls (e.g. VPN, IP allowlist, HTTP basic auth). See Production Deployment for more details.
- Quick Start (Docker)
- Architecture
- Setup
- Development
- Production Deployment
- Container Deployment
- Session Persistence
- Shared
- Scripts
- Testing
- CI
- Releasing
Browser (Vue) → uwu-admin-api (Rust/axum :3001) → Matrix Homeserver
↕
Admin Room
(bot sends !admin commands,
reads server responses)
uwu-admin versions follow Continuwuity versions to indicate compatibility. Set the tag to your Continuwuity server version (e.g. v0.5.5 or 0.5.5), which floats to the latest uwu-admin patch for that version. Use an exact tag like v0.5.5-2 to pin a specific build. latest always points to the most recent release regardless of Continuwuity version — it may move to a newer version that is incompatible with your homeserver. Use latest with caution.
docker run -d -p 8080:8080 \
-e JWT_SECRET=$(openssl rand -hex 32) \
-e ENCRYPTION_KEY=$(openssl rand -hex 32) \
-v uwu-data:/data \
pnrxa/uwu-admin:v0.5.6Open http://localhost:8080, create an admin account, and add your homeserver.
- Rust (2024 edition)
- Node.js 22+
- A Continuwuity homeserver with an admin bot account and admin room
Copy the example environment file and generate secrets:
cp api/.env.example api/.envGenerate values for JWT_SECRET and ENCRYPTION_KEY:
openssl rand -hex 32Paste a unique value into each field in api/.env. Both JWT_SECRET and ENCRYPTION_KEY are required — the server will refuse to start without them.
| Variable | Description | Default |
|---|---|---|
JWT_SECRET |
32-byte hex key for signing auth tokens | required |
ENCRYPTION_KEY |
32-byte hex key for encrypting access tokens at rest | required |
ADMIN_USERNAME |
Seed an admin account on first start (skips setup screen) | none |
ADMIN_PASSWORD |
Password for the seeded admin account | none |
DATABASE_URL |
SQLite connection string | sqlite:uwu-admin.db?mode=rwc |
API_LISTEN |
API bind address | 127.0.0.1:3001 |
CORS_ORIGIN |
Allowed CORS origin (enables CSRF protection and credentialed cookies) | none |
COOKIE_SECURE |
Set the Secure flag on refresh token cookies |
true |
ALLOW_PRIVATE_HOMESERVERS |
Allow adding homeservers that resolve to private/loopback IPs (needed when the homeserver is on the same host or network) | false |
Start the API and web frontend in two terminals:
# Terminal 1 — API
cd api
cargo run # Starts on :3001# Terminal 2 — Web
cd web
npm install
npm run dev # Vite dev server on :5173, proxies /api → :3001Open http://localhost:5173, create an admin account on first launch, then add a homeserver by entering its URL, bot credentials, and admin room ID or alias. Room fields accept either a room ID (!abc:example.com) or a room alias (#admins:example.com) — aliases are resolved automatically on connect.
Alternatively, use the quadlet-dev.sh script to run everything in a Podman container.
# API
cd api
cargo build --release
# Web
cd web
npm run build # Output in dist/If uwu-admin is only accessible on a trusted local network (e.g. LAN, tailnet, Docker bridge), the defaults work as-is. Recommended settings:
| Variable | Recommendation |
|---|---|
CORS_ORIGIN |
Can be left unset. All access is same-origin and the network is trusted. |
COOKIE_SECURE |
Set to false — you are likely serving over plain HTTP. |
ALLOW_PRIVATE_HOMESERVERS |
Set to true if the homeserver is on the same host or network. |
If you're serving uwu-admin over HTTPS on a local network (e.g. using a self-signed certificate or a private CA), keep COOKIE_SECURE at the default (true) since cookies will be sent over TLS.
| Variable | Recommendation |
|---|---|
CORS_ORIGIN |
Set to your internal URL (e.g. https://admin.local:8080) if accessing from a different origin. Can be left unset for same-origin access. |
COOKIE_SECURE |
Leave at the default (true). |
ALLOW_PRIVATE_HOMESERVERS |
Set to true if the homeserver is on the same host or network. |
If you need remote access, place uwu-admin behind a TLS-terminating reverse proxy (e.g. Caddy, nginx, Traefik) with additional access controls such as a VPN, IP allowlist, or HTTP basic auth. The application serves plain HTTP and should not be exposed directly to the internet — without TLS, credentials, tokens, and cookies are sent in plaintext.
| Variable | Recommendation |
|---|---|
CORS_ORIGIN |
Set to your external URL (e.g. https://admin.example.com). Enables server-side CSRF protection as a defense-in-depth layer on top of SameSite=Strict cookies. |
COOKIE_SECURE |
Leave at the default (true) when behind TLS. |
Container images are available on GitHub Container Registry and Docker Hub.
See containers/ for Docker and Podman Quadlet deployment options.
Both require JWT_SECRET and ENCRYPTION_KEY to be set as environment variables — see the example compose file and quadlet config.
The API stores server connections in a local SQLite database (uwu-admin.db by default). Access tokens are encrypted at rest using ChaCha20-Poly1305.
On startup the API restores saved connections, validates each token against its homeserver, and removes any stale sessions automatically.
Helper scripts live in the scripts/ directory.
Regenerates shared/command-tree.json from the uwu-admin fork of continuwuity. Clones the fork into ../continuwuity if it doesn't already exist, fetches upstream (including tags), rebases on upstream/main, and runs cargo xtask generate-command-tree. When a tag is specified, the script checks out that tag for generation then returns to main — the repo always ends up on main. Build prerequisites are the same as for continuwuity itself (Rust, C/C++ compiler, libclang, liburing, make).
./scripts/update-command-tree.sh [-w] [-p] [tag]| Option | Description |
|---|---|
tag |
Generate from a specific upstream version tag (e.g. v0.5.0). Omit to use main |
-w |
Push tags and commits to the fork (for maintainers). Without this flag, nothing is pushed |
-p |
Create a PR with the updated command tree (requires a version tag). Uses gh CLI |
Examples:
./scripts/update-command-tree.sh # Fetch + rebase on upstream main, generate command tree
./scripts/update-command-tree.sh v0.5.0 # Fetch + rebase, generate from a specific tag, return to main
./scripts/update-command-tree.sh -w # Same as above + push tags and main to fork
./scripts/update-command-tree.sh -w v0.5.0 # Same as above with a specific tag
./scripts/update-command-tree.sh -p v0.5.6 # Generate from a specific tag + open a PR with the changes
./scripts/update-command-tree.sh -w -p v0.5.6 # Same as above + push tags and main to forkRuns the full test suite against a fresh Quadlet build. Rebuilds the container image, wipes the database, then runs frontend unit tests, backend tests (unit + integration), and E2E tests against the container.
./scripts/test.shDevelopment helper for managing the uwu-admin Podman Quadlet. Builds the container image, installs quadlet unit files to ~/.config/containers/systemd/, and manages the systemd user service.
./scripts/quadlet-dev.sh <command>| Command | Description |
|---|---|
build |
Build the container image |
install |
Copy quadlet files and reload systemd |
start |
Build image (if needed), install quadlets (if needed), and start the service |
stop |
Stop the service |
rebuild |
Stop, rebuild image, and restart |
restart |
Restart the service without rebuilding |
reset-db |
Stop, wipe the database volume, and restart with a fresh DB |
test |
Rebuild image, wipe DB, and start (fresh environment for E2E tests) |
status |
Show service status and recent logs |
logs |
Follow the service journal logs |
destroy |
Stop service, remove quadlet files, volume, and image |
The shared/ directory contains data shared between the API and web frontend.
The file shared/command-tree.json describes every admin command (names, descriptions, argument types). It powers the console's autocomplete, and input validation on both the API and frontend. It is auto-generated from the continuwuity fork source code via update-command-tree.sh.
cd api
cargo test # Unit tests (no server needed)
cargo test -- --skip integration # Same, explicitly skipping integration tests
cargo test # Full suite including integration tests (needs server)Unit tests cover auth, crypto, input validation, command parsing, and response handling.
Integration tests require a running Continuwuity instance. Add the following to api/.env:
| Variable | Description |
|---|---|
TEST_HOMESERVER |
Homeserver URL (e.g. https://matrix.example.com) |
TEST_USERNAME |
Bot username |
TEST_PASSWORD |
Bot password |
TEST_ROOM_ID |
Admin room ID or alias |
The integration suite includes an exhaustive command tree test that walks every leaf command in shared/command-tree.json, sends it to the server with appropriate test arguments (matching each arg's type — user IDs, room IDs, event IDs, numbers, etc.), and verifies the command parses successfully. This catches any drift between the generated command tree and the actual server command definitions.
cd web
npm test # Run all tests once
npm run test:watch # Run in watch mode during developmentUses Vitest with jsdom. Tests cover:
- Lib utilities — response parser (all 4 output branches), HTML sanitization, query key factories, Tailwind class merging
- Composables — command autocomplete suggestions, argument hints, input validation
- API layer — token management, auth header injection, error handling, 401 refresh flow, request timeouts
- Pinia stores — auth (login/register/logout), command execution and history, server connection management
End-to-end tests use Playwright with Chromium and run against a real API + Vite dev server stack.
cd web
npx playwright install --with-deps chromium
npx playwright testThe suite requires a running uwu-admin-api on :3001 and the same test variables used by the backend integration tests. Copy the web example env and fill in values:
cp web/.env.example web/.env| Variable | Description | Default |
|---|---|---|
E2E_BASE_URL |
Base URL to test against (skip to use Vite dev server) | http://localhost:5173 |
TEST_HOMESERVER |
Homeserver URL (e.g. https://matrix.example.com) |
— |
TEST_USERNAME |
Bot username | — |
TEST_PASSWORD |
Bot password | — |
TEST_ROOM_ID |
Admin room ID or alias | — |
A global setup step handles account creation (or login) and adds a test server, saving session tokens so individual specs start authenticated. Tests run serially (workers: 1) because they share server state.
Specs cover:
- Auth — registration, login, logout flows
- Server management — adding, switching, removing servers
- Console — sending admin commands, viewing responses
- User actions — user list table actions (profile, ban, deactivate, etc.)
- Room actions — room list table actions (details, aliases, bans, etc.)
- Theme toggle — light/dark mode switching
- Copy to clipboard — copying values from the UI
You can also point the tests at an already-running instance (e.g. a container) by setting E2E_BASE_URL:
E2E_BASE_URL=http://localhost:8080 npx playwright testWhen testing against a Podman Quadlet, use http://127.0.0.1:8080 instead of localhost — Playwright resolves localhost to [::1] (IPv6), which the container port binding may not listen on.
A GitHub Actions workflow (.github/workflows/test.yml) runs on every push to main and on pull requests.
| Job | Runner | Trigger | What it does |
|---|---|---|---|
| frontend | ubuntu-latest |
push + PR | npm ci, type-check, Vitest unit tests |
| backend | self-hosted (push) / ubuntu-latest (PR) |
push + PR | cargo test — unit tests always, integration tests on push (secrets available) |
| e2e | self-hosted | push only | Builds the API, starts it in the background, installs Playwright + Chromium, runs the full e2e suite, uploads the HTML report as an artifact |
| update-command-tree | self-hosted | manual (workflow_dispatch) |
Clones the continuwuity fork, generates shared/command-tree.json for the given version tag, and opens a PR |
The e2e job only runs on pushes to main (not PRs) because it needs repository secrets and a self-hosted runner with access to a live Continuwuity instance. The Playwright HTML report is uploaded as a build artifact and retained for 14 days.
The update-command-tree workflow is triggered manually from the Actions tab. It clones the public continuwuity fork over HTTPS — no additional secrets are required beyond the default GITHUB_TOKEN.
uwu-admin versions track Continuwuity versions — v0.5.5-0 of uwu-admin is the initial release compatible with Continuwuity v0.5.5. When uwu-admin needs additional releases for the same Continuwuity version (bug fixes, UI improvements, etc.), we increment the suffix: v0.5.5-1, v0.5.5-2, and so on.
| Git tag | Meaning |
|---|---|
v0.5.5-0 |
Initial release for Continuwuity v0.5.5 |
v0.5.5-1 |
First uwu-admin patch for the same Continuwuity version |
v0.5.5-2 |
Second uwu-admin patch |
v0.6.0-0 |
Tracks Continuwuity v0.6.0 |
Docker images get three tags: the exact version (v0.5.5-2), a floating base version (v0.5.5) that always points to the latest -N release, and latest.
| Docker tag | Behaviour |
|---|---|
v0.5.5-2 |
Pinned to one specific build |
v0.5.5 |
Floating — always the latest -N for that Continuwuity version |
latest |
Floating — always the most recent release overall |
Updates version references in package.json, Cargo.toml, and README.md, commits the changes, creates a git tag, and pushes everything. Shows a diff and prompts for confirmation before making any changes.
./scripts/release.sh <version-tag>Example:
./scripts/release.sh v0.5.6-0
./scripts/release.sh v0.5.6-1Pushing a v* tag triggers the release workflow (.github/workflows/release.yml), which builds the container image, pushes it to ghcr.io and Docker Hub, and creates a GitHub Release with auto-generated notes.