ampersand-supabase ampersand-supabase/ARCHITECTURE.md
Version: 1.0 Status: Implementation-ready Last updated: 2026-03-28

[&] Ecosystem — Shared Supabase Architecture

Version: 1.0 Status: Implementation-ready Last updated: 2026-03-28

1) Design philosophy

One Supabase instance serves the entire [&] Protocol ecosystem. Each product gets its own PostgreSQL schema within a single database. This gives operators — whether running locally via supabase start or self-hosting on their own infrastructure — a single setup step for the full stack.

1.1 Goals

  • One command setup: supabase start spins up every product's data layer

  • One account everywhere: shared auth.users, one login across all ecosystem apps

  • Schema isolation: each product's tables live in a dedicated schema, no naming collisions

  • Independent RLS: each schema has its own Row-Level Security policies

  • Self-hosting friendly: an operator on their own domain routes subdomains to apps, all hitting one Supabase

  • Cost efficient: one Supabase Pro plan ($25/mo) instead of $200+/mo for separate projects

1.2 Non-goals

  • Cross-product-schema foreign keys (e.g., fleet.* must not FK to kag.*). Products MAY reference amp.workspaces and auth.users — those are the shared core, not cross-product coupling.

  • Forcing Supabase on products that don't need it (Graphonomous keeps SQLite, SpecPrompt/Deliberatic/OpenSentience have no storage)

2) Schema map

supabase (single instance)
│
├── auth.*              ← Supabase Auth (managed, do not modify)
├── vault.*             ← Supabase Vault for secrets
├── storage.*           ← Supabase Storage for file uploads
│
├── amp.*               ← Shared ecosystem core (profiles, workspaces, members)
├── kag.*               ← BendScript — knowledge graph editor
├── webhost.*           ← WebHost.Systems — hosting control plane
├── fleet.*             ← FleetPrompt — agent marketplace
├── geo.*               ← GeoFleetic — spatial intelligence
├── temporal.*          ← TickTickClock — temporal intelligence
├── orchestrate.*       ← AgenTroMatic — deliberation & reputation
├── govern.*            ← Delegatic — org governance
└── agentelic.*         ← Agentelic — agent lifecycle

2.1 Why amp.* for shared core (not public)

The public schema is Supabase's default and comes with implicit grants. Putting shared tables in amp.* makes the boundary explicit: ecosystem-shared tables live in amp, product-specific tables live in their own schema. This prevents accidental coupling and makes it clear what's shared infrastructure vs. product-specific.

BendScript's existing tables (currently in public) will migrate to kag.*. WebHost.Systems tables will go into webhost.*.

3) Shared core: amp schema

The amp schema holds identity, tenancy, and cross-product structures. Every product reads from amp for user/workspace context but writes only to its own schema for product data.

3.1 Tables

TablePurpose
amp.profilesUser profile (mirrors auth.users, auto-created on signup)
amp.workspacesMulti-tenant workspace container
amp.workspace_membersUser-workspace membership with roles
amp.product_entitlementsWhich products a workspace has access to

3.2 Workspace-based tenancy

All products use workspace-based multi-tenancy (not single-owner user_id). This is the pattern BendScript already uses and is more flexible than WebHost.Systems' current user_id-only model.

A user can belong to multiple workspaces. A workspace can use multiple products. RLS checks workspace membership via amp.workspace_members.

3.3 Product entitlements

-- Which products a workspace can access
amp.product_entitlements (
  workspace_id  uuid REFERENCES amp.workspaces(id),
  product       text NOT NULL,  -- 'kag', 'webhost', 'fleet', etc.
  plan          text NOT NULL DEFAULT 'free',
  enabled_at    timestamptz NOT NULL DEFAULT now(),
  PRIMARY KEY (workspace_id, product)
)

This lets an operator enable/disable products per workspace without touching product schemas.

3.4 Auth flow

User signs up (any app) ──→ auth.users row created
                          ──→ trigger creates amp.profiles row
                          ──→ trigger creates default amp.workspaces row
                          ──→ trigger creates amp.workspace_members (owner)
                          ──→ product entitlements added by application layer on first product access

3.5 Cross-app identity

Every product schema references auth.users(id) for user identity and amp.workspaces(id) for workspace context. The auth.uid() function works in RLS policies across all schemas.

4) Per-product schemas

Each product schema follows this pattern:

CREATE SCHEMA IF NOT EXISTS <product>;
GRANT USAGE ON SCHEMA <product> TO anon, authenticated, service_role;
-- Grant table-level permissions as tables are created

4.1 Schema ownership rules

  • Each product schema is self-contained — no foreign keys to other product schemas

  • Products MAY reference amp.workspaces(id) and auth.users(id) only

  • Products MUST NOT reference tables in other product schemas

  • Cross-product data sharing happens via application-level API calls, not SQL joins

4.2 RLS pattern

Every product uses the same workspace membership check from amp:

-- Reusable helper (defined once in amp schema)
CREATE FUNCTION amp.is_workspace_member(target_workspace_id uuid)
RETURNS boolean AS $$
  SELECT EXISTS (
    SELECT 1 FROM amp.workspace_members
    WHERE workspace_id = target_workspace_id
      AND user_id = auth.uid()
  );
$$ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = amp;

-- Each product uses it in RLS policies:
CREATE POLICY "fleet_agents_select"
ON fleet.agents FOR SELECT
USING (amp.is_workspace_member(workspace_id));

4.3 Product schema summaries

Migrations exist for schemas marked with [live]. Others are planned.

kag.* (BendScript) [live] — knowledge graphs, planes, nodes, edges, AI generations, API keys. Migrated from current public.* tables.

webhost.* (WebHost.Systems) [live] — agents, deployments, metrics_events, billing_usage, subscriptions, audit_log. Migrated from current Convex schema + spec.

fleet.* (FleetPrompt) [live] — publishers, agents, agent_versions, trust_scores, installs, categories, audit_events. Full-text + trigram search via pg_trgm.

geo.* (GeoFleetic) [live] — fleets, assets, geofences (PostGIS geometry), routes (LineString), geofence_events. PostGIS extension required.

temporal.* (TickTickClock) [live] — streams, data_points (range-partitioned via pg_partman), anomalies, forecasts, patterns. Replaces TimescaleDB with native partitioning for PG17 compatibility.

orchestrate.* (AgenTroMatic) [live] — agents, deliberations, bids, reputation_scores, traces.

govern.* (Delegatic) [live] — orgs (tree hierarchy), memberships, policies (monotonic inheritance), goal_refs, audit_events.

agentelic.* (Agentelic) [live] — agents, builds, test_runs, deployments (staged: staging → canary → production).

5) Extensions

All extensions are created once in migration 000_extensions.sql:

CREATE EXTENSION IF NOT EXISTS pgcrypto;      -- UUIDs, hashing
CREATE EXTENSION IF NOT EXISTS citext;        -- case-insensitive text
CREATE EXTENSION IF NOT EXISTS vector;        -- pgvector for embeddings (kag, fleet)
CREATE EXTENSION IF NOT EXISTS postgis;       -- spatial (geo)
CREATE EXTENSION IF NOT EXISTS pg_trgm;       -- trigram search (fleet)
CREATE EXTENSION IF NOT EXISTS pg_cron;       -- scheduled jobs (webhost, temporal)
CREATE EXTENSION IF NOT EXISTS pg_partman;    -- range partitioning (temporal)

6) Migration structure

All migrations live in one flat supabase/migrations/ directory, namespaced by number range:

supabase/migrations/
├── 000_extensions.sql              ← shared extensions
├── 001_amp_schema.sql              ← amp.* core tables
├── 002_amp_rls.sql                 ← amp.* RLS policies + helpers
├── 003_amp_triggers.sql            ← new-user bootstrap trigger
│
├── 010_kag_schema.sql              ← kag.* tables (BendScript)
├── 011_kag_rls.sql                 ← kag.* RLS
│
├── 020_webhost_schema.sql          ← webhost.* tables
├── 021_webhost_rls.sql             ← webhost.* RLS
├── 022_webhost_cron.sql            ← webhost.* pg_cron jobs
│
├── 030_fleet_schema.sql            ← fleet.* tables
├── 031_fleet_rls.sql               ← fleet.* RLS
│
├── 040_geo_schema.sql              ← geo.* tables (PostGIS)
├── 041_geo_rls.sql                 ← geo.* RLS
│
├── 050_temporal_schema.sql         ← temporal.* tables (pg_partman)
├── 051_temporal_rls.sql            ← temporal.* RLS
│
├── 060_orchestrate_schema.sql      ← orchestrate.* tables
├── 061_orchestrate_rls.sql         ← orchestrate.* RLS
│
├── 070_govern_schema.sql           ← govern.* tables
├── 071_govern_rls.sql              ← govern.* RLS
│
├── 080_agentelic_schema.sql        ← agentelic.* tables
└── 081_agentelic_rls.sql           ← agentelic.* RLS

Number ranges are reserved per product (000-009 shared, 010-019 kag, 020-029 webhost, etc.). Future migrations within a product increment within their range (e.g., 012_kag_add_templates.sql).

7) Authentication across apps

7.1 Shared auth, independent sessions (default)

One auth.users table. Each app initializes its own Supabase client with the same project URL/anon key. Sessions are browser-origin-scoped by default — logging into fleet.example.com does not log you into webhost.example.com.

7.2 SSO across subdomains (operator choice)

If all apps are on subdomains of one domain, the operator can enable shared sessions by setting cookieOptions.domain = ".example.com" in each app's Supabase client config. This is a deployment-time choice, not a schema concern.

7.3 Redirect URLs

Each app registers its auth callback in config.toml (local) or Supabase dashboard (hosted):

additional_redirect_urls = [
  "http://localhost:5173",          # BendScript dev
  "http://localhost:5174",          # WebHost dev
  "http://localhost:5175",          # FleetPrompt dev
  "https://bend.example.com/**",   # production
  "https://webhost.example.com/**",
  "https://fleet.example.com/**",
]

7.4 Email templates

Email templates MUST use {{ .RedirectTo }} instead of {{ .SiteURL }} so magic links and password resets route back to the originating app.

7.5 Elixir apps (JWT validation)

Elixir apps (FleetPrompt, GeoFleetic, etc.) validate Supabase JWTs using the JWKS endpoint:

GET https://<project>.supabase.co/auth/v1/.well-known/jwks.json

Verify with :jose or :joken. Extract sub (user UUID) for Ecto queries. Same UUID works across all schemas.

7.6 OAuth providers

Only one redirect URI needed per OAuth provider (Google, GitHub, etc.):

https://<project>.supabase.co/auth/v1/callback

Per-app routing happens via the redirectTo parameter on the Supabase→App leg.

8) config.toml

The shared config.toml exposes all product schemas via PostgREST:

[api]
schemas = ["amp", "kag", "webhost", "fleet", "geo", "temporal", "orchestrate", "govern", "agentelic"]

Each app's Supabase client can target a specific schema by setting the Accept-Profile header or using the .schema() method in the JS client:

// BendScript queries kag.* tables
const { data } = await supabase.schema('kag').from('nodes').select('*')

// WebHost queries webhost.* tables
const { data } = await supabase.schema('webhost').from('agents').select('*')

Elixir apps use Ecto's @schema_prefix:

defmodule Fleet.Agent do
  use Ecto.Schema
  @schema_prefix "fleet"

  schema "agents" do
    field :name, :string
    belongs_to :workspace, Amp.Workspace, type: :binary_id
  end
end

9) Deployment topologies

9.1 Local AIOS

supabase start
  → PostgreSQL with all schemas
  → Auth, Storage, Realtime, Edge Functions
  → Studio at localhost:54323 (browse all schemas)

Each app connects to localhost:54321 with local anon key

9.2 Self-hosted (operator's domain)

operator.example.com
├── supabase self-hosted (or Supabase cloud project)
├── bend.operator.example.com    → BendScript app    → kag.* schema
├── host.operator.example.com    → WebHost app        → webhost.* schema
├── fleet.operator.example.com   → FleetPrompt app    → fleet.* schema
└── ...

All apps share one Supabase URL. Cloudflare/nginx routes subdomains to app containers.

9.3 Official hosted ([&] cloud)

Same as self-hosted but on *.ampersand.box or similar. Products that aren't ready yet simply don't expose a subdomain — their schema exists but no app routes to it.

10) Migration path from current state

10.1 BendScript (public.* → kag.*)

BendScript's existing public.* tables move to kag.*:

  • public.graphskag.graphs

  • public.nodeskag.nodes

  • public.edgeskag.edges

  • etc.

The public.profiles, public.workspaces, public.workspace_members tables become amp.* (shared core). BendScript's existing workspace model is the template for the shared layer.

Migration strategy: ALTER TABLE public.foo SET SCHEMA kag; for each table, then update the app's Supabase client to use .schema('kag').

10.2 WebHost.Systems (Convex → webhost.*)

WebHost.Systems is converting from Convex to Supabase. Its tables go into webhost.* following the spec in WebHost.Systems/project_spec/spec_v1/30_DATA_MODEL_SUPABASE.md. The main change is replacing user_id-based ownership with workspace-based tenancy from amp.*.

10.3 Other products

FleetPrompt, GeoFleetic, TickTickClock, AgenTroMatic, Delegatic, and Agentelic are new — they get fresh schemas with no migration burden.

11) Products that do NOT use Supabase

ProductStorageReason
GraphonomousEmbedded SQLite + sqlite-vecLocal-first MCP server; network latency would defeat the purpose
SpecPromptGit-based specsCLI/format standard, no persistent service
DeliberaticIn-memory + Merkle chainsProtocol specification, not a data service
OpenSentienceETS (governance shim)Research protocols wrapping OTP supervision

These products can still authenticate users via Supabase JWT validation if they need identity context, but they do not store data in Supabase.

12) Security checklist

  • [ ] RLS enabled on every table in every exposed schema

  • [ ] No plaintext secrets in any table (use Supabase Vault)

  • [ ] workspace_id present on all tenant-owned tables for RLS

  • [ ] Shared RLS helpers in amp schema (SECURITY DEFINER, locked search_path)

  • [ ] Edge Functions use service_role for server-only mutations

  • [ ] Email templates use {{ .RedirectTo }} not {{ .SiteURL }}

  • [ ] CORS handled explicitly in Edge Functions

  • [ ] pg_cron jobs scoped to their product schema

  • [ ] No cross-schema foreign keys between products

Open in the interactive atlas