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, and manages a purgatory queue for books with invalid ISBNs.
The system processes crawl jobs asynchronously via Redis: the API enqueues crawl commands, and a separate crawler service consumes them and publishes typed crawl events back through Redis.
These events are processed in real time and streamed to the UI via SSE.
- Kotlin
- Spring Boot
- Spring Data JPA
- PostgreSQL
- Redis
- Flyway
- Spring Security resource server
- Java 21
- PostgreSQL
- Redis
- A Clerk application with a JWKS endpoint
The system uses Redis as the only communication layer between API and crawler.
crawler:commands
Used to enqueue crawl jobs from the API.
crawl:events
Used by the crawler to publish:
- scraped books
- progress updates
- completion events
- error events
Events are consumed asynchronously by the API and streamed to clients.
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
REDIS_URL=redis://localhost:6379
CLERK_JWKS_URL=https://example.clerk.accounts.dev/.well-known/jwks.json
IMAGES_DIR=/path/to/imagesCreate 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
All endpoints except /api/v1/ping and /api/v1/images/** 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
}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.
- API enqueues crawl jobs into Redis (
crawler:commands) - crawler consumes jobs using a blocking queue (
BLPOP) - crawler publishes events into Redis (
crawl:events) - API consumes events and:
- updates database state
- buffers and batch-inserts books
- streams updates to frontend via SSE
- Only one crawl per source can run at a time
- Duplicate crawl requests are rejected by the crawler using Redis locks
- Locks have TTL and are periodically extended by the crawler
Schema changes are managed with Flyway migrations in:
src/main/resources/db/migration/
Migrations run automatically on startup.