Skip to content
tech-thinkerPublic

About

Owelith is the personal account and many flow manager. Beautiful UI with Golang backend.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Owelith

Owing + Elending + Ith — a self-hosted app for tracking what you owe, what you're owed, and your financial history.

Owelith is a self-hosted personal-finance and debt-tracking application. A Go backend (REST + gRPC) serves a React single-page UI from the same binary, so the whole product ships as one container image. Track accounts, transactions, categories, savings / repayment goals, and checklists — backed by a wallet with an immutable transaction history and role-based access control.

Docker Release Image


Table of contents


Features

  • Accounts / wallets — CASH (prepaid) and CREDIT (postpaid) accounts with running balances.
  • Transactions — immutable audit log of every balance change (top-up, deduction, transfers, repayments).
  • Categories — organize transactions with built-in system categories plus per-user custom categories.
  • Goals — savings and repayment targets with progress tracking.
  • Checklists — reusable task lists.
  • Authentication — JWT access + refresh tokens signed with Ed25519, stored/revoked via Redis, with token rotation.
  • Roles — ADMIN (manage users, system data) and USER (own data), enforced by middleware + ownership checks.
  • Admin user management — list/create/update users and enable/disable accounts.
  • Two transports — REST API (Gin) and a gRPC health service.
  • Observability — OpenTelemetry tracing (HTTP, gRPC, GORM), structured JSON logs (Zap), health checks, optional pprof.
  • Single image — the built React SPA is embedded and served from the Go binary at /ui.

Tech stack

Area Technology
Language Go 1.26
REST Gin
gRPC google.golang.org/grpc + protobuf
ORM / DB GORM — PostgreSQL (default), MySQL, SQLite
Cache / tokens Redis (go-redis/v9)
Config Viper (env-based)
Logging Zap (structured JSON)
Auth golang-jwt/jwt/v5 + crypto/ed25519 + bcrypt
Money shopspring/decimal
Tracing OpenTelemetry (otelkit)
CLI urfave/cli v1
Frontend React 18 + TypeScript + Vite
UI Tailwind CSS, TanStack Query, Zustand, React Router, lucide-react
Container Distroless, multi-stage build

Architecture

                    ┌────────────────────────────────────────────┐
   Browser  ─────▶  │  Go binary (single image)                  │
   :8080/ui         │                                            │
                    │  app/rest       Gin HTTP handlers          │
                    │  app/grpc       gRPC handlers              │
                    │  service        business logic             │
                    │  repository     data access                │
                    │  models         GORM entities              │
                    └───────┬───────────────────────┬────────────┘
                            │                       │
                     ┌──────▼──────┐        ┌───────▼───────┐
                     │ PostgreSQL  │        │    Redis      │
                     │  (GORM)     │        │ (token store) │
                     └─────────────┘        └───────────────┘

The backend is layered so each concern has one home:

Layer Responsibility Path
Transport HTTP/gRPC request handling & serialization app/rest, app/grpc
Service Business logic, validation, orchestration service/
Repository Data access, queries, transactions repository/
Models Domain entities & GORM tags models/
Frontend React SPA served as static files at /ui webui/

Repository layout

owelith/
├── main.go                 # Entry point — CLI commands (start, api, grpc)
├── config/                 # Centralized config (Viper)
├── constants/              # App-wide constants (env names, roles…)
├── instance/               # Singleton init (DB, Redis, registry, migrations)
├── logger/                 # Structured logging
├── runner/                 # REST + gRPC server runners
├── utils/                  # Shared utilities (banner, tracing, keys…)
├── errors/ · errorslug/    # Typed errors + error slugs
├── models/                 # GORM entities
├── repository/             # Data-access layer
├── service/                # Business-logic layer
├── app/
│   ├── rest/               # Gin controllers, router, middlewares
│   └── grpc/               # gRPC handlers, proto, router
├── webui/                  # React + Vite frontend (builds into ../public/ui)
├── public/ui/              # Built frontend (served by the backend at /ui)
├── migrations/             # SQL migrations (reference)
├── docker/                 # Backend Dockerfiles (dev, debug, prod)
├── docker-compose*.yml     # Compose files per environment
└── Makefile                # Build / test / run / docker targets

Quick start (local development)

Prerequisites

  • Go 1.26+
  • Node.js 20+ (for the frontend)
  • PostgreSQL and Redis (or run them via Docker — see below)
  • make (optional, but used throughout)

1. Configure environment

cp .env.example .env

Generate a JWT signing key and put it in .env (see Generate a JWT signing key for details):

openssl genpkey -algorithm Ed25519 -out ed25519.pem
printf 'JWT_PRIVATE_KEY=%s\n' \
  "$( (openssl pkey -in ed25519.pem -outform DER | tail -c 32; \
        openssl pkey -in ed25519.pem -pubout -outform DER | tail -c 32) | base64 -w0 )"

2. Start dependencies (Postgres + Redis)

docker compose -f docker-compose.yml -f docker-compose-dev.yml up -d postgres redis

This exposes Postgres on localhost:5432 and Redis on localhost:6379, matching the defaults in .env.example.

3. Install dependencies and run

make setup            # go mod tidy
make frontend-setup   # cd webui && npm install
make run              # go run main.go start  (REST + gRPC)

Open http://localhost:8080/ui.

For frontend hot-reload during development:

make frontend-dev     # Vite dev server on http://localhost:5173

Or use the fully Dockerised dev stack (backend hot-reload via modd + Vite): make docker-up-dev.


On-prem installation with Docker

Production deployment runs three containers — backend (API + UI), PostgreSQL, and Redis — on a single host. The backend image already bundles the built React UI, so no separate web server is required.

Prerequisites

Requirement Notes
Docker Engine 24+
Docker Compose v2 (docker compose, not docker-compose)
openssl to generate the JWT key
Resources ≥ 1 vCPU, ≥ 2 GB RAM, ≥ 5 GB disk
Ports 8080 (UI + REST API), 9090 (gRPC) — open/free on the host

1. Generate a JWT signing key

Owelith signs JWTs with Ed25519. JWT_PRIVATE_KEY must be the base64 encoding of the raw 64-byte private key (seed ‖ public). This produces exactly that using only openssl:

openssl genpkey -algorithm Ed25519 -out ed25519.pem

export JWT_PRIVATE_KEY="$(
  ( openssl pkey -in ed25519.pem -outform DER        | tail -c 32
    openssl pkey -in ed25519.pem -pubout -outform DER | tail -c 32
  ) | base64 -w0
)"

echo "$JWT_PRIVATE_KEY"
# sanity check → must print 64
echo -n "$JWT_PRIVATE_KEY" | base64 -d | wc -c

macOS: base64 -w0 isn't available — use base64 | tr -d '\n' instead.

Keep ed25519.pem in a secure place (or delete it after copying the base64 value into your .env). Rotating this key invalidates all existing sessions.

2. Create the deployment directory and .env

sudo mkdir -p /opt/owelith && cd /opt/owelith

cat > .env <<EOF
DB_NAME=owelith
DB_USER=owelith
DB_PASS=$(openssl rand -hex 16)
JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}
IS_REGISTRATION_ENABLED=true
EOF

chmod 600 .env

3. Create the Compose file

Save the following as /opt/owelith/docker-compose.yml. It pulls the published image from GitHub Container Registry (no source checkout needed).

services:
  postgres:
    image: postgres:16-alpine
    container_name: owelith-postgres
    environment:
      POSTGRES_DB: ${DB_NAME:-owelith}
      POSTGRES_USER: ${DB_USER:-owelith}
      POSTGRES_PASSWORD: ${DB_PASS:-owelith}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-owelith} -d ${DB_NAME:-owelith}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    container_name: owelith-redis
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  backend:
    image: ghcr.io/tech-thinker/owelith:latest   # or pin a release: :1.2.3
    container_name: owelith-backend
    environment:
      - APP_NAME=owelith
      - APP_ENV=production
      - API_PORT=8080
      - GRPC_PORT=9090
      - DB_DRIVER=postgres
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_USER=${DB_USER:-owelith}
      - DB_PASS=${DB_PASS:-owelith}
      - DB_NAME=${DB_NAME:-owelith}
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_DB=0
      - JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}
      - JWT_ACCESS_EXPIRY=15
      - JWT_REFRESH_EXPIRY=10080
      - IS_REGISTRATION_ENABLED=${IS_REGISTRATION_ENABLED:-true}
      - CORS_ORIGINS=*
      - UI_ENABLED=true
    ports:
      - "8080:8080"   # UI + REST API
      - "9090:9090"   # gRPC
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Building from source instead? Clone the repo and use the checked-in docker-compose.yml, then run docker compose up -d --build (or make docker-deploy). It additionally starts an nginx-served copy of the UI on :8081, but the UI at http://<host>:8080/ui works either way.

4. Start the stack

cd /opt/owelith

# If the GHCR package is private, authenticate first:
# echo <GITHUB_PAT_with_read:packages> | docker login ghcr.io -u <github-user> --password-stdin

docker compose up -d
docker compose ps
docker compose logs -f backend   # watch startup / migrations

The backend creates/updates its schema automatically on boot. Wait until docker compose ps shows backend healthy and the logs report the server is listening.

5. Create the first administrator

There is no default/bootstrapped account, and self-registration creates a USER. So:

a. Register the first account (via the UI at http://<host>:8080/ui/register, or the API):

curl -X POST http://localhost:8080/gateway/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","email":"admin@example.com","name":"Administrator","password":"ChangeMe123!","deviceId":"setup"}'

b. Promote it to ADMIN:

docker compose exec postgres \
  psql -U "${DB_USER:-owelith}" -d "${DB_NAME:-owelith}" \
  -c "UPDATE users SET role='ADMIN' WHERE username='admin';"

c. Log out and back in so the new token carries the ADMIN role. You can now manage users from Users in the sidebar.

Once your admin exists, set IS_REGISTRATION_ENABLED=false in .env and run docker compose up -d to close public sign-ups.

6. Access the application

Service URL / address
Web UI http://<host>:8080/ui
REST API http://<host>:8080/api/v1/…, http://<host>:8080/gateway/auth/…
Health http://<host>:8080/api/v1/health
gRPC <host>:9090

7. Put it behind TLS (recommended)

Terminate TLS at a reverse proxy and forward to 127.0.0.1:8080. Example with Caddy:

# /etc/caddy/Caddyfile
owelith.example.com {
    reverse_proxy 127.0.0.1:8080
}

Then bind the backend to localhost only by changing its ports mapping to "127.0.0.1:8080:8080" and "127.0.0.1:9090:9090". Set CORS_ORIGINS to your public origin (e.g. https://owelith.example.com) instead of *.

Upgrade

cd /opt/owelith
docker compose pull          # fetch the newest image
docker compose up -d         # recreate containers; schema migrates on boot

To pin a release, set image: ghcr.io/tech-thinker/owelith:1.2.3 (or 1.2, 1) in the Compose file and docker compose up -d.

Backup & restore

# Backup
docker compose exec -T postgres pg_dump -U owelith owelith > owelith-$(date +%F).sql

# Restore
cat owelith-2026-01-01.sql | docker compose exec -T postgres psql -U owelith -d owelith

Redis holds only ephemeral tokens, so backing up Postgres is sufficient. For extra safety, back up the postgres_data and redis_data volumes too.

Uninstall / teardown

docker compose down            # stop & remove containers (data volumes kept)
docker compose down -v         # also delete the database/redis volumes (destructive)

Configuration reference

All configuration is environment-driven. The values below are used by the container deployment.

Variable Default Description
VERSION 1.0.0 Reported app version
APP_NAME owelith Service name (logs/tracing)
APP_ENV production development | staging | production
TZ UTC Container timezone
API_PORT 8080 REST HTTP port
GRPC_PORT 9090 gRPC port
CORS_ORIGINS * Comma-separated allowed origins
DB_DRIVER postgres postgres | mysql | sqlite
DB_HOST postgres Database host
DB_PORT 5432 Database port
DB_USER owelith Database user
DB_PASS — Database password
DB_NAME owelith Database name
REDIS_HOST redis Redis host
REDIS_PORT 6379 Redis port
REDIS_PASSWORD (empty) Redis password
REDIS_DB 0 Redis logical database
JWT_PRIVATE_KEY — Required. Base64 of the raw 64-byte Ed25519 private key
JWT_ACCESS_EXPIRY 15 Access-token lifetime (minutes)
JWT_REFRESH_EXPIRY 10080 Refresh-token lifetime (minutes; 7 days)
IS_REGISTRATION_ENABLED true Allow public self-registration
UI_ENABLED true Serve the built SPA from the backend at /ui
OTEL_ENABLED false Enable OpenTelemetry export
OTEL_EXPORTER_URL — OTLP endpoint (e.g. http://otel-collector:4317)
OTEL_PROTOCOL grpc grpc | http

API overview

Base URL: http://<host>:8080. All authenticated endpoints expect Authorization: Bearer <accessToken>.

Auth — /gateway/auth

Method Path Description
POST /gateway/auth/register Create a USER account
POST /gateway/auth/login Exchange credentials for an access + refresh token
POST /gateway/auth/renew_access_token Mint a new access token
POST /gateway/auth/renew_refresh_token Rotate the refresh token
POST /gateway/auth/logout Revoke the current session

Application — /api/v1

Method Path Description
GET /health Health check
GET/PUT /profile Read / update the current profile
POST /profile/change-password Change password
* /accounts Wallet/account management
* /transactions Transaction listing & creation
* /categories Category CRUD
* /goals Goals CRUD
* /checklists Checklists CRUD
GET/POST/PUT/PATCH /users, /users/:id, /users/:id/status Admin only user management

gRPC exposes a health service on port 9090.

Example: log in

curl -X POST http://localhost:8080/gateway/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":"ChangeMe123!","deviceId":"cli"}'

Operations

docker compose ps                    # container status
docker compose logs -f backend       # follow backend logs
docker compose restart backend       # restart just the API
docker compose exec backend sh       # shell into the container (distroless: may be absent)
  • Logs are structured JSON on stdout (level info in production, debug in development).
  • Health: GET /api/v1/health returns the status of dependencies.
  • Tracing: set OTEL_ENABLED=true and point OTEL_EXPORTER_URL at an OTLP collector (e.g. Jaeger/Tempo) to export traces.

Development

# Backend
make run              # run REST + gRPC
go run main.go api    # REST only
go run main.go grpc   # gRPC only
make build            # build ./owelith binary

# Frontend
make frontend-dev     # Vite dev server (:5173, proxies API to backend)
make frontend-build   # build into ../public/ui (served by the backend)
make frontend-lint
make frontend-type-check

# Docker
make docker-up-dev    # dev stack: backend hot-reload + Vite
make docker-up        # production stack (build from source)
make docker-up-staging
make docker-up-debug  # backend under Delve on :2345
make docker-config    # validate every compose configuration
make docker-clean     # stop everything and remove volumes

Adding a feature

Follow the layered pattern: Model → Repository → Service → Transport, then wire it into the registry (instance/registry/models.go + instance/instance.go) and expose it via a controller (app/rest/controllers/) and/or gRPC handler. See AGENTS.md for the full convention guide.


Testing

# Backend (race detector + coverage)
make test
go test -v ./service/... -race

# Coverage report
make coverage-html && make coverage-serve

# Frontend
cd webui && npm run type-check

License

See the repository for license information.

About

Owelith is the personal account and many flow manager. Beautiful UI with Golang backend.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages