Files
bookshelf/server

Bookshelf server

PocketBase backend for the Bookshelf app — see ../docs/SPEC.md for the authoritative data model and API rules. This directory contains everything needed to provision and run it.

Layout

server/
  pocketbase          PocketBase v0.40.2 binary (gitignored — platform-specific)
  pb_data/            SQLite databases + uploaded files (gitignored — runtime state)
  pb_migrations/       Schema history, auto-captured — a fresh instance replays
                       these on first boot and ends up with the full schema
  pb_hooks/           JS hook that closes a PocketBase quirk (see below)
  setup-schema.sh     Idempotent schema/rules provisioning (curl + jq only)
  create-user.sh      Superuser-driven account creation (no public sign-up)
  deploy/             systemd unit, Docker, backups, remote-access guidance

Quickstart (local dev)

PocketBase must already be running (see deploy/README.md for how to run it as a proper service; for a quick local check you can just run the binary directly: ./pocketbase serve).

# 1. Provision the schema + API rules (safe to re-run).
./setup-schema.sh http://127.0.0.1:8090 <superuser-email> <superuser-password>

# 2. Create app accounts — there is no self-registration.
./create-user.sh you@example.com "a strong password" "Your Name"

Both scripts also read PB_URL/PB_EMAIL/PB_PASS from the environment, or fall back to .dev-credentials (gitignored, dev-instance-only — see .dev-credentials in this directory if present) if no args are given.

Schema summary

Three collections — bookcases, shelves, books — each requiring authentication for every action (list/view/create/update/delete). Deletion in the app is always a soft deleted flag (tombstone), never an actual record delete, so sync can propagate it — see SyncEngine in the Android app and docs/SPEC.md's Sync design section.

The built-in users collection is locked down: createRule = null means only a superuser can create an account (via create-user.sh); regular users can view any user (needed to resolve the added_by relation) and update only their own record.

The pb_hooks quirk

PocketBase's declarative list/search rule acts as a row-level SQL filter, not a hard gate: an unauthenticated request against a collection whose listRule requires auth still gets 200 OK with an empty result, rather than an error — because the rule can't be cleanly separated into "deny the whole request" vs. "just don't return matching rows" (see pocketbase/pocketbase#6492). No private data ever leaks this way, but a 200 is still the wrong signal for "you're not allowed here." pb_hooks/main.pb.js adds a small onRecordsListRequest hook that turns that into a 403 for the three app collections. It's plain JS, auto-loaded by the stock pocketbase binary — no recompilation, no framework — so it ships and deploys exactly like pb_migrations/.

Verifying a deployment

# Anonymous requests must be rejected (4xx) for all three collections:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/api/collections/books/records
# -> 403

# Self-registration must be rejected:
curl -s -X POST http://127.0.0.1:8090/api/collections/users/records \
  -H 'Content-Type: application/json' -d '{"email":"x@x.com","password":"password123","passwordConfirm":"password123"}'
# -> 403 "Only superusers can perform this action."

See deploy/README.md for running the server long-term, reaching it from outside your home network, and backups.