Skip to content

Projects

Selected work

Projects

Things I have built. The desktop view shows them as an interactive solar system — here is the readable list.

  • HRM

    live

    Full-stack HR management system

    An HR system with the boring parts done properly. Every change is written to an append-only log, and each entry is signed against the one before it. If someone edits the history, an admin endpoint walks the chain and tells you the exact entry where it broke. Two databases, because people data and audit data want different things. Rate limiting runs on Postgres, so there is no Redis to keep alive.

    • 2910 tests
    • 92.2% coverage
    • Tamper-evident audit log

    Integrations

    • Google OAuth
    • GitHub OAuth
    • Sentry
    • OpenTelemetry
    • Next.js
    • React
    • TypeScript
    • PostgreSQL
    • MongoDB
    • Prisma
    • MUI
    • Jest
    • Playwright
    • Docker
    • GitHub Actions
    • Zod
    • NextAuth
    • next-intl
    • ReactFlow
    • Pino
    • pg-boss
    • Anthropic API
    • Stryker
    • OpenTelemetry
    • Sentry
    • Vercel
  • Platform

    live

    Community platform built on HRM

    A live community site for a World of Warcraft guild, running at vuohiliitto.com. HRM sits inside it as a git submodule, so accounts, permissions and auditing came for free and the rest got built on top. Members earn XP and levels, chat in tabs with proper whispers, and put together Mythic+ teams by pulling character stats from Raider.IO. New members get a guided tour instead of a wall of buttons.

    • Real users
    • 1388 tests
    • HRM as a submodule

    Integrations

    • Raider.IO API
    • Google OAuth
    • GitHub OAuth
    • GitHub API
    • Turborepo
    • Next.js
    • React
    • TypeScript
    • PostgreSQL
    • Prisma
    • NextAuth
    • MUI
    • Playwright
    • Jest
    • next-intl
    • Zod
    • framer-motion
    • Vercel
  • Portfolio

    wip

    This site

    The site you are reading. The pages themselves are static, with nothing running behind them, but the terminal on the contact page is wired to a language model on a machine in my house. Ask it something and a FastAPI service answers from my own repositories, retrieving over pgvector with a Finnish language router in front, reached through a Tailscale funnel. The projects page is a solar system you can drag around and the landing page is one particle field that keeps rearranging itself. It is where the motion work happens, so the production apps do not have to carry it.

    • Self-hosted LLM on a home GPU
    • Answers only from my own corpus
    • Static build, no SSR
    • Astro
    • Three.js
    • GSAP
    • TypeScript
    • Tailwind CSS
    • postprocessing
    • FastAPI
    • pgvector
    • fastembed
    • lingua
    • asyncpg
    • Tailscale Funnel
    • Docker
    • GitHub Actions
    • CodeQL
    • Vitest
    • Playwright
    • pytest
    • Sentry
  • ReadLog

    live

    Track every book you've read

    A reading log. Search for a book, say whether you read it on paper, as an e-book or listened to it, give it a rating, done. It asks Open Library and Google Books at the same time and merges the answers, because neither of them has everything. There is a public feed of what people finished recently, with no names attached.

    • 90 tests
    • Searches two book APIs at once

    Integrations

    • Open Library
    • Google Books
    • Google OAuth
    • Next.js
    • React
    • TypeScript
    • Prisma
    • PostgreSQL
    • NextAuth
    • MUI
    • Jest
    • Neon
    • Vercel
  • ReadLog .NET

    live

    ReadLog, rebuilt in .NET

    The same reading log, written again from scratch in ASP.NET Core to find out what a port actually costs. Razor Pages instead of React, EF Core and SQLite instead of Prisma and Postgres. It runs on Azure free tier, which means it falls asleep after twenty minutes and the first visit is slow. The repo keeps notes on every decision where the .NET way and the Next.js way disagreed.

    • Same app, second stack
    • Free Azure tier
    • Porting notes for every decision

    Integrations

    • Open Library
    • Google Books
    • Google OAuth
    • .NET 8
    • ASP.NET Core
    • Razor Pages
    • EF Core
    • SQLite
    • ASP.NET Identity
    • Bootstrap 5
    • xUnit
    • Docker
    • GitHub Actions
    • Azure App Service
  • AudiobookMaker

    wip

    Turns books into audiobooks

    A desktop app that reads a PDF, EPUB or Word file out loud and saves it as an audiobook. Scanned books go through OCR first. Four voice engines to pick from, depending on whether you want it free, offline, or cloned from a short clip of someone speaking. Finnish is the hard part, so it gets its own nineteen-pass cleanup before a word is spoken: Finnish text-to-speech mangles numbers and abbreviations in very predictable ways. Ships as a Windows installer with everything bundled.

    • 3000+ tests
    • Nineteen-pass Finnish text cleanup
    • Voices the story in Spacepotatis

    Integrations

    • Microsoft Edge-TTS
    • Python
    • PyMuPDF
    • ebooklib
    • ocrmypdf
    • Tesseract
    • num2words
    • edge-tts
    • Piper
    • Chatterbox
    • VoxCPM2
    • PyTorch
    • CustomTkinter
    • pydub
    • pygame
    • ffmpeg
    • PyInstaller
    • Inno Setup
    • GitHub Actions
    • Qwen VoiceDesign
    • pytest
  • Spacepotatis

    live

    Browser shooter: your potato vs the galaxy

    A browser game where a potato in a shield bubble shoots bugs. It boots like an old terminal, opens into a 3D solar system you can drag around, then drops you into a top-down fight. All the music comes from strudel-patterns and all the voice from AudiobookMaker, narrated by Grandmom. In May a player lost their save, because the anti-cheat checked that numbers had not grown too fast but never that they had shrunk. That one is written up as an incident runbook.

    • ~1170 tests
    • Original music and voice
    • Has an incident runbook

    Integrations

    • Google OAuth
    • Next.js
    • React
    • TypeScript
    • Phaser 4
    • Three.js
    • GSAP
    • PostgreSQL
    • Kysely
    • NextAuth
    • Tailwind CSS
    • Zod
    • Neon
    • Vercel
    • Vitest
  • Strudel Patterns

    live

    Algorithmic music, written as code

    Music written in Strudel, where a whole track is one JavaScript expression you edit while it is playing. Drums, bass, synth layers and effects live as separate pieces you can stack into new tracks. Nine are written up, and some of those ended up as the Spacepotatis soundtrack and the music on this landing page. Every session is logged next to the git history, so you can see how a track got where it did.

    • Nine finished tracks
    • Scores Spacepotatis and this site
    • Strudel
    • JavaScript
    • Web Audio API
    • Claude Code
  • claude-continue

    wip

    Keeps Claude Code running back to back

    Claude Code works in five-hour windows. This waits for one to roll over and starts the next, so there is no dead time while you are asleep. Python with no dependencies at all, running unattended through launchd or Task Scheduler, and it checks whether a session is mid-thought before it types anything. The README is honest that working this way grows the pile of code nobody has reviewed yet.

    • No runtime dependencies
    • macOS, Windows, WSL, Linux
    • Runs unattended
    • Python
    • Tkinter
    • ccusage
    • PyInstaller
    • launchd
    • Task Scheduler
    • tmux
    • GitHub Actions
  • PasswordManager

    wip

    Zero-knowledge password manager in Rust

    All the crypto lives in one Rust crate. The CLI, the browser client, the sync server and the Chrome extension all use that same crate, compiled either natively or to WebAssembly, so there is only ever one place to get it wrong. The server only ever sees ciphertext. Unlocking takes about 430 milliseconds on purpose, because the key derivation is tuned well above the library defaults. The README also lists five things it will not protect you from, which seemed more useful than pretending.

    • One crypto crate, four clients
    • Server sees only ciphertext
    • ~430ms unlock, on purpose

    Integrations

    • Google OAuth
    • Tailscale Funnel
    • Cloudflare Tunnel
    • Rust
    • WebAssembly
    • Tokio
    • SQLite
    • Argon2id
    • XChaCha20-Poly1305
    • wasm-bindgen
    • Chrome Extension (MV3)
    • JavaScript
    • GitHub Actions
    • axum
    • zeroize
  • claude-agents

    live

    Cheaper models for the boring work

    Fourteen Claude Code subagents, each pinned to the cheapest model that can do its job. Reading and reporting goes to Haiku, mechanical edits go to Sonnet, and only design work gets the expensive model. Each one works out what a repo uses on its own, so the same set covers JavaScript, C# and Python without configuration. Two of them exist because a review workflow quietly spent 3.8 million tokens at the wrong price.

    • 14 model-pinned agents
    • Model and effort both pinned
    • MIT licensed
    • Claude Code
    • Claude Haiku
    • Claude Sonnet
    • Claude Opus
    • Bash
    • Markdown
  • Feedback Intelligence

    live

    Reads customer feedback without making things up

    Takes messy free-text feedback and turns it into something a manager can act on. The interesting part is how little of it is AI: the model tidies up what people typed and finds themes across a pile of it, and the rest is ordinary code. Alerts are keyword scans. Sentiment is arithmetic. It runs a Finnish model on a machine at home, so hosting costs nothing. Every decision about where AI was allowed is written down, including the four rounds of arguing it back out.

    • AI in exactly two places
    • Finnish-first, runs locally
    • Costs nothing to host

    Integrations

    • Tailscale Funnel
    • .NET 8
    • C#
    • SQLite
    • Ollama
    • Poro 2 8B
    • Microsoft.Extensions.AI
    • xUnit
    • Docker
    • Node.js
    • Azure Static Web Apps
    • Azure Functions
    • GitHub Actions
    • CodeQL