Supasheet.

Database Schema

How Supasheet organises its schemas, the meta layer, and feature views

Schema Organisation

Supasheet uses PostgreSQL schemas to separate concerns. You build your application in your own schemas (one per domain) and let Supasheet introspect them through its supasheet meta schema.

SchemaOwned byPurpose
supasheetSupasheetMeta layer, RBAC, users, audit logs, comments, notifications, templates
publicYouDefault schema for general application tables
authSupabaseManaged by Supabase Auth — do not modify
storageSupabaseManaged by Supabase Storage — do not modify
<your schemas>YouOne schema per business domain (desk, crm, hr, blog, store, …)

Your Application Schemas

Create one schema per domain. The example seed files in supabase/examples/ follow this pattern (desk, crm, hr, finance, store, blog, inventory, manufacturing, procurement, quality, lms, hostel).

CREATE SCHEMA IF NOT EXISTS desk;

CREATE TABLE desk.tasks (
  id          UUID PRIMARY KEY DEFAULT extensions.uuid_generate_v4(),
  title       TEXT NOT NULL,
  status      TEXT NOT NULL DEFAULT 'pending',
  -- Always reference supasheet.users for user relationships
  user_id     UUID REFERENCES supasheet.users(id) ON DELETE CASCADE,
  created_at  TIMESTAMPTZ DEFAULT now(),
  updated_at  TIMESTAMPTZ DEFAULT now()
);

Always reference supasheet.users(id) instead of auth.users(id). See User Management for the reasoning.

The supasheet Schema

The internal schema that powers everything. Created by the migrations in supabase/migrations/.

Table / ViewDefined inPurpose
supasheet.users20250523000814_users.sqlUser profiles synced with auth.users
supasheet.user_roles20250523000822_roles.sqlUser ↔ role assignments
supasheet.role_permissions20250523000822_roles.sqlRole ↔ permission grants
supasheet.tables00000000000000_meta.sqlCached metadata for every accessible table
supasheet.columns00000000000000_meta.sqlCached metadata for every accessible column
supasheet.views00000000000000_meta.sqlCached metadata for every accessible view
supasheet.materialized_views00000000000000_meta.sqlCached metadata for materialised views
supasheet.audit_logs20250928062812_audit_logs.sqlAppend-only audit trail
supasheet.notifications20251006051303_notifications.sqlNotification payloads
supasheet.user_notifications20251006051303_notifications.sqlPer-user fan-out + read state
supasheet.comments20260514000001_comments.sqlPer-record threaded comments

Refreshing the Meta Layer

supasheet.tables, supasheet.columns, supasheet.views, and supasheet.materialized_views are materialized views — they're a cached snapshot of your schema, not a live introspection query. There are no event triggers keeping them in sync automatically. After any DDL change — CREATE/ALTER/DROP TABLE, VIEW, or MATERIALIZED VIEW, a schema change, an enum change (ALTER TYPE ... ADD VALUE), or a COMMENT ON ... — you must refresh them yourself:

select supasheet.refresh_metadata();

Forgetting this step is the most common cause of "my new table/column/comment metadata isn't showing up." Get in the habit of ending every migration with select supasheet.refresh_metadata();.

refresh_metadata() is defined in 00000000000000_meta.sql and simply re-runs REFRESH MATERIALIZED VIEW for all four meta views:

create or replace function supasheet.refresh_metadata () returns void as $$
BEGIN
    REFRESH MATERIALIZED VIEW supasheet.tables;
    REFRESH MATERIALIZED VIEW supasheet.columns;
    REFRESH MATERIALIZED VIEW supasheet.views;
    REFRESH MATERIALIZED VIEW supasheet.materialized_views;
END;
$$ LANGUAGE plpgsql;

EXECUTE on this function is revoked from anon/authenticated/service_role by default — only the database owner can call it. In practice that means running it from the Supabase SQL Editor, the CLI (supabase db reset / db push re-run all migrations including the final refresh materialized view calls in 00000000000000_meta.sql, so a fresh migration run is always in sync), or as the last statement in your own migration file.

Together with the permission-aware functions supasheet.get_schemas(), get_tables(), get_views(), get_columns(), and get_related_tables(), these cached meta views are how the UI discovers what to render.

Feature Views via JSON Comments

Dashboards, charts, reports, and templates are not separate tables — they are views whose comment identifies them by type. Supasheet ships discovery functions that filter supasheet.views for each type:

TypeDiscovery functionDocumented in
chartsupasheet.get_charts(p_schema)Charts
dashboard_widgetsupasheet.get_widgets(p_schema)Dashboards
reportsupasheet.get_reports(p_schema)Reports
templatesupasheet.get_templates(p_schema)Templates

Example

-- A pie chart of task statuses
CREATE VIEW desk.task_status_pie AS
SELECT
  status::text AS label,
  COUNT(*)::int AS value
FROM desk.tasks
GROUP BY status;

COMMENT ON VIEW desk.task_status_pie IS '{
  "type": "chart",
  "name": "Task Status",
  "description": "Current task status breakdown",
  "chart_type": "pie"
}';

REVOKE ALL ON desk.task_status_pie FROM authenticated, service_role;
GRANT SELECT ON desk.task_status_pie TO authenticated;

INSERT INTO supasheet.role_permissions (role, permission) VALUES
  ('user', 'desk.task_status_pie:select');

-- Required after creating the view and setting its comment
select supasheet.refresh_metadata();

Exposing Schemas via PostgREST

For Supasheet to query a schema through the publishable key, it must be exposed in supabase/config.toml:

[api]
schemas = ["public", "supasheet", "desk", "crm", "hr"]
extra_search_path = ["public", "extensions", "supasheet", "desk", "crm", "hr"]

After editing the config:

# Local
npx supabase stop && npx supabase start

# Cloud — set "Exposed schemas" in Project Settings → Data API

supasheet must be exposed for the platform to work. Application schemas only need to be exposed once you start adding tables / views in them.

Next Steps

On this page