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
| Target | Entry process | Persistent data | Typical use |
|---|---|---|---|
| Vercel | Root server.js as @vercel/node serverless handler | Supabase Postgres when POSTGRES_URL / DATABASE_URL is set; otherwise ephemeral SQLite under /tmp (accounts disabled) | Primary hosted product |
| Local dev | node 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/Dockerfile | SQLite on mounted volume (e.g. /data/walls.sqlite) | Optional long-lived single-node deploy |
Vercel configuration is in vercel.json:
- Build:
npm ci, thennode scripts/prepare-studio.mjs(studio catalog). - Runtime: One Node build on
server.jswithpublic/**,server/**, andsharpbundled. - Routing: All paths
/(.*)→/server.js(API and HTML shell). Output Directory in the Vercel project must stay empty so the Node entry is not replaced by static-only hosting.
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 product | Used today? | Role |
|---|---|---|
| Auth | Yes (when configured) | Email/password signup, login, recovery, confirmation via REST (server/supabase-auth.js) |
| Postgres | Yes on Vercel (when integrated) | Durable store for users, sessions, boards, assets, votes |
| Storage | No | Uploaded wall images are stored as WebP bytes in the `assets` table, processed by Sharp in the app |
| RLS policies in repo | No | Database 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:
- On Vercel, prefers
DATABASE_URL, thenPOSTGRES_URL, thenPOSTGRES_URL_NON_POOLING, each normalized throughsessionPoolerUrl(). - Supabase transaction pooler URLs (
:6543onpooler.supabase.com) are rewritten to session pooler port `:5432` for serverless compatibility. - Local / Docker can use non-pooling URLs for CLI or direct connections; migrations for Postgres run from app boot via
PostgresStore.migrate().
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):
| URL | Serves |
|---|---|
/ | explore.html unless ?board= or ?auth= → index.html |
/wall | index.html (editor/viewer) |
/boards | boards.html |
Share links use /?board=<share_token> (opaque token on boards.share_token).
API surface (summary)
| Area | Methods | Module | Notes |
|---|---|---|---|
| Health / version | GET /api/health, GET /api/version | server | Health checks DB connectivity |
| Session / auth | GET /api/session, signup/login/logout, password reset, confirm, provider callback | accounts.js | HttpOnly session cookie; Supabase tokens stored server-side on sessions |
| Boards | GET/POST /api/boards, GET/PUT/DELETE /api/boards/:id | walls.js | Revision-aware saves; 409 on stale revision |
| Uploads | POST /api/boards/:id/assets, GET /api/assets/:id | uploads.js | Sharp validation; bytes in DB |
| Explore | GET /api/explore, vote/unvote on public boards | discover.js | Lists visibility = 'public' only |
| Static | GET allowlisted files | server | CSP and security headers on all responses |
Mutating request guards
Non-GET requests require:
- Allowed
Origin(configured origin or actual request host on Vercel), Sec-Fetch-Sitenotcross-site,- Header
X-Wall-Request: 1(set by client JS).
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:
- SQLite:
server/migrations/*.sqltracked in tablemigrations. - Postgres:
server/migrations/postgres/*.sqltracked inschema_migrations(advisory lock on migrate).
Core tables
| Table | Purpose | ||
|---|---|---|---|
users | App user id, email, optional supabase_id link to Supabase Auth user | ||
sessions | Hashed session token, expiry, provider (supabase / test adapters), optional Supabase refresh/access tokens, recovery flag | ||
boards | Wall record: owner, share_token, name, visibility (private \ | shared \ | public), JSON settings / playlist, monotonic revision |
board_items | Positioned items on 1200×800 canvas; typed columns + JSON content; optional asset_id | ||
assets | Image WebP bytes + dimensions; scoped to board_id | ||
board_members | Schema present for future sharing roles; not used for access control in current release | ||
board_votes | One row per (board_id, user_id) for explore voting | ||
rate_limits | App-level rate limiting keys |
Storage abstraction
openStore() in server/database.js returns either:
- `SqliteStore` — serialized writes via queue; WAL mode locally.
- `PostgresStore` —
postgresdriver,max: 1connection per serverless instance,prepare: false, migrations on boot.
Both stores expose the same async API (get, all, run, transaction) so domain modules stay storage-agnostic.
What is not in the database
- Guest drafts before signup: kept in browser storage (
guest.js) until claimed. - Catalog sprites under
public/assets/: shipped as static files, not per-user DB rows.
---
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
| Local | Vercel preview | Vercel production | Docker production | |
|---|---|---|---|---|
NODE_ENV | development | production | production | production |
| Database | SQLite default | Postgres if URL set; else /tmp SQLite | Postgres expected for accounts | SQLite on volume |
| Supabase Auth | Optional | Optional (guest mode can boot without) | Required for signup/login paths when requireSupabase applies | Required (requireSupabase when not on Vercel) |
APP_ORIGIN | Default http://127.0.0.1:4173 | VERCEL_URL or env override | APP_ORIGIN or VERCEL_PROJECT_PRODUCTION_URL | Must be HTTPS |
| Account persistence | Yes (SQLite) | Yes if Postgres URL present; else API 503 for account routes | Yes with integration | Yes (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)
| Variable | Used by | Notes |
|---|---|---|
APP_ORIGIN | Links, auth redirects, CORS-style origin checks | Share URLs use this on Vercel production |
SUPABASE_URL | Auth client | Also accepts NEXT_PUBLIC_SUPABASE_URL |
SUPABASE_PUBLISHABLE_KEY | Auth client | Also accepts anon/publishable NEXT_PUBLIC_* variants |
POSTGRES_URL | Postgres store | From Supabase integration on Vercel |
DATABASE_URL | Postgres store | Overrides pooler URL when set |
POSTGRES_URL_NON_POOLING | Postgres store | Fallback; still session-pooler-normalized on Vercel |
DATABASE_PATH | SQLite path | Local/Docker/e2e |
PORT, HOST | listenApp | Vercel sets PORT; no bind host on serverless |
VERCEL, VERCEL_URL, VERCEL_ENV | Origin + store selection | Set by platform |
ALLOW_LOCAL_AUTH_FOR_TESTS | Test-only password auth | Forbidden 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
- Version control: GitHub (
adamsloane/digitalwalls). - Deploy: Vercel Git integration (project settings +
vercel.json). - CI in repo: No
.github/workflowsin this checkout; validation is via local/ agent runs ofnpm run check,npm test, andnpm run test:e2e(Playwright).
---
5. Open questions (not decided in code/docs)
These are intentional TBDs for product/ops; the system above reflects what is implemented regardless.
| Topic | Current state | Decision still open |
|---|---|---|
| Row Level Security | App-layer checks only; server connects to Postgres with integration credentials | Whether to add Supabase RLS for defense-in-depth |
| Canonical production URL | APP_ORIGIN can be production domain or derived from Vercel URLs; auth accepts request host on signup | Single canonical domain vs preview URLs for Supabase redirect allowlist |
| CI scope | Tests exist but no mandatory GitHub Actions workflow | Unit-only vs full Playwright on every PR |
| Docker vs Vercel | Both supported in code | Whether Docker remains a first-class production target or self-host-only |
| Object storage | Images in Postgres/SQLite BLOBs | Whether to move uploads to Supabase Storage or another blob store at scale |
---
6. Related documents
| Document | Contents |
|---|---|
| BACKEND_OPERATIONS.md | Run locally, Vercel settings, backups, image limits, cookies |
| SUPABASE_AUTH.md | Email templates, redirect URLs, management scripts |
| FRONTEND_FLOWS.md | Editor, guest, recovery UX contracts |
| DATA_FLOWS.md | CRUD and authz per persisted entity |
| plans/2026-10-19-architecture-and-production-readiness-plan.md | Production-readiness phases and separate audit/standards docs |