docs: the server is on the sprite and internet-exposed
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
This commit is contained in:
1 parent
26f76bf0f7
commit
1385ad286f
5 files changed
+149
-26
No files matched your search
@@ -124,6 +124,34 @@ that keystore).
|
|||||||
|
|
||||||
## Deploying the server
|
## Deploying the server
|
||||||
|
|
||||||
|
### Where this library's server actually runs today
|
||||||
|
|
||||||
|
The live instance runs **on the development sprite** and is **reachable from
|
||||||
|
the open internet** over HTTPS, on a sprite-issued hostname. That is a
|
||||||
|
deliberate interim choice — it is what the phones are pointed at — and it has
|
||||||
|
one consequence worth stating plainly, because the rest of this README was
|
||||||
|
written under the opposite assumption:
|
||||||
|
|
||||||
|
> **PocketBase's API rules are the only thing standing between this library
|
||||||
|
> and the internet.** There is no home NAT, no VPN, and no reverse proxy in
|
||||||
|
> front of it. The private-by-construction properties described above
|
||||||
|
> (`createRule = null` on `users`, every collection requiring a logged-in
|
||||||
|
> user, and the `pb_hooks` guard that turns an anonymous list into a 403
|
||||||
|
> rather than an empty 200) are load-bearing rather than defence-in-depth.
|
||||||
|
> Verify them against the *public* hostname, not `127.0.0.1` — localhost
|
||||||
|
> cannot tell you what the world can reach. `server/README.md` has the curls.
|
||||||
|
|
||||||
|
The sprite suspends when idle and wakes on an incoming request, so the first
|
||||||
|
call after a quiet period pays a cold start. The app is offline-first, so
|
||||||
|
this shows up as a slower sync rather than a failure.
|
||||||
|
|
||||||
|
Moving to home hardware (a NUC, an old laptop, a Pi) behind Tailscale is
|
||||||
|
still the intended end state, and the deploy docs below describe it. Nothing
|
||||||
|
in the app has to change to make that move: the server URL is entered on the
|
||||||
|
setup screen, never compiled in.
|
||||||
|
|
||||||
|
### Running it on your own hardware
|
||||||
|
|
||||||
See [`server/README.md`](server/README.md) for the schema and provisioning
|
See [`server/README.md`](server/README.md) for the schema and provisioning
|
||||||
scripts, and [`server/deploy/`](server/deploy/) for running PocketBase
|
scripts, and [`server/deploy/`](server/deploy/) for running PocketBase
|
||||||
long-term (systemd or Docker), reaching it from outside your home network
|
long-term (systemd or Docker), reaching it from outside your home network
|
||||||
@@ -153,17 +181,18 @@ meant to be, publicly distributed.
|
|||||||
|
|
||||||
Read this before assuming more polish than exists:
|
Read this before assuming more polish than exists:
|
||||||
|
|
||||||
- **The app has never run on a physical device or emulator.** This
|
- **No automated testing runs on a device.** The app itself has been
|
||||||
environment has no KVM, so there is no Android emulator available.
|
installed and used on a real phone since 2026-09-09, and several rounds of
|
||||||
Everything here was verified via `./gradlew assembleDebug`,
|
fixes came out of that; but this environment has no KVM, so there is no
|
||||||
`testDebugUnitTest` (JVM/Robolectric unit tests), and Paparazzi screenshot
|
emulator, and every *automated* check is a JVM one. Verification is
|
||||||
rendering (`src/test/snapshots/images/`) — real logic paths (ISBN
|
`./gradlew assembleDebug`, `testDebugUnitTest` (JVM/Robolectric unit
|
||||||
checksums, metadata merging, sync conflict resolution, DAO queries) are
|
tests), and Paparazzi screenshot rendering
|
||||||
unit-tested, and every screen has been rendered to a static PNG in both
|
(`src/test/snapshots/images/`) — real logic paths (ISBN checksums,
|
||||||
light and dark theme, but nothing has been tap-tested on an actual screen.
|
metadata merging, sync conflict resolution, DAO queries) are unit-tested,
|
||||||
Camera/barcode scanning in particular has only been exercised through unit
|
and every screen is rendered to a static PNG in both light and dark theme.
|
||||||
tests of the pure logic (`IsbnBarcodeAnalyzer`/`ScanCodeFilter`), never a
|
Camera/barcode scanning is covered only through unit tests of the pure
|
||||||
live camera.
|
logic (`IsbnBarcodeAnalyzer`/`ScanCodeFilter`); the live camera path is
|
||||||
|
exercised by hand on a phone or not at all.
|
||||||
- **Sync has been round-tripped against a real PocketBase exactly once**
|
- **Sync has been round-tripped against a real PocketBase exactly once**
|
||||||
(a live-server test covering auth, push with client-generated ids, pull,
|
(a live-server test covering auth, push with client-generated ids, pull,
|
||||||
last-write-wins, tombstones, and a byte-for-byte cover round-trip — see
|
last-write-wins, tombstones, and a byte-for-byte cover round-trip — see
|
||||||
|
|||||||
+59
-7
@@ -64,11 +64,60 @@ change it, write a NEW file and use that for the next wave.
|
|||||||
- Android SDK: `~/toolchain/android-sdk` (platforms;android-37.0, build-tools;37.0.0, platform-tools)
|
- Android SDK: `~/toolchain/android-sdk` (platforms;android-37.0, build-tools;37.0.0, platform-tools)
|
||||||
- **No KVM, no emulator.** Verify only via `./gradlew assembleDebug`, JVM unit tests, and
|
- **No KVM, no emulator.** Verify only via `./gradlew assembleDebug`, JVM unit tests, and
|
||||||
Paparazzi PNG rendering. Never claim the app was "run".
|
Paparazzi PNG rendering. Never claim the app was "run".
|
||||||
- PocketBase v0.40.2 service, **127.0.0.1:8090, deliberately NOT internet-exposed**
|
- PocketBase v0.40.2 service, **bound to `0.0.0.0:8090` and INTERNET-EXPOSED** via the
|
||||||
(no `--http-port`, so the sprite proxy can't reach it). Restart:
|
sprite proxy at **https://bookshelf-dev-b2jqx.sprites.app** (the service is registered
|
||||||
`sprite-env services restart pocketbase`. Logs: `/.sprite/logs/services/pocketbase.log`.
|
with `--http-port 8090`, so the proxy routes to it). This is the live server the user's
|
||||||
|
phone talks to. Restart: `sprite-env services restart pocketbase`.
|
||||||
|
Logs: `/.sprite/logs/services/pocketbase.log`. See "Where the server lives" below.
|
||||||
- Superuser creds: `server/.dev-credentials` (gitignored).
|
- Superuser creds: `server/.dev-credentials` (gitignored).
|
||||||
|
|
||||||
|
## Where the server lives — ANSWERED 2026-09-12: on this sprite, publicly
|
||||||
|
This was an open question through waves 1-7 and it now has an answer: **for now the
|
||||||
|
server stays here on the sprite and is reachable from the open internet.** The user's
|
||||||
|
phone talks to `https://bookshelf-dev-b2jqx.sprites.app`. Earlier notes in this file
|
||||||
|
and in `server/deploy/` said the opposite (localhost-only, unreachable from outside);
|
||||||
|
they were true when written and are now corrected in place. `server/deploy/` still
|
||||||
|
documents systemd/Docker/Tailscale for an eventual move to home hardware — that is a
|
||||||
|
future option, not what is running.
|
||||||
|
|
||||||
|
Consequences that were not true when the app was designed:
|
||||||
|
|
||||||
|
1. **PocketBase's own API rules are now the ONLY thing between this library and the
|
||||||
|
internet.** There is no NAT, no Tailscale, no reverse proxy in front of it. The
|
||||||
|
security curls in "Verification standard" below stopped being a formality the day
|
||||||
|
this changed — run them against the PUBLIC hostname, not `127.0.0.1`, because
|
||||||
|
localhost cannot tell you what the world can reach.
|
||||||
|
2. **The sprite auto-suspends when idle** and wakes on an incoming HTTP request
|
||||||
|
(that is what `--http-port` buys). So the server is still "often unreachable" in
|
||||||
|
the sense SPEC's offline-first rule cares about — just for a different reason than
|
||||||
|
residential NAT, and with a cold-start delay on the first request after a pause
|
||||||
|
rather than a hard failure. The size of that delay has **not** been measured; if a
|
||||||
|
sync or a first login ever looks pathologically slow, measure it before assuming a
|
||||||
|
bug in the app.
|
||||||
|
3. The URL is a sprite-scoped hostname. If this sprite is ever renamed or rebuilt the
|
||||||
|
URL changes, and every phone has to be re-pointed at it on the setup screen. That
|
||||||
|
is an argument for moving to Tailscale + real hardware eventually, not a reason to
|
||||||
|
hardcode anything.
|
||||||
|
|
||||||
|
### Posture re-verified against the PUBLIC hostname, 2026-09-12
|
||||||
|
| Check (anonymous, over the internet) | Result |
|
||||||
|
|---|---|
|
||||||
|
| `GET /api/collections/books/records` | **403** `{"message":"Authentication required."}` |
|
||||||
|
| `GET /api/collections/shelves/records` | **403** |
|
||||||
|
| `GET /api/collections/bookcases/records` | **403** |
|
||||||
|
| `POST /api/collections/users/records` (self-registration) | **403** |
|
||||||
|
| `GET /api/health` | 200 (intended — it is a health check and leaks nothing) |
|
||||||
|
| `GET /api/collections/users/records` | **200 `{"items":[]}`** — see below |
|
||||||
|
|
||||||
|
**Known gap, not a leak:** `users` LIST answers **200 with an empty array** instead of
|
||||||
|
403. No data escapes — there are two real accounts in the DB and an anonymous caller
|
||||||
|
sees neither, because `listRule` filters the rows out — but the status code is wrong
|
||||||
|
and it is the exact PocketBase quirk `pb_hooks/main.pb.js` exists to paper over. That
|
||||||
|
hook lists only `bookcases`, `shelves`, `books`; `users` was never added to it. One
|
||||||
|
word of a fix, and worth doing now that the collection is world-reachable: some
|
||||||
|
clients read "200 with []" as an allowed request. **Not yet done — do not record it as
|
||||||
|
done until the curl above returns 403.**
|
||||||
|
|
||||||
## STATE: what is DONE
|
## STATE: what is DONE
|
||||||
### Wave 1A — server: COMPLETE and verified by the orchestrator (not just self-reported)
|
### Wave 1A — server: COMPLETE and verified by the orchestrator (not just self-reported)
|
||||||
`server/` contains `setup-schema.sh` (idempotent), `create-user.sh`, `pb_hooks/main.pb.js`,
|
`server/` contains `setup-schema.sh` (idempotent), `create-user.sh`, `pb_hooks/main.pb.js`,
|
||||||
@@ -158,7 +207,8 @@ Workers self-report optimistically. Before accepting any wave:
|
|||||||
the user, not something to paper over — the user explicitly cares how this looks.
|
the user, not something to paper over — the user explicitly cares how this looks.
|
||||||
|
|
||||||
## Open questions for the user (not yet asked — deferred, not forgotten)
|
## Open questions for the user (not yet asked — deferred, not forgotten)
|
||||||
- Where the server will actually live (home box vs a sprite) — only affects the deploy README.
|
- ~~Where the server will actually live (home box vs a sprite)~~ — **ANSWERED 2026-09-12:
|
||||||
|
it stays on this sprite, internet-exposed. See "Where the server lives" above.**
|
||||||
- Their two account emails, for `create-user.sh`. Not needed until the app can log in.
|
- Their two account emails, for `create-user.sh`. Not needed until the app can log in.
|
||||||
|
|
||||||
## HAZARD #5 — THE SPRITE AUTO-SUSPENDS; detached workers do NOT keep it awake
|
## HAZARD #5 — THE SPRITE AUTO-SUSPENDS; detached workers do NOT keep it awake
|
||||||
@@ -283,8 +333,9 @@ library, scaffold — each light + dark).
|
|||||||
3. **R8 is off.** Acceptable per F3's prompt, but the release APK is 41.8MB.
|
3. **R8 is off.** Acceptable per F3's prompt, but the release APK is 41.8MB.
|
||||||
Turning it on requires proving Room/Retrofit/kotlinx-serialization/ML Kit
|
Turning it on requires proving Room/Retrofit/kotlinx-serialization/ML Kit
|
||||||
survive minification.
|
survive minification.
|
||||||
4. Still-open user questions, unchanged: where the server will actually live, and
|
4. Still-open user questions: the two account emails for `create-user.sh`.
|
||||||
the two account emails for `create-user.sh`.
|
(Where the server will live was ANSWERED on 2026-09-12 — it stays on this
|
||||||
|
sprite, internet-exposed. See "Where the server lives" near the top.)
|
||||||
|
|
||||||
## First on-device test — 2026-09-09
|
## First on-device test — 2026-09-09
|
||||||
The user installed the signed APK on a real phone. It runs. This closes the
|
The user installed the signed APK on a real phone. It runs. This closes the
|
||||||
@@ -372,7 +423,8 @@ that.
|
|||||||
**Still open, unchanged:** the free Google Books API key (keyless returns 429;
|
**Still open, unchanged:** the free Google Books API key (keyless returns 429;
|
||||||
worth doing on its own merits but no longer the leading theory), R8 still off so
|
worth doing on its own merits but no longer the leading theory), R8 still off so
|
||||||
the release APK is 41.8MB and too large to send over the file channel (30MB cap),
|
the release APK is 41.8MB and too large to send over the file channel (30MB cap),
|
||||||
where the server will live, and the two account emails for `create-user.sh`.
|
and the two account emails for `create-user.sh`. (Where the server will live
|
||||||
|
was ANSWERED on 2026-09-12: it stays on this sprite, internet-exposed.)
|
||||||
|
|
||||||
## Wave 5 — G-diagnostics: COMPLETE, verified by the orchestrator 2026-09-09
|
## Wave 5 — G-diagnostics: COMPLETE, verified by the orchestrator 2026-09-09
|
||||||
Commit `93f972b`. The app now distinguishes barcode-didn't-decode from
|
Commit `93f972b`. The app now distinguishes barcode-didn't-decode from
|
||||||
|
|||||||
+8
-4
@@ -4,10 +4,14 @@ Two-person shared home library. Android app + self-hosted PocketBase.
|
|||||||
ALL workers must follow this exactly. Do not invent alternative names.
|
ALL workers must follow this exactly. Do not invent alternative names.
|
||||||
|
|
||||||
## Non-negotiables
|
## Non-negotiables
|
||||||
- Offline-first. Home server is often unreachable (residential NAT). Every read
|
- Offline-first. The server is often slow or unreachable. Every read comes from
|
||||||
comes from Room. Every write lands in Room first, syncs later. No screen may
|
Room. Every write lands in Room first, syncs later. No screen may block on
|
||||||
block on network.
|
network. (The original reason was residential NAT; the server currently runs
|
||||||
- Private. No public registration. Auth required for all data access.
|
on a sprite that suspends when idle and wakes on request, so the same rule
|
||||||
|
holds for a different reason. The requirement does not depend on which.)
|
||||||
|
- Private. No public registration. Auth required for all data access. As of
|
||||||
|
2026-09-12 the server is INTERNET-EXPOSED, so these rules are the only thing
|
||||||
|
protecting the library — not defence-in-depth behind a home NAT.
|
||||||
- Server URL is NOT hardcoded; user enters it on first run.
|
- Server URL is NOT hardcoded; user enters it on first run.
|
||||||
|
|
||||||
## Repo layout
|
## Repo layout
|
||||||
|
|||||||
+33
-4
@@ -66,16 +66,45 @@ no recompilation, no framework — so it ships and deploys exactly like
|
|||||||
|
|
||||||
## Verifying a deployment
|
## 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
|
```sh
|
||||||
# Anonymous requests must be rejected (4xx) for all three collections:
|
BASE=https://your-server.example # the PUBLIC base URL, not localhost
|
||||||
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8090/api/collections/books/records
|
|
||||||
# -> 403
|
# 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:
|
# Self-registration must be rejected:
|
||||||
curl -s -X POST http://127.0.0.1:8090/api/collections/users/records \
|
curl -s -X POST "$BASE/api/collections/users/records" \
|
||||||
-H 'Content-Type: application/json' -d '{"email":"x@x.com","password":"password123","passwordConfirm":"password123"}'
|
-H 'Content-Type: application/json' -d '{"email":"x@x.com","password":"password123","passwordConfirm":"password123"}'
|
||||||
# -> 403 "Only superusers can perform this action."
|
# -> 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
|
See `deploy/README.md` for running the server long-term, reaching it from
|
||||||
outside your home network, and backups.
|
outside your home network, and backups.
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
# Deploying Bookshelf's PocketBase server
|
# Deploying Bookshelf's PocketBase server
|
||||||
|
|
||||||
|
> **This is not what is running today.** The live server currently runs on the
|
||||||
|
> development sprite, bound to `0.0.0.0:8090` and published over HTTPS on a
|
||||||
|
> sprite-issued hostname — **internet-exposed**, with PocketBase's API rules as
|
||||||
|
> its only protection (see `../README.md` § "Where the live server runs"). This
|
||||||
|
> document describes the *intended* end state: your own hardware at home, not
|
||||||
|
> directly exposed. Where it says PocketBase is bound to localhost and
|
||||||
|
> unreachable from outside, that is a statement about the deployment described
|
||||||
|
> here, not about the instance the phones are currently pointed at.
|
||||||
|
|
||||||
This assumes a spare always-on machine at home (a mini PC, NUC, Raspberry Pi,
|
This assumes a spare always-on machine at home (a mini PC, NUC, Raspberry Pi,
|
||||||
or an old laptop) running Linux. Pick **one** of the two run methods below —
|
or an old laptop) running Linux. Pick **one** of the two run methods below —
|
||||||
systemd or Docker — not both.
|
systemd or Docker — not both.
|
||||||
|
|||||||
Reference in new issue
Block a user