Kotlin/Spring Boot backend for the Libri catalog.
It stores book metadata in PostgreSQL, serves cover images from local storage, exposes authenticated endpoints for the admin UI, provides internal endpoints used by the crawler, and manages a purgatory queue for books with invalid ISBNs.
- Kotlin
- Spring Boot
- Spring Data JPA
- PostgreSQL
- Flyway
- Spring Security resource server
- Java 21
- PostgreSQL
- A Clerk application with a JWKS endpoint
- A built
libri-crawlerbinary if you want to trigger crawls from the API
The application reads environment variables through Spring config and local .env
loading in development.
Required variables:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=libri
DB_USER=libri
DB_PASS=secret
CLERK_JWKS_URL=https://example.clerk.accounts.dev/.well-known/jwks.json
IMAGES_DIR=/absolute/path/to/images
CRAWLER_BINARY_PATH=/absolute/path/to/libri-crawler/bin/crawler
INTERNAL_API_KEY=change-me
CORS_ALLOWED_ORIGINS=http://localhost:3000Create the image directory before startup:
mkdir -p "$IMAGES_DIR"makeThis starts the following processes in parallel:
bootRun— runs the Spring Boot applicationbuild --continuous— recompiles on file changes, triggering devtools hot reload
If you also have a local clone of libri-crawler, you can enable automatic rebuilding of the crawler binary on file changes.
To enable crawler watching:
-
Install
watchexec:brew install watchexec
-
Set
DEV_CRAWLER_DIRin your.envto point to your local crawler repo:DEV_CRAWLER_DIR=../libri-crawler
When configured, an additional process will run:
- Crawler watcher — rebuilds the Go binary on file changes
If DEV_CRAWLER_DIR is not set, the crawler watcher is skipped automatically and only the API runs.
All endpoints except /api/v1/ping, /api/v1/images/**, and /api/v1/internal/**
require a valid Bearer token.
Authorization: Bearer <token>
Admin endpoints require the authenticated user to have is_admin: true, which is
mapped to ROLE_ADMIN.
Set this in Clerk dashboard → Users → select user → Public metadata:
{
"is_admin": true
}Internal crawler endpoints use X-Internal-Key instead of JWT authentication.
GET /api/v1/pingGET /api/v1/images/{isbn}.jpg
GET /api/v1/booksGET /api/v1/books/{code}GET /api/v1/sources
POST /api/v1/admin/booksPUT /api/v1/admin/books/{isbn}DELETE /api/v1/admin/books/{isbn}POST /api/v1/admin/crawlPOST /api/v1/admin/crawl/{source}GET /api/v1/admin/crawlGET /api/v1/admin/crawl/eventsGET /api/v1/admin/purgatoryPOST /api/v1/admin/purgatory/{id}/approveDELETE /api/v1/admin/purgatory/{id}
Book create and update requests use multipart/form-data:
book— JSON payload with book metadatafile— uploaded cover image
On POST /api/v1/admin/books, file is required. On
PUT /api/v1/admin/books/{isbn}, file is optional.
Protected by X-Internal-Key, not JWT.
POST /api/v1/internal/books/batchGET /api/v1/internal/books/exists
Schema changes are managed with Flyway migrations in:
src/main/resources/db/migration/
Migrations run automatically on startup.