Appearance
Backend, API, images and access control
Source snapshot: 2026-10-03. This describes repository contracts and Compose configuration, not the running VPS. The companion hardening must be merged and deployed before its protections can be described as active in production.
Architecture
text
Android / iOS app
|
| HTTPS: data, actions, scan uploads
v
Deployment HTTPS entry / reverse proxy
+--> Fastify / Node / TypeScript API
| +--> PostgreSQL + pgvector
| | accounts, holdings, catalogue, pricing, vectors
| +--> Python embedding services (depending on scan path)
+--> Nginx --> data/images (read-only)
| card art, set logos, block logos
+--> tcg-vision (when exposed by the selected scan path)
DINOv2 + FAISS; catalogue enrichment via the API
External sources --> import workers --> PostgreSQL + data/images
scan reference constructionPublic routing depends on deployment configuration. Docker ports are not ports to expose publicly. PostgreSQL and internal services should not be reachable directly by phones. A service listed in Compose is not proof that it runs on the VPS or is used by a given mobile build.
Request flows and media
- Holdings: bearer token -> resolved account -> owner-filtered SQL -> response.
- Artwork: API returns metadata and URLs; the app downloads bytes from Nginx.
- Scan: photo -> configured recognition path -> matching references -> catalogue enrichment. Imports populate catalogue data separately from user requests.
Shared catalogue files live in data/images; card_images holds references. Set logos use set-logos, block logos use series-logos. Nginx serves them publicly with one-year immutable caching. The app uses React Native Image and currently prefers high-quality images in grids too; no dedicated managed disk cache or general prefetch layer was identified in this flow. Missing logos have a fallback. Some French One Piece artwork falls back to the official website when a local mirror is absent. Fixed UI assets remain in the app bundle.
Catalogue art is not a photograph owned by a collector. Ownership, quantities, purchase prices and the user's association with a card are private data. Private uploads must never be placed in the publicly served image directory. Scan Debug photos use a separate operator-protected API namespace; no dedicated personal collection-photo service is established by this architecture.
Authentication versus authorisation
User tokens contain 32 random bytes; only SHA-256 hashes are stored. Revoked tokens are rejected. A client-supplied account ID is not authentication. Tokens are currently long-lived opaque credentials; this hardening does not introduce expiry or automatic rotation without a mobile renewal flow.
| Surface | Boundary |
|---|---|
/me, /collection/**, /alerts/**, /push/** | User session plus object ownership |
| Catalogue mutations and Scan Debug | Separate operator credential |
| Catalogue and shared artwork | Public reads |
/public/collections/:slug | Explicit scoped, revocable share-link access |
| Email sign-in | Public initial proof, IP limits and verification attempt budget |
Collection and alert SQL binds the authenticated owner. Foreign collection items and missing items both return 404 for mutations. Explicit sharing does not expose private endpoints or fields excluded by its scope.
Companion hardening
The global early-request hook fails closed for private namespaces even if a new route forgets its local hook. Resolved identity is stored in an internal request-local WeakMap rather than a writable request property, and the next request checks the token again. Private/auth responses use private, no-store. HTTP credentials are redacted; operator audit labels contain a digest instead of a secret prefix. User-token format is bounded to its minted length. A conditional PostgreSQL upsert atomically enforces the email resend interval under concurrent requests. HTTP tests use two identities and SQL builder tests pin ownership predicates; these do not replace PostgreSQL integration tests or a production audit.
Open media-policy decision
"Only collection images" may mean personal photographs or shared catalogue art for owned cards. This change does not close public catalogue URLs while that meaning remains undecided. Private photographs require separate private storage, owner checks or short-lived signed delivery, and cross-account tests; that service is not implemented here.
Restricting shared catalogue art to holdings would also require closing direct Nginx delivery and changing mobile loading, Explorer, search, scan and shares. An API-only check cannot protect a bypassable public file URL. Already downloaded images cannot be made secret retroactively. Thumbnails, controlled mobile caching and versioned URLs are separate performance improvements, not part of this fix.
Deployment checks
Use two real accounts to test missing/revoked tokens, cross-account object IDs, share scope/revocation, replayed email codes and concurrent requests. Verify HTTPS, trusted proxy configuration, rate limits, private database connectivity and non-exposure of debug files. Production mail must use a real provider, not the development console transport which logs the email message.
Code references: backend src/api/src/auth.ts, collection/session.ts, collection/queries.ts, routes/authEmail.ts, docker/compose.yml, docker/nginx.conf; mobile src/data/remote/catalogApi.ts, src/ui/CardTile.tsx, src/components/SetLogo.ts.
References: OWASP object-level authorisation, OWASP authentication.