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 inpgvector).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(andut_*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- theorchestrator/utilitymodel 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 toowner_id = auth.uid()(profiles useid = auth.uid()). - Artifacts are readable by the owner or when
visibility <> 'private'- that is exactly how anonymous/share/a/:slugaccess works. - Storage for the
filesbucket is private; policies scope objects to the uploading user's folder. - Artifact images (
artifact-imagesbucket) 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 becomeworkspace-visible via triggers. Similarly, when an item is added to aworkspace-visible collection, it automatically becomesworkspace-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:
- By email: Add an email address directly, and that person can sign up with it.
- 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. Theredeem_invite_linkRPC 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.