Skip to content

About

Self-hosted personal book library — scan or enter an ISBN, rate it, mark it read. ASP.NET Core API + Angular web app, backed by the free Open Library catalog.

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

29 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AnyBookStore

AnyBookStore lets you scan or manually enter the ISBN of any book — novels, manga, technical literature, comics — to build a personal library with your own ratings and a read/unread status. This repository holds the backend of that ecosystem: a REST API and a background worker. A web frontend and a mobile app (sharing one Avalonia UI codebase, already scaffolded here) are planned on top of this backend.

Why it's built this way

  • The book catalog is free, always. Metadata comes from the Open Library API — no key, no paid tier, no account-gated dependency.
  • API requests are always fast. The API never calls Open Library synchronously. A background worker owns all communication with Open Library and keeps the local catalog cache current; the API only ever reads/writes its own Postgres database.
  • You never hit a dead end. If a book isn't in the catalog yet, the lookup response says so and invites you to add it yourself. Your own entry is private until it's published — either by you, or automatically once the worker finds an official match on Open Library.

Architecture

AnyBookStore.Domain            Plain entities & enums (Book, Author, Picture, UserBook, PendingLookup, ...)
AnyBookStore.Infrastructure    EF Core (Postgres), Open Library client, publish/enrichment services
AnyBookStore.Api               ASP.NET Core minimal API (JWT + Google OAuth, book/library endpoints)
AnyBookStore.Worker            Background service: resolves the lookup queue, re-syncs stale entries

Domain has no dependency on EF Core or ASP.NET Core, so both the Api and the Worker (and any future backend component) share the same model without pulling in unrelated framework weight.

Data model in one paragraph

Book rows are ISBN-keyed and either Private (visible only to their creator) or Published (visible to everyone). An ISBN is only unique among published, non-retired books — several users can independently hold private drafts of the same not-yet-published ISBN without colliding. A private draft becomes canonical either when its owner explicitly publishes it, or automatically once the background worker finds a matching Open Library record; if a published book for that ISBN already exists by then, the draft is retired and its UserBook (library) entries are relinked to the canonical book instead. UserBook is designed for a future offline-sync mobile client: client-generatable IDs, an UpdatedAt sync cursor, soft deletes, and Postgres xmin-based optimistic concurrency exposed via ETag/If-Match.

Background worker

  • LookupProcessingWorker polls a PendingLookup queue (populated whenever an ISBN lookup misses the cache) every 10 seconds, with exponential backoff (1h → 6h → 1d → 3d → 7d) on repeated misses.
  • FreshnessSweepWorker re-checks already-published, Open-Library-sourced catalog entries older than 90 days once a day, so corrections on Open Library's end eventually reach the cache.

Tech stack

.NET 10 · ASP.NET Core Minimal APIs · EF Core + Npgsql (PostgreSQL) · ASP.NET Core Identity (password hashing) + Google OAuth · JWT bearer auth with rotating refresh tokens · prometheus-net.AspNetCore for /metrics · Docker Compose · xUnit + NSubstitute + Shouldly (Testcontainers for the API's integration suite, EF Core InMemory for unit tests) · Avalonia UI + CommunityToolkit.Mvvm for the client (web/desktop/mobile, one codebase)

Getting started

Prerequisites

Run the full backend

cp .env.example .env   # then fill in real values - see below
docker compose up -d --build

This starts three containers: db (Postgres), api (port 8080), worker. The API applies pending EF Core migrations automatically on startup.

.env values

Variable Purpose
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB Postgres credentials, shared by all three containers
JWT_SIGNING_KEY HMAC-SHA256 signing key for access tokens — needs at least 256 bits (openssl rand -base64 32)
AUTHENTICATION__GOOGLE__CLIENTID / AUTHENTICATION__GOOGLE__CLIENTSECRET From a Google Cloud Console "Web application" OAuth client; the authorized redirect URI must be <your-domain>/auth/external/google/callback

ASP.NET Core validates every registered authentication scheme on the very first incoming request — if the Google credentials are missing or empty, the entire API returns 500 on every route, not just the Google ones. Keep them set even if you don't use Google login yet.

Running locally without Docker

docker compose up -d db                 # just the database
dotnet run --project AnyBookStore.Api
dotnet run --project AnyBookStore.Worker

Local defaults (dev-only signing key, localhost connection string) live in each project's appsettings.Development.json. Secrets that shouldn't be committed (Google OAuth credentials) still need to come from the environment — e.g. in fish:

set -gx AUTHENTICATION__GOOGLE__CLIENTID "..."
set -gx AUTHENTICATION__GOOGLE__CLIENTSECRET "..."

Running the web client

dotnet run --project AnyBookStore.Browser

Opens a dev server on http://localhost:5235 (see AnyBookStore.Browser/Properties/launchSettings.json). It's an Avalonia app compiled to WebAssembly — the same UI codebase (AnyBookStore/) that will back the desktop and mobile clients, rather than a separate web framework, so views/view models/services are written once. Points at the API via AnyBookStore/Configuration/AppConfig.cs (http://localhost:5087 in Debug builds); the API's WebClient:BaseUrl config must match wherever the client is actually served from, for CORS and the Google OAuth redirect to work.

API overview

In development, an interactive reference is available at /scalar/v1. All routes below are relative to the API's base URL (no /api prefix — the API is meant to live on its own subdomain).

Method Route Auth Purpose
POST /auth/register — Email/password registration
POST /auth/login — Email/password login
POST /auth/refresh — Rotates a refresh token for a new access token
POST /auth/logout ✓ Revokes a refresh token
GET /auth/external/google — Starts the Google OAuth handshake
GET /auth/external/google/complete — Finishes it, returns the same token pair as /login
GET /books/isbn/{isbn} optional Cache-only lookup; on a miss, returns 404 with canCreate: true and queues a background lookup
POST /books ✓ Creates a private draft for an ISBN (idempotent)
POST /books/{id}/publish ✓ Publishes your own draft
GET /library ✓ Lists your active library entries
GET /library/{id} ✓ Gets one entry (returns an ETag)
POST /library ✓ Adds a book to your library (idempotent)
PATCH /library/{id} ✓ Updates rating/read status; requires If-Match, 409 on a stale version
DELETE /library/{id} ✓ Soft-deletes an entry
GET /library/changes?since= ✓ Sync feed: everything (including deletes) changed after since
GET /metrics — Prometheus scrape endpoint

Testing

dotnet test AnyBookStore.Api.Tests            # integration tests, spins up Postgres via Testcontainers
dotnet test AnyBookStore.Infrastructure.Tests # unit tests, EF Core InMemory
dotnet test AnyBookStore.Worker.Tests         # unit tests, EF Core InMemory

Status

The backend (API + worker, auth, catalog, library sync, Docker deployment, tests) is feature-complete per the original plan.

The client is a single Avalonia UI codebase (AnyBookStore/) shared across web (AnyBookStore.Browser, WebAssembly), desktop (AnyBookStore.Desktop) and, eventually, mobile (AnyBookStore.Android/AnyBookStore.iOS) — one source of truth for both the UI and the wire contracts (AnyBookStore.Contracts, referenced by both the client and the API). Auth (email/password + Google OAuth, token refresh, DE/EN localization) is implemented and working end-to-end against the real API on the web target; desktop/mobile still need their own OAuth completion flow (the browser-redirect approach doesn't carry over as-is), and the actual library screens (list/add-by-ISBN/rate/remove) are still a placeholder on every target.

About

Self-hosted personal book library — scan or enter an ISBN, rate it, mark it read. ASP.NET Core API + Angular web app, backed by the free Open Library catalog.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages