Every event, profile and photo is now scoped to a signed-in account, so
separate people can track separate puppies on one server.
Server:
- users + sessions tables; bcrypt passwords; random session tokens stored
hashed and set as an HttpOnly cookie. Middleware gates /api/* behind a
valid session.
- register/login/logout/me endpoints. Registration requires a shared invite
code (-invite-code / PUPPY_INVITE_CODE); empty disables it.
- events, config and photos are keyed by user_id; the sync upsert guards
against cross-user overwrites and reads are scoped, so accounts are isolated.
Photos live under photos/<user_id>/ and are only served to their owner.
- in-place schema migration adds user_id and reshapes config; legacy
single-tenant data (including imported events.json) is parked ownerless and
adopted by the first account to register.
Client:
- login/register gate in front of the app; the tracker only boots once the
session check resolves. localStorage is namespaced per user.
- 401s bounce back to login; an offline reload falls back to the last cached
session so offline-first still works. Logout clears the session and reloads.
Deployment:
- module.nix gains inviteCodeFile (secret via EnvironmentFile) and
secureCookies options.
Verified end to end (curl + a headless-browser run of the auth flow):
isolation between accounts, invite enforcement, first-user adoption, photo
ownership, and session persistence across reload.