SupaNet
Building on SupaNet

Data model

The tables, the enums, and the RLS rules that protect them.

SupaNet's behaviour is mostly defined by its database. Understanding the tables and, more importantly, the row-level security (RLS) rules is the key to working on it safely.

The tables

The schema lives in supabase/migrations. The main tables:

  • profiles, conversations, messages - users and chat.
  • artifacts, files - saved content and uploads.
  • documents, document_chunks - the PDF knowledge base (chunks live in pgvector).
  • collections, collection_artifacts, collection_files, collection_todos, collection_tables, collection_whiteboards, collection_card_boards, collection_terminology - named groups of content you can scope a chat to.
  • user_tables (and ut_* physical tables) - the real Postgres tables feature (Airtable-but-real-Postgres).
  • table_forms - public write-forms for anonymous submissions into user tables (contact forms, signup sheets, etc.).
  • todos - tasks and checklists.
  • whiteboards - Excalidraw canvases for planning and diagramming.
  • card_boards - free-form card walls for prioritizing ideas.
  • terminology - glossary of terms and definitions.
  • skills - both always-on prompts and on-demand skills.
  • tools, agents, guardrails - the configurable capabilities.
  • webhooks, webhook_events - inbound triggers and their log.
  • activity_log - the live feed of what the system did.
  • mcp_tokens - per-user tokens for the MCP server.
  • mcp_servers - connected external MCP endpoints (Zapier, etc.), each with its own token in Vault and cached toolset.
  • model_profiles - the orchestrator / utility model bindings.
  • integrations, inbox_messages - Vault-backed email config and the inbox.
  • usage_events - per-call token and cost accounting.
  • allowed_emails, invite_links - the invite allowlist and shareable invite links.
  • forged_functions - the audit/redeploy record for Forge.
  • vault_secrets - Vault-backed team secrets.

Enums include visibility (private / workspace / unlisted / public), message_role, and artifact_type.

RLS is the security boundary

This is the rule that matters most: RLS is the security boundary - never weaken it. The browser holds the public anon key, so anything not protected by a policy is effectively public.

The core patterns:

  • Owner-only tables (conversations, messages, files, profiles) scope rows to owner_id = auth.uid() (profiles use id = auth.uid()).
  • Artifacts are readable by the owner or when visibility <> 'private' - that is exactly how anonymous /share/a/:slug access works.
  • Storage for the files bucket is private; policies scope objects to the uploading user's folder.
  • Artifact images (artifact-images bucket) are private by default; RLS allows reading if the owner (by folder) or if the linked artifact is public/unlisted. Image privacy mirrors the artifact's visibility.
  • Knowledge sharing is separate from file visibility. Document chunks are readable by the owner or when the parent document's scope is workspace; only the owner can change the scope. The raw PDF stays owner-private regardless.
  • Collection visibility propagation: When a collection becomes workspace-visible, all private to-dos, links, whiteboards, card boards, tables, and inbox messages in it automatically become workspace-visible via triggers. Similarly, when an item is added to a workspace-visible collection, it automatically becomes workspace-visible (if it was private). This ensures items in a shared collection are always accessible to everyone who can see the collection. Artifacts and files are excluded from this propagation.

Invite-only signup

The first signup becomes the admin. After that, a BEFORE INSERT guard on auth.users rejects signups unless the email is in allowed_emails (admin managed). Admins have two methods to invite people in Settings → Invite people:

  1. By email: Add an email address directly, and that person can sign up with it.
  2. By shareable link: Create an opaque URL token to hand out (Slack, DM, etc.). When a recipient opens /join/:token, they can sign up with any email they choose. The redeem_invite_link RPC allowlists that email on the fly, so the same BEFORE INSERT guard still protects signups — the link just authorizes an email in the moment. Links can be set to expire after a date or after a max number of uses; admins can revoke or delete them anytime.

A trigger auto-creates a profiles row on signup.

When you change the schema

Update the migration, apply it, run npm run gen:types to regenerate the typed schema, and re-check the Supabase security advisors. Keeping database.types.ts in sync with the migrations is what keeps the front end type-safe.

Usage and cost

Every model call returns a usage object (tokens and cost). A shared helper writes one usage_events row per call from all four loops. The admin-only Usage page summarises spend and shows the OpenRouter balance. RLS on usage_events mirrors the activity log: own-or-admin read, service-role writes only.

On this page