Swizzy/skuld

★ 0Forks 0RustGitHub ↗Compare

README

Skuld

Self-hosted debt ledger. Rust + Axum + Postgres, vanilla PWA frontend. Replaces a Coda.io "Skuld Mamma" doc with a mobile-friendly web app: a running balance, a transaction list, an always-visible add form, and a step-chart graph view with range presets and hover.

Deploy on Coolify

  1. Create a Postgres resource in Coolify. Note its internal connection URL.
  2. Create an Application from this git repo, build pack = Dockerfile.
  3. Google OAuth client (Google Cloud Console → Credentials → OAuth client, type Web):
    • Authorized redirect URI: https://<your-domain>/auth/callback
  4. Set environment variables on the Application:
    var value
    DATABASE_URL the Coolify Postgres internal URL
    GOOGLE_CLIENT_ID from Google
    GOOGLE_CLIENT_SECRET from Google
    OAUTH_REDIRECT_URL https://<your-domain>/auth/callback
    BOOTSTRAP_ADMIN_EMAIL your Google email (first-boot admin)
    SESSION_SECRET 32+ random bytes
    COOKIE_SECURE true (leave default; set false only for local HTTP)
  5. Set the app port to 3000 (matches the image's EXPOSE and the PORT env); assign your domain (Coolify provisions HTTPS via its Traefik proxy).
  6. Deploy. On first boot the app runs migrations and bootstraps your admin account. The transactions table starts empty; add transactions in-app.
  7. Sign in with Google. Invite others from the account menu (admin only).

Only Google accounts whose email is on the allowlist (the users table) can sign in. The first admin comes from BOOTSTRAP_ADMIN_EMAIL; thereafter manage the allowlist in-app via POST /api/users / DELETE /api/users/:id.

Local dev

docker run -d --name skuld-pg -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=skuld -p 5432:5432 postgres:17
cp .env.example .env   # fill values; DATABASE_URL=postgres://postgres:pw@localhost:5432/skuld
#   SESSION_SECRET must be 32+ bytes; set COOKIE_SECURE=false for plain-HTTP localhost
cargo run

Open http://localhost:8080. (Google login needs real OAuth credentials + the redirect URI registered; the rest of the app runs without them.) The table starts empty; add transactions in-app.

Testing

Tests run against a disposable, always-clean Postgres (tmpfs-backed, port 5433). One command resets it and runs the suite:

bash scripts/test.sh        # or: pwsh scripts/test.ps1

Each test gets its own isolated database via #[sqlx::test], so runs never share state.

Architecture

One Rust binary serves everything:

  • GET /health — liveness (Coolify health checks)
  • /auth/login · /auth/callback · /auth/logout — Google OIDC (authorization code + PKCE), allowlist-gated, signed HMAC session cookie
  • /api/transactions (CRUD), /api/export, /api/users (admin allowlist) — session-gated JSON API
  • everything else — static SPA from web/ (dark-mode PWA, installable, offline shell)

belopp is NUMERIC(12,2); datum is DATE. Positive amounts increase the debt, negative decrease it; the balance is SUM(belopp).

Contributors

Swizzy

Issues