Digital Walls — system architecture (current state)

This document describes how the application is built and deployed today. It is not a product roadmap, security audit, or operations runbook. For deploy procedures and backups, see BACKEND_OPERATIONS.md. For auth setup, see SUPABASE_AUTH.md. For UX behavior, see FRONTEND_FLOWS.md. For create/read/update/delete per table, see DATA_FLOWS.md.

The aspirational data model in DATA_MODEL.md is older reference material; executable schema lives under server/migrations/ (SQLite) and server/migrations/postgres/ (Postgres).

---

1. Deploy topology

Hosting targets

TargetEntry processPersistent dataTypical use
VercelRoot server.js as @vercel/node serverless handlerSupabase Postgres when POSTGRES_URL / DATABASE_URL is set; otherwise ephemeral SQLite under /tmp (accounts disabled)Primary hosted product
Local devnode server.js (npm start)SQLite file data/walls.sqlite (override with DATABASE_PATH)Development and unit/integration tests
Docker (self-host)Same Node server in docker/DockerfileSQLite on mounted volume (e.g. /data/walls.sqlite)Optional long-lived single-node deploy

Vercel configuration is in vercel.json:

Browser assets live in `public/` (HTML, JS, CSS, static sprites). That separation keeps client bundles out of the serverless entry and allows the CDN to serve static files where configured. The Node handler still serves public/ for local and Docker runs.

There is no root Dockerfile for Vercel Git deploys (only docker/Dockerfile) to avoid accidental Container Registry deploys.

Supabase integration

Supabase productUsed today?Role
AuthYes (when configured)Email/password signup, login, recovery, confirmation via REST (server/supabase-auth.js)
PostgresYes on Vercel (when integrated)Durable store for users, sessions, boards, assets, votes
StorageNoUploaded wall images are stored as WebP bytes in the `assets` table, processed by Sharp in the app
RLS policies in repoNoDatabase access uses the server’s Postgres connection; authorization is enforced in application code

Connecting the Vercel Supabase integration typically sets SUPABASE_URL, publishable/anon key, and POSTGRES_URL. The app does not use a Supabase service-role key at runtime.

Postgres connection pooling

server/runtime-config.js resolves the database URL:

There is no Prisma in this repository; ignore Prisma-related env vars unless some external tooling sets them.

High-level topology

                    ┌─────────────────────────────────────┐
                    │           Browser                   │
                    │  public/*.js, explore.html, etc.    │
                    └─────────────────┬───────────────────┘
                                      │ HTTPS
                    ┌─────────────────▼───────────────────┐
                    │  Vercel (@vercel/node) or Node host │
                    │  server.js → createApp()            │
                    └─────┬───────────────────┬───────────┘
                          │                   │
              ┌───────────▼──────────┐   ┌────▼──────────────┐
              │ Supabase Auth (REST) │   │ PostgresStore or  │
              │ signup/login/reset   │   │ SqliteStore       │
              └──────────────────────┘   └────┬──────────────┘
                                              │
                                    Supabase Postgres (hosted)
                                    or local SQLite file

---

2. Request flow

Pages and routing

The HTTP server (server/server.js) handles:

1. **/api/* — JSON API (auth, boards, assets, explore, health). 2. /studio, /studio-library/* — Internal studio tooling (`server/studio.js`). 3. Static allowlist** — HTML/JS/CSS under public/, plus /assets/* and curated paths. Database paths, .env, and repo internals are not served.

HTML entry mapping (examples):

URLServes
/explore.html unless ?board= or ?auth= → index.html
/wallindex.html (editor/viewer)
/boardsboards.html

Share links use /?board=<share_token> (opaque token on boards.share_token).

API surface (summary)

AreaMethodsModuleNotes
Health / versionGET /api/health, GET /api/versionserverHealth checks DB connectivity
Session / authGET /api/session, signup/login/logout, password reset, confirm, provider callbackaccounts.jsHttpOnly session cookie; Supabase tokens stored server-side on sessions
BoardsGET/POST /api/boards, GET/PUT/DELETE /api/boards/:idwalls.jsRevision-aware saves; 409 on stale revision
UploadsPOST /api/boards/:id/assets, GET /api/assets/:iduploads.jsSharp validation; bytes in DB
ExploreGET /api/explore, vote/unvote on public boardsdiscover.jsLists visibility = 'public' only
StaticGET allowlisted filesserverCSP and security headers on all responses

Mutating request guards

Non-GET requests require:

Production responses add HSTS and a restrictive Content-Security-Policy.

Typical flows

Guest → account → save

Browser (guest.js local draft)
  → POST /api/signup (Supabase Auth + app user row)
  → Session cookie
  → PUT /api/boards/:id (claim/sync wall state)
  → Postgres or SQLite persists boards + assets

Owner edit with autosave

wall.js → PUT /api/boards/:id { revision, items, settings, ... }
  → walls.save() validates + transaction
  → revision increment or 409 Conflict

Public explore

explore.js → GET /api/explore?sort=votes|recent
  → boards where visibility = public
  → optional POST/DELETE vote (signed-in user only)

Shared link view (no account)

GET /?board=<share_token> → GET /api/boards/<token> (read)
  → walls.access() enforces private vs shared/public
  → GET /api/assets/:id for images when allowed

Authorization rules (private owner session vs link-based read) are implemented in `walls.js`, `uploads.js`, and `accounts.js`, not in Supabase RLS.

---

3. Data model (implemented)

Migrations apply automatically on startup:

Core tables

TablePurpose
usersApp user id, email, optional supabase_id link to Supabase Auth user
sessionsHashed session token, expiry, provider (supabase / test adapters), optional Supabase refresh/access tokens, recovery flag
boardsWall record: owner, share_token, name, visibility (private \shared \public), JSON settings / playlist, monotonic revision
board_itemsPositioned items on 1200×800 canvas; typed columns + JSON content; optional asset_id
assetsImage WebP bytes + dimensions; scoped to board_id
board_membersSchema present for future sharing roles; not used for access control in current release
board_votesOne row per (board_id, user_id) for explore voting
rate_limitsApp-level rate limiting keys

Storage abstraction

openStore() in server/database.js returns either:

Both stores expose the same async API (get, all, run, transaction) so domain modules stay storage-agnostic.

What is not in the database

---

4. Environments and configuration

Behavior is driven by resolveRuntimeConfig() (server/runtime-config.js) and environment variables. See .env.example for a template; the server does not auto-load .env files in production.

Runtime matrix

LocalVercel previewVercel productionDocker production
NODE_ENVdevelopmentproductionproductionproduction
DatabaseSQLite defaultPostgres if URL set; else /tmp SQLitePostgres expected for accountsSQLite on volume
Supabase AuthOptionalOptional (guest mode can boot without)Required for signup/login paths when requireSupabase appliesRequired (requireSupabase when not on Vercel)
APP_ORIGINDefault http://127.0.0.1:4173VERCEL_URL or env overrideAPP_ORIGIN or VERCEL_PROJECT_PRODUCTION_URLMust be HTTPS
Account persistenceYes (SQLite)Yes if Postgres URL present; else API 503 for account routesYes with integrationYes (SQLite)

On Vercel, if Postgres or Supabase is missing, the app still serves guest/explore static flows and logs warnings at boot; account-backed saves return 503 rather than writing to ephemeral disk.

Environment variables (meaningful today)

VariableUsed byNotes
APP_ORIGINLinks, auth redirects, CORS-style origin checksShare URLs use this on Vercel production
SUPABASE_URLAuth clientAlso accepts NEXT_PUBLIC_SUPABASE_URL
SUPABASE_PUBLISHABLE_KEYAuth clientAlso accepts anon/publishable NEXT_PUBLIC_* variants
POSTGRES_URLPostgres storeFrom Supabase integration on Vercel
DATABASE_URLPostgres storeOverrides pooler URL when set
POSTGRES_URL_NON_POOLINGPostgres storeFallback; still session-pooler-normalized on Vercel
DATABASE_PATHSQLite pathLocal/Docker/e2e
PORT, HOSTlistenAppVercel sets PORT; no bind host on serverless
VERCEL, VERCEL_URL, VERCEL_ENVOrigin + store selectionSet by platform
ALLOW_LOCAL_AUTH_FOR_TESTSTest-only password authForbidden when NODE_ENV=production

Scripts only (not runtime app): SUPABASE_ACCESS_TOKEN, SUPABASE_SERVICE_ROLE_KEY, test account vars — see AGENT_TEST_ACCOUNT.md.

Git and deployment

---

5. Open questions (not decided in code/docs)

These are intentional TBDs for product/ops; the system above reflects what is implemented regardless.

TopicCurrent stateDecision still open
Row Level SecurityApp-layer checks only; server connects to Postgres with integration credentialsWhether to add Supabase RLS for defense-in-depth
Canonical production URLAPP_ORIGIN can be production domain or derived from Vercel URLs; auth accepts request host on signupSingle canonical domain vs preview URLs for Supabase redirect allowlist
CI scopeTests exist but no mandatory GitHub Actions workflowUnit-only vs full Playwright on every PR
Docker vs VercelBoth supported in codeWhether Docker remains a first-class production target or self-host-only
Object storageImages in Postgres/SQLite BLOBsWhether to move uploads to Supabase Storage or another blob store at scale

---

6. Related documents

DocumentContents
BACKEND_OPERATIONS.mdRun locally, Vercel settings, backups, image limits, cookies
SUPABASE_AUTH.mdEmail templates, redirect URLs, management scripts
FRONTEND_FLOWS.mdEditor, guest, recovery UX contracts
DATA_FLOWS.mdCRUD and authz per persisted entity
plans/2026-10-19-architecture-and-production-readiness-plan.mdProduction-readiness phases and separate audit/standards docs