Work / 2026 / tool
FinnaRenewer
Automatically renews Oulu Finna library loans every week, for anyone who signs up.
- Status
- live
- Stack
- Next.js · TypeScript · PostgreSQL (Neon) · Drizzle ORM · Auth.js · Cheerio · Web Push · Vercel Cron
- Schedule
- Weekly, via Vercel Cron
- Users
- Multi-tenant, isolated credentials
- Interface with Finna
- Server-side scrape, Cheerio
- Notifications
- Web Push
A multi-user service that logs into the Oulu Finna library system on a weekly cron and renews every loan it can, then pushes a notification about what it did. Built because library fines are a tax on forgetting, and forgetting is not a character flaw worth paying for.
The problem
The library has your books, a renewal button, and no API. Renewing on time is a solved problem for anyone who never forgets. For everyone else it is a small recurring fine. The task is mechanical, weekly, and exactly the kind of thing a computer should have taken over decades ago.
Constraints
- No API exists. The only interface is the website a human uses.
- Other people’s library credentials. The blast radius of getting storage wrong is somebody else’s account, which raises the bar on every decision below.
- Must run unattended. If it needs a human to notice it failed, it has not replaced the human.
Decisions
Scrape server-side with Cheerio, never in the browser. Credentials and session handling stay on the server. The client never sees a library session.
Neon Postgres with Drizzle, not a key-value store. Users, sessions, credentials and renewal history are relational and worth querying. Drizzle keeps migrations in the repo so the schema history is reviewable.
Auth.js with the Drizzle adapter, and bcrypt for passwords. Application auth is separate from library credentials, and neither is rolled by hand.
Web Push instead of email. The output of a successful run is one line — which loans were renewed and which failed. Email for that is heavy, and a failed renewal is time-sensitive in a way an inbox is not.
Weekly, not daily. Renewal windows are wide enough that daily runs would be mostly no-ops hitting somebody else’s server. Politeness toward a service you depend on is an engineering property.
Tradeoffs
Scraping is brittle by construction: the library redesigns their page and the service breaks. That is accepted, not solved — the alternative was not building it. The failure mode is loud rather than silent, which is the part that matters.
Vercel Cron is the least portable dependency in the stack, and the first thing to replace when this moves to the homelab. A cron container and the same handler behind a route is a small, known amount of work.