Vlad Ilie
Lumea BookTruck — catalogue, stock, loans and field uploads for a mobile library
Next.js · Pro-bono · NGO

Lumea BookTruck — catalogue, stock, loans and field uploads for a mobile library

lumea.booktruck.ro ↗
1 month ago
•••
9 min read

Context

BookTruck is the mobile library of Cărțile pe Față, the reading association whose IT infrastructure I've looked after pro-bono for years. The trucks lend books to children in rural schools, so behind every trip there is stock to track, loans to record, returns to chase — and a lot of photos and videos.

Lumea BookTruck is the app that holds all of that together. The public side is a searchable catalogue of the books that travel with the trucks. The panel is where admins, librarians and volunteers do their day-to-day work, from a desk or from a phone in a school courtyard.

The problem

Two things were scattered. The catalogue, stock and loans to schools lived partly in spreadsheets and partly in people's memory: it wasn't clear how many copies of a book were in the truck and how many at headquarters, or how many were still at a given school. And the media from each trip ended up, in the association's own words, chaotically in a WhatsApp group — compressed, unsorted and impossible to find a month later, even though the organisation already had plenty of Google Drive space.

What I built

Permissions are a matrix per resource (books, stock, loans, returns, authors, genres, schools, locations, stats, team, gallery) for three roles. Volunteers browse the catalogue and record loans and returns. Librarians manage the whole catalogue, state changes included. Admins also handle schools, locations and stock moves, history exports, upload sessions and the team. Every action is checked on the server, and the menu hides the sections your role can't use.

Under the hood

The interesting decision is in the uploads. Files never pass through the server. The server only opens a Google Drive resumable upload session with a service account and hands its URI to the browser. The browser then sends the file in 8 MB chunks, at most three files at a time so the phone's connection isn't saturated. If the connection drops or stalls, the upload doesn't start over: the browser backs off a little longer between attempts, asks Drive how much it already has and carries on from there. Any progress resets the retry budget, so a long clip on a flaky connection keeps going as long as it moves forward. Proxying the files would have hit the 4.5 MB request-body limit and the timeouts of Vercel Functions. Volunteers never get access to the Drive, each session has a storage cap, and every completed file is logged (name, type, size, duration), which feeds the panel's stats.

Stock is kept per book and location, with two numbers: how many copies the location holds and how many are available right now. A loan only lowers the available count, and a return to a different location moves the copies from one stock to the other. Locations are referenced by an identifier that never changes, so old loans and logs stay correct after a location is renamed or retired. The arithmetic for moving a whole location is a pure function, tested without a database.

Authentication moved from Supabase Auth to Better Auth with passwordless magic links and sign-up disabled. I kept every existing user id and foreign key with an additive backfill, so nothing had to be remapped. Sending links is rate limited in Redis per email address and per IP, because every email costs credits. The admin role can't be granted, changed or impersonated from the app, only in the database. Supabase now only stores book covers and avatars, with every write done on the server after a permission check.

Redis also caches the catalogue stats, refreshed weekly, and last month's top, generated on the first visit of the new month with no cron. It carries the presence heartbeats too: one every two minutes, so a small team stays comfortably within a free plan. If Redis is missing, the features that need it are skipped and the rest of the app carries on. Schema changes are hand-written SQL migrations, and every date is pinned to Europe/Bucharest so the server and the browser always show the same time. The core rules — permissions, stock moves, invitations, logs, the history export and the validation of the loan and return forms — are covered by Vitest.

Result

One link per trip, and files land organised in Drive instead of a chat group. Large videos upload from a phone even on weak connections, without ever touching — or overloading — the server. A loan or a return at a school is one scanned ISBN and one confirmation, and the stock on every truck and location stays current. Librarians and volunteers share one panel, each seeing only what their role allows, and the catalogue the children borrow from is finally searchable by anyone.

Stack

Next.js 16 (App Router), React 19, TypeScript, Prisma 7 on Supabase Postgres, Better Auth, Tailwind CSS 4 with shadcn/ui, TanStack Table, react-hook-form + zod, Recharts, ZXing, Redis, Google Drive API, Brevo, Supabase Storage, Vitest, Vercel.

Technologies:

  • Next.js
  • Prisma
  • Better Auth
  • Google Drive API
  • Supabase
  • Redis
  • Vitest
  • Vercel

Share on: