ᚢ The strength rune · a rune in Yggdrasil Panel

Build strength,
one rune at a time.

A training app for iPhone and web, built to be used one-handed with sweaty fingers. A set is logged in under two seconds — including in the basement gym where there is no signal at all.

<2sper set
100%offline-ready
168tests
0setup
Starter programme – Full body
Exercise 1/7
Leg press (machine)
Target: 10–12 reps
Try 62.5 kg next time
kg
60
+
reps
12
+
✓ Log set
1.  60 kg × 12 ⚡ NEW RECORD
2.  60 kg × 11
🕐 Rest timer 1:28 Done
Train
Stats
Valhalla
Me

⚠️ Early development & built with Claude Code. Uruz is provided as-is, with no warranty and no liability whatsoever — you use it entirely at your own risk. Free software under AGPL-3.0.

What it looks like

Real screens, real numbers.

The images below were captured from the running app with twelve weeks of demo training in the database — not drawn, not touched up, and not anyone's real training log.

The Train screen with today's workout and a list of programmes
Train — today's workout, or pick freely
Set logging with weight, reps and a record being celebrated
Log a set — pre-filled, one tap
Statistics with tonnage, consistency score and fun insights
Stats — tonnage, progress, insights
Valhalla with the leaderboard, milestones and runes
Valhalla — leaderboard, runes and milestones
Kvasir with the weekly analysis and suggested questions
Kvasir — the weekly analysis and "ask anything"
The exercise library with drawings and filters
Library — exercises with steps and cues
What it does

Dry, factual training logging — with a soul on top.

The core is fast logging and honest statistics. The Norse layer is seasoning, never a gimmick: one tap turns it all the way down to plain numbers.

Under two seconds per set

Weight and reps are pre-filled from last time, so usually you just confirm. The rest timer starts itself, and records are celebrated the moment they happen.

Works without a signal

Every set is saved locally first and syncs by itself once the signal returns. No spinner, no lost sets, no excuses.

Statistics that mean something

Progress per exercise, tonnage, an attendance calendar and muscle balance — plus fun insights that keep you coming back.

Kvasir, the coach

A weekly analysis based on your own numbers, a chat you can ask for advice, a programme laid out from four questions — and the ability to adapt the training when something hurts.

Valhal

Runes to unlock, ranks from Thrall to Einherjar, milestones and a friendly rivalry between everyone in the hall.

🕊

Huginn & Muninn

The ravens remember for you: reminders on your training days, praise after a good session, and a gentle nudge if you have been away. Never shame.

The core

Built for the basement under the gym

Most training apps stall when the signal does. Uruz does not. Every set gets its id in the browser, goes into a queue in IndexedDB and appears immediately — long before the server has heard about it.

When the network returns, the queue drains itself. Because the id is made up front, the same queue can be replayed twice without duplicating a single set or awarding the same record again.

  • Optimistic UI — the set is there before it is sent
  • The queue survives restarts, reloads and flight mode
  • Order is preserved: the queue stops at the first failure
  • Verified by pulling the network mid-workout
⌁ Offline — saved locally, syncs later
Starter programme – Full body
Exercise 3/7
Seated row
Last: 40 kg × 12, 11
kg
40
+
reps
12
+
✓ Log set
1.  40 kg × 12queued
2.  40 kg × 12queued
Train
Stats
Valhalla
Me
Stats
Overview
22
Workouts
118t
Tonnage
13
Week streak
Consistency score 81%
Fun insights
🚌
You have lifted 117,625 kg
That's 9.8 × city bus
📈
On this day
Leg press: 47.5 → 55 kg (+16%)
😅
Most skipped exercise
Romanian deadlift — 4×
Train
Stats
Valhalla
Me
Stats

Both the dry numbers and the ones that get you moving

Progress graphs per exercise with estimated 1RM, total tonnage, an attendance calendar, and a muscle balance that reveals whether you push far more than you pull.

And then the other layer: your tonnage translated into something you can picture, what you lifted on this day four weeks ago, and which exercise you tend to skip.

  • Every number on the page is covered by a unit test
  • Consistency counts for more than being strong
  • Export to CSV and JSON — your data is yours
ᛘ Kvasir

Tell it where it hurts. It adjusts the training.

Kvasir is not a chatbot glued on the side. He knows your numbers, your programme and your aches — and he proposes concrete changes that you approve. He can lay the programme out in the first place, too: four questions, and a plan built only from exercises your library actually has.

My right shoulder hurts when I press overhead
ᛘ Kvasir It is important to listen to your body. Since the shoulder press hurts, I have swapped it out to avoid irritating it further. I chose the triceps pushdown so you still work the arms — just without loading overhead.
Swap
Shoulder press (machine) → Triceps pushdown (cable) Avoids the painful overhead movement.
If it persists, have it looked at by a doctor or a physiotherapist. Kvasir is not a doctor.
I only have 20 minutes today and want to prioritise back
ᛘ Kvasir · four days later I have shortened the workout so you can finish in 20 minutes with the focus on your back. I have removed the shoulder press because of your pain; hvis the pain continues, you should see a doctor or a physiotherapist.
Adjust
Seated row · Lat pulldown Prioritised, so the back gets most of the time.
Remove
Shoulder press · Chest press Spares the shoulder and keeps the workout inside 20 minutes.

Notice the second message: it does not mention the shoulder at all. Kvasir remembers it on his own — and keeps allowing for it until you say it is fine again.

🔒

Suggests — does not decide

Nothing changes by itself. You see every change with its reasoning and approve it. The result is saved as a copy, so your original programme survives.

🎯

Cannot invent exercises

The model picks from a list of real exercise ids, and every id is validated against the library afterwards. Unknown ones are discarded — never guessed.

Knows its place

No diet advice, no comments about bodies or weight, no promises of healing. Pain is always referred to a doctor or a physiotherapist.

Your model, your choice

Not tied to one vendor

Kvasir talks to Anthropic, OpenAI, Google, Ollama — or anything that speaks the OpenAI protocol, including a model running on your own server at home. With no API key at all, if it does not need one.

Switching provider is three lines in a config file. No code changes.

  • Reasoning models are handled — and made fast
  • Only anonymised numbers leave the server
  • With no model, the app coaches on rules instead
.env.local
# Your own model on your own network — no key
AI_PROVIDER=custom
AI_BASE_URL=http://your-server.lan:8080/v1
AI_MODEL=gemma-4-26b-qat

# …or a cloud service
AI_PROVIDER=anthropic
AI_MODEL=claude-sonnet-5
AI_API_KEY=sk-ant-…
Valhal
Leaderboard
🏆
Traveller of the week
Kristian — 2 workouts
1 K
Kristian (you)
Jarl
🔥 13 22
2 I
Ib
Karl
🔥 9 18
Runes & badges · 4 of 8
Train
Stats
Valhalla
Me
Valhal

Friendly rivalry — not a competition

Runes unlock as you show up. Ranks run from Thrall to Einherjar, and attendance weighs heaviest: the app rewards turning up, not being the strongest.

The leaderboard shares totals only — never anyone's raw training log. A private profile still takes part in the rivalry.

  • Badges are recomputed from history, never from events
  • Milestones in things you can picture — a car, a ship
  • A streak is not broken by a week that is not over yet
Running it

A rune in Yggdrasil Panel

Uruz is first and foremost built to run as a Rune in Yggdrasil Panel — the panel that turns a server into something you can point and click at. A rune is an app the panel can sow: you pick it, fill in a couple of fields, and it is running with its own subdomain, its own data directory and its own backup.

The app works out the address it is being reached on, so sign-in links and Face ID work behind the panel's proxy without anyone typing a domain anywhere. Everything else — AI, email, notifications — are fields you can leave blank.

  • Runes → Carve a rune → import yggdrasil/uruz.yaml
  • Sow a server, give it a subdomain, and it runs
  • Backup captures the whole database, WAL files and all
without the panel
# One call, and it runs
docker run -d \
  -p 3000:3000 \
  -v uruz-data:/data \
  ghcr.io/kristianwind/uruz:latest

# Or entirely locally
npm install && npm run setup
npm run dev
Under the bonnet

The domain does not know a database exists.

Everything that can be computed is a pure function with no I/O. That is why every number in the app is unit-tested, and why Uruz can coach sensibly with no AI at all.

UI React Server + Client Components

Pages and components. No domain logic — they only call into /lib.

Domain pure functions · 168 tests

1RM and progression · statistics · badges and rank · coach insights and adaptation. No fetch, no database, no React.

Data one surface, two backends

node:sqlite locally (zero setup) · Supabase/Postgres with Row Level Security in production. The UI never sees a database driver.

Services replaceable

AI providers behind one chat() · passkeys and sign-in links behind one auth surface · web push with email as the fallback · an offline queue in IndexedDB.

LayerChoiceWhy
FrameworkNext.js 15 · TypeScript · React 19One codebase for web and PWA; can be mounted under a larger panel
Databasenode:sqlite → Supabase/PostgresZero setup locally, RLS in production
PrivacyRow Level SecurityAccess is enforced in the database, not only in the UI
LoginPasskeys · password · sign-in linkFace ID where it works, a password where it does not
OfflineIndexedDB queue + service workeriOS lacks Background Sync — the app drives the queue
AIOwn provider surfaceAny provider, including a key-free model on your own network
LanguageDanish · EnglishThe exercise content too, not just the buttons
Get started

Three commands. No accounts, no keys.

Uruz runs locally on a built-in database. You do not have to install Postgres, create a Supabase account or obtain an API key to see the app running.

terminal
# 1 — fetch dependencies
npm install

# 2 — prepare database and content
npm run setup

# 3 — start the app
npm run dev
optional
# See the app with 12 weeks of data in it
npm run db:seed:demo
npm run db:seed:history

# Keys for notifications
npm run gen:vapid

# Run the tests
npm test

Then open localhost:3000. The first time, you are asked to create the administrator account — everyone else you invite yourself.

📱

On your iPhone

Open in Safari → the Share icon → “Add to Home Screen”. Uruz then sits on the home screen and opens full-screen with no browser bar.

In the cloud

Run the migrations in supabase/migrations/, set the keys as environment variables and deploy to Vercel. RLS comes with them.

📖

Documentation

README for setup, ARCHITECTURE for how it fits together, and DECISIONS for why each choice was made.