Baseline: wave 1A server complete, wave 1B Android scaffold + design system green

assembleDebug, testDebugUnitTest, and recordPaparazziDebug all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bThmkmyUUdqQpy3MXFFe5
This commit is contained in:
2026-09-06 01:58:37 +00:00
commit 6c17e42037
79 changed files with 3773 additions and 0 deletions
+81
View File
@@ -0,0 +1,81 @@
# 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`).
```sh
# 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](https://github.com/pocketbase/pocketbase/discussions/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
```sh
# 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.