Version: 1.0 Status: Implementation-ready Last updated: 2026-03-28
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.
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
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)
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
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.*.
amp schemaThe 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.
| Table | Purpose |
|---|---|
amp.profiles | User profile (mirrors auth.users, auto-created on signup) |
amp.workspaces | Multi-tenant workspace container |
amp.workspace_members | User-workspace membership with roles |
amp.product_entitlements | Which products a workspace has access to |
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.
-- 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.
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
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.
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
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
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));
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).
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)
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).
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.
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.
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/**",
]
Email templates MUST use {{ .RedirectTo }} instead of {{ .SiteURL }} so magic links and password resets route back to the originating app.
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.
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.
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
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
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.
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.
BendScript's existing public.* tables move to kag.*:
public.graphs → kag.graphs
public.nodes → kag.nodes
public.edges → kag.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').
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.*.
FleetPrompt, GeoFleetic, TickTickClock, AgenTroMatic, Delegatic, and Agentelic are new — they get fresh schemas with no migration burden.
| Product | Storage | Reason |
|---|---|---|
| Graphonomous | Embedded SQLite + sqlite-vec | Local-first MCP server; network latency would defeat the purpose |
| SpecPrompt | Git-based specs | CLI/format standard, no persistent service |
| Deliberatic | In-memory + Merkle chains | Protocol specification, not a data service |
| OpenSentience | ETS (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.
[ ] 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