Data object flows

Per-entity create → read → update → delete behavior for data the app persists today. This complements:

Authorization is enforced in Node (accounts.js, walls.js, uploads.js, discover.js), not Supabase RLS.

Legend: Actor = who can trigger the flow. API = HTTP entry points. Cascades follow foreign keys in server/migrations/.

---

Guest draft (browser only)

Not a database table. Stored in localStorage under keys managed by public/guest.js.

OperationActorBehavior
Create / updateAnonymous visitorEdit wall in browser; draft JSON kept locally (digital-walls-guest-drafts-v1).
ReadSame deviceReload restores draft from localStorage.
DeleteUser or appCleared after successful claim, or when draft is blank and discarded.
Claim → accountSigned-in ownerclaimGuestDraft() creates a server board (POST /api/boards), uploads embedded images (POST /api/boards/:id/assets), then saves state (PUT /api/boards/:id). Intent stored in digital-walls-guest-claim-v1.

See FRONTEND_FLOWS.md for retention during signup/login failure.

---

users

App account row. Password proof for production users lives in Supabase Auth; password_hash is empty when supabase_id is set.

OperationActorAPI / triggerDB effect
CreateNew signupPOST /api/signup → Supabase signup → establishProviderSessionINSERT users with new UUID, email, supabase_id, empty password_hash
Create (link)Existing email, no supabase_idSame flow after Supabase confirmsUPDATE users SET supabase_id=… on matching email
Create (tests only)Local test serverPOST /api/signup with ALLOW_LOCAL_AUTH_FOR_TESTS=1INSERT with scrypt password_hash, no supabase_id
ReadSession holderGET /api/session, current() on other routesSELECT id, email (via session + Supabase user match)
Update—No profile APIEmail changes would require Supabase dashboard / future feature
Delete—Not exposed in UIDELETE users cascades: sessions, boards (and their items, assets, votes), board_members, board_votes

Rules: Supabase identity must have email_confirmed_at before app user is linked (providerAccount). Duplicate email with different supabase_id → 409. Account routes call requireDurable() → 503 on Vercel without Postgres.

---

sessions

Server-side login for the browser cookie. Stores hashed app session token and (for Supabase) provider tokens.

OperationActorAPI / triggerDB effect
CreateAfter successful authSignup/login, POST /api/auth/confirm, POST /api/auth/session, recovery confirmINSERT sessions with token_hash, 30-day expires_at, provider, optional Supabase tokens, recovery flag
ReadAny request with cookiesession(req)SELECT by token_hash where not expired
UpdateAutomaticSupabase access token near expiry in current()UPDATE sessions refresh tokens on same row
DeleteUserPOST /api/logoutDELETE row for current cookie; Supabase logout best-effort
DeletePassword changePUT /api/password after recoveryDELETE FROM sessions WHERE user_id=? (all devices)
DeleteInvalid Supabase tokencurrent() on 401/403 from AuthDELETE current session row
Delete (tests)LoginLocal auth pathExpired rows purged; new session inserted

Cookie: __Host-dw_session (production) or dw_session (dev), HttpOnly, SameSite=Lax, 30 days.

Read path for “who is logged in”: cookie → session row → (Supabase) refresh if needed → getUser → match users.id + supabase_id.

---

boards

Owned wall (board). Identified internally by id; shared by permanent share_token in URLs.

OperationActorAPIDB effect
CreateOwnerPOST /api/boardsINSERT boards — default name, private, revision 1, default settings/playlist JSON; new share_token
Read (list)OwnerGET /api/boardsSELECT metadata for owner_id
Read (one)Owner, or visitor with linkGET /api/boards/:idOrShareTokenFull serialize: settings, playlist, items; canEdit / mode from ownership
UpdateOwner onlyPUT /api/boards/:idTransaction: revision check → replace all board_items → update board fields → revision+1; stale revision → 409
DeleteOwnerDELETE /api/boards/:idTransaction: delete items, delete board (assets and votes cascade via FK)

Access matrix (`walls.access`):

visibilityOwnerAnonymous / other user
privateread + editno access (403)
sharedread + editread via id or share token
publicread + editread via id or share token; also listed on Explore

Limits: 1000 boards per owner; create rate limit per user. share_token and board id are stable across renames and visibility changes.

Nested data (not separate tables): settings (wallConfig), playlist — updated only through board PUT, validated in validateState().

---

board_items

Canvas objects on a 1200×800 wall. No standalone CRUD API.

OperationActorAPIDB effect
CreateOwnerPart of PUT /api/boards/:idRows inserted after full delete of prior items for that board
ReadAnyone who can read boardGET /api/boards/:idSELECT ordered by z_index, id; merged into item JSON
UpdateOwnerSame PUTFull replace: DELETE all items for board, re-INSERT from payload
DeleteOwnerOmit from save payload on PUTRow not re-inserted; orphaned assets GC’d (see assets)

Rules: Each item’s asset_id must reference an assets row on the same board. Item content JSON holds type-specific fields (text, folder pages, playlist UI state, etc.). Types enforced by DB check constraint (includes living on migrated DBs).

---

assets

WebP image bytes stored in the database.

OperationActorAPIDB effect
CreateOwnerPOST /api/boards/:id/assets (raw body, ≤10 MB)Sharp decode/resize → INSERT assets
ReadBoard readerGET /api/assets/:idSELECT bytes; preceded by walls.access(board_id)
Update—No in-place updateReplace by new upload + item reference on save
DeleteOwner (indirect)Board PUT GC, or board DELETEGC: delete assets on board with no referencing board_items and created_at older than 1 hour; board delete cascades all assets

Limits: 100 MB total WebP per board; 60 uploads/user/hour (rate limit). Private board images require owner session; shared/public allow read when board is readable.

---

board_votes

Explore upvotes — at most one row per (board_id, user_id).

OperationActorAPIDB effect
CreateSigned-in userPOST /api/explore/:boardId/voteINSERT if board visibility = public and not already voted
ReadAnyoneGET /api/exploreAggregated vote_count and voted flag per public board
Update—N/A—
DeleteVoterDELETE /api/explore/:boardId/voteDELETE row for user+board
Delete (cascade)—Owner deletes board or user deletedFK cascade removes votes

Rules: Voting on non-public boards → 404. Duplicate vote → 409. Explore list only includes visibility = 'public'.

---

board_members

Schema only for future sharing roles.

OperationActorAPIDB effect
Any—None in appNo reads or writes from application code

Editing remains owner-only. See BACKEND_OPERATIONS.md.

---

rate_limits

Ephemeral counters for abuse protection (not user-facing).

OperationActorTriggerDB effect
Create / updateServerBefore auth, upload, vote, create, etc.Upsert row: increment count, set resets_at; expired keys deleted in same transaction
ReadServerAfter incrementCompare count to caller’s max
DeleteServerExpiry sweep on each hit()DELETE WHERE resets_at < now

Keys are opaque strings chosen per action (e.g. auth:{ip}, upload:{userId}, vote:{userId}). Exceeding max → 429.

---

migrations / schema_migrations

Bookkeeping for applied SQL files — not business data.

StoreTableWritten when
SQLitemigrationsEach file in server/migrations/*.sql applied at boot
Postgresschema_migrationsEach file in server/migrations/postgres/*.sql inside advisory-locked transaction

No API. See ARCHITECTURE.md §3.

---

End-to-end diagrams

Owner save (items + board metadata)

PUT /api/boards/:id  { revision, visibility, state }
  → walls.access(edit)
  → revision match?
  → DELETE board_items (board_id)
  → INSERT board_items (each item, optional asset_id)
  → DELETE orphaned assets (>1h, unreferenced)
  → UPDATE boards (name, visibility, settings, playlist, revision++)

Share link read

GET /?board=:share_token
  → GET /api/boards/:share_token
  → walls.access(read) — private blocks non-owner
  → GET /api/assets/:id for each referenced image
  → walls.access on asset.board_id

Public explore + vote

GET /api/explore?sort=votes|recent
  → SELECT boards WHERE visibility = 'public'

POST /api/explore/:boardId/vote  (session required)
  → assert public
  → INSERT board_votes

---

Coverage notes

TopicDocumented hereAlso see
Supabase email confirm / recoverySession create paths onlySUPABASE_AUTH.md
Editor autosave / 409 UXBoard updateFRONTEND_FLOWS.md
Aspirational entities (NFC table, share_links, etc.)Not implementedDATA_MODEL.md