Backend & hosting
WeaveForge separates domain logic (@weaveforge/core) from persistence, auth, and blob storage. The web app selects a backend provider at deploy time — same pattern as integrations.
Today the default is Supabase (managed Postgres + Auth + Storage). For larger orgs or self-hosting, you can target:
- Postgres + your own auth (Oracle Cloud free-tier VM, Neon, RDS, …)
- Cloudflare (Workers/Pages + Hyperdrive or D1 + R2 + Access)
Repository interfaces in @weaveforge/core are the swap boundary — not PostgREST query builders.
Architecture
packages/core/ apps/web/src/
├── features/*/domain/ ├── backend/ ← Postgres repos, auth
│ IPaperRepository, … │ config.ts ← NEXT_PUBLIC_BACKEND_PROVIDER
├── storage/ │ wire-backend.ts
│ IBlobStore ├── storage/ ← blobs (separate layer)
├── backend/ │ config.ts ← BLOB_PROVIDER, R2, tiering
│ IAuthService │ wire-storage.ts
│ ICurrentUserProvider │ providers/supabase|s3|tiered/
│ IAdminUserProvisioner └── integrations/ ← Zotero, GitLab, …
Flow
readBackendConfig()+readStorageConfig()read env.wireBackend()constructs repositories and auth; callswireStorage()for blobs.bootstrap.tswires use-cases + facades.- Feature UI calls facades only — never Supabase SDK or storage SDK.
What is already abstracted (~80%)
| Layer | Port | Supabase adapter |
|---|---|---|
| All entities | IPaperRepository, IProjectRepository, … (16+ in core) |
Supabase*Repository |
| Auth (browser) | IAuthService |
SupabaseAuthService |
| Session (repos) | ICurrentUserProvider |
SupabaseSessionProvider |
| Admin create-user | IAdminUserProvisioner |
SupabaseAdminUserProvisioner |
| Images | IBlobStore → PaperImageStore |
wireStorage() → SupabaseBlobStore (see storage/) |
What stays Postgres-specific (for now)
- SQL migrations in
supabase/migrations/—auth.uid(), RLS policies,SECURITY DEFINERhelpers - Supabase adapters use PostgREST (
.from().select().eq())
A postgres provider reuses the same schema and reimplements adapters with pg or an HTTP API — no use-case changes.
Configuration
# Backend provider (default: supabase)
NEXT_PUBLIC_BACKEND_PROVIDER=supabase # supabase | postgres
# Supabase (when provider = supabase)
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
SUPABASE_SERVICE_ROLE_KEY=eyJ... # server only — /api/admin/create-user
# Postgres (when provider = postgres — future)
DATABASE_URL=postgres://user:pass@host:5432/thesis
NEXT_PUBLIC_BACKEND_PROVIDER=postgres requires DATABASE_URL and selects the server-side adapter — see docs/backend/postgres-provider.md. Default remains supabase.
It is not the self-hosting switch, and setting it in a deployed app breaks the browser bundle: the client repositories reach the database over HTTP through PostgREST, which a Postgres connection string cannot replace. To move a deployed app onto your own database, set NEXT_PUBLIC_DATA_URL — docs/backend/oracle-shift-guide.md.
Default: Supabase
Best for solo researchers and small labs: free tier, magic-link auth, RLS, zero ops.
- Create a Supabase project.
- Apply migrations (
supabase db pushor SQL editor). - Set env vars above.
- Run
npm run dev.
User-facing setup: README §3–5.
Self-hosted Postgres (Oracle Cloud, VPS, Neon)
Goal: Keep the same schema and RLS model; replace Supabase Auth/PostgREST with your stack.
Steps to add a postgres provider
Host Postgres — apply all files in
supabase/migrations/(they are plain PostgreSQL;auth.usersbecomes your identity table or you add auserstable and adjust RLS).Auth — implement
IAuthService+ICurrentUserProvider:- Issue JWTs with
sub= user uuid. - Set
request.jwt.claim.subper connection (or replaceauth.uid()withcurrent_setting('app.user_id')in a migration fork).
- Issue JWTs with
Repositories — copy a
Supabase*Repository→Postgres*Repositoryusingpg/ Drizzle / Kysely. Same tables, same columns; enforceproject_idfilters in app code if you drop RLS.Blob store — implement
IBlobStoreunderapps/web/src/storage/providers/(S3/R2, tiered). Wired viawireStorage(), notwire-backend.tsdirectly.Admin provisioner — implement
IAdminUserProvisioner(create user +profilesrow).Wire — add
case "postgres":inwire-backend.ts; blob adapter inwire-storage.ts.Deploy — set
NEXT_PUBLIC_BACKEND_PROVIDER=postgresandDATABASE_URLfor server-side code. The browser needs a data API of its own; seeoracle-shift-guide.md.
Oracle Cloud free tier (sketch)
| Component | Suggestion |
|---|---|
| Compute | ARM VM (Always Free) — Docker Compose |
| Database | Postgres 16 on VM or Oracle Autonomous (paid) |
| Object storage | OCI Object Storage — IBlobStore |
| TLS | Caddy / nginx reverse proxy |
| App | next build + next start on VM, or container |
Auth: self-hosted Keycloak, Authentik, or simple JWT + bcrypt — wired through IAuthService.
Cloudflare (sketch)
| Component | Suggestion |
|---|---|
| Frontend | Cloudflare Pages — deploy Next.js (static + server functions) |
| Database | Hyperdrive → external Postgres (Neon, your OCI VM) or D1 (requires schema/RLS rework) |
| Blobs | R2 — implement IBlobStore with S3 API |
| Auth | Cloudflare Access + service token, or Auth0/Clerk as IAuthService |
| API routes | Workers for /api/admin/create-user with service binding to Hyperdrive |
Recommended path on Cloudflare: Hyperdrive + existing Postgres migrations + new Postgres*Repository adapters — avoids rewriting RLS in D1.
Workers constraint: browser cannot hold service-role keys; keep privileged ops in Worker routes behind IAdminUserProvisioner.
Adding a new backend provider (checklist)
| Step | Action |
|---|---|
| 1 | Add id to BackendProviderId in backend/config.ts |
| 2 | Create backend/providers/<name>/wire-<name>-backend.ts |
| 3 | Implement IAuthService, ICurrentUserProvider, IAdminUserProvisioner |
| 4 | Implement repository interfaces in wire-supabase-backend.ts |
| 5 | Implement blob adapters in storage/providers/ + wire-storage.ts |
| 6 | Add case in wire-backend.ts |
| 7 | Document env vars in .env.local.example |
| 8 | Run contract tests (in-memory + live integration) |
Do not abstract PostgREST per-table — one adapter class per repository is the right granularity.
Python SDK
python/weaveforge/container.py still uses Supabase directly. When the web postgres provider lands, mirror the same ports in Python (IExperimentRepository, etc.) and add a DATABASE_URL code path.
Testing
npm run build:core
npm test -w @weaveforge/core
npm test -w @weaveforge/web
npm run check:solid
Live Supabase contract tests: set WEAVEFORGE_SUPABASE_URL, WEAVEFORGE_SUPABASE_ANON_KEY, and either WEAVEFORGE_TOKEN (preferred) or legacy WEAVEFORGE_EMAIL / WEAVEFORGE_PASSWORD.
Related
storage/README.md— blob layer (R2 hot, OCI cold tiering)plans/completed/migration-plan.md— phased self-host plan- DESIGN.md — SOLID, composition root, repository contracts
- integrations.md — third-party services (Zotero, GitLab, …)
- dev.md — feature modules and facades