The long-standing open question "where will the server actually live" is answered for now: it stays on this sprite, bound to 0.0.0.0:8090 and published over HTTPS by the sprite proxy. Several docs asserted the opposite — HANDOFF said "127.0.0.1:8090, deliberately NOT internet-exposed (no --http-port, so the sprite proxy can't reach it)", which is flatly wrong today. The consequence is the part worth writing down: PocketBase's API rules are now the only thing between this library and the internet. There is no NAT, no VPN, no reverse proxy. So the anonymous-access curls stop being a formality, and they have to run against the PUBLIC hostname — localhost cannot tell you what the world can reach. Re-verified that way: books/shelves/bookcases LIST all 403, self-registration 403, health 200. One gap found while re-verifying, recorded but NOT fixed: users LIST answers 200 with an empty array instead of 403. Nothing is disclosed — two real accounts exist and the listRule filters both out — but it is the same wrong-signal quirk pb_hooks/main.pb.js exists to close, and that hook never listed the users collection. SPEC's offline-first rationale is amended rather than its rule: the reason is no longer residential NAT but a sprite that suspends when idle and wakes on request. The rule is unchanged and does not depend on which. server/deploy/ still documents systemd/Docker/Tailscale on home hardware; it now says up front that this is the intended end state, not what is running. Also corrected, since it was adjacent and plainly false: README still claimed the app had never run on a physical device. It has, since 2026-09-09. What is true is that no *automated* test runs on a device — there is no emulator on this box. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPpdG8VnRfS3KkisR3HUAE
111 lines
4.9 KiB
Markdown
111 lines
4.9 KiB
Markdown
# 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
|
|
|
|
**Run these against the address the outside world uses, not `127.0.0.1`.**
|
|
A localhost curl cannot tell you what is reachable from the internet, and
|
|
the live instance of this server (see below) *is* on the internet, with
|
|
these rules as its only protection.
|
|
|
|
```sh
|
|
BASE=https://your-server.example # the PUBLIC base URL, not localhost
|
|
|
|
# Anonymous requests must be rejected for all three collections:
|
|
for c in books shelves bookcases; do
|
|
curl -s -o /dev/null -w "$c %{http_code}\n" "$BASE/api/collections/$c/records"
|
|
done
|
|
# -> 403, 403, 403
|
|
|
|
# Self-registration must be rejected:
|
|
curl -s -X POST "$BASE/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."
|
|
```
|
|
|
|
A `200` with a non-empty `items` array from any of those is a breach, and a
|
|
`200` with an empty one means the `pb_hooks` guard is not loaded — see the
|
|
quirk described above.
|
|
|
|
> **Known gap:** `GET /api/collections/users/records` currently answers
|
|
> **200 with an empty array** rather than 403. Nothing is disclosed — the
|
|
> `listRule` filters every row out, so an anonymous caller sees no accounts,
|
|
> no emails, no ids — but it is the same wrong-signal quirk as above, and
|
|
> `pb_hooks/main.pb.js` does not yet cover the `users` collection. Adding it
|
|
> to that hook's collection list closes it.
|
|
|
|
## Where the live server runs
|
|
|
|
The instance these phones talk to runs **on the development sprite**, bound
|
|
to `0.0.0.0:8090` and published over HTTPS on a sprite-issued hostname, so
|
|
it is **internet-exposed**. That is an interim arrangement; `deploy/` covers
|
|
the intended move to home hardware behind Tailscale. The app never hardcodes
|
|
a server URL — it is entered on the first-run setup screen — so that move
|
|
needs no code change, only re-pointing each phone.
|
|
|
|
See `deploy/README.md` for running the server long-term, reaching it from
|
|
outside your home network, and backups.
|