Supabase Schema Visualizer

1. Open the SQL Editor in your Supabase project (left sidebar → SQL Editor) and run this query:

WITH excluded AS (
  SELECT unnest(ARRAY[
    'information_schema',
    'auth', 'storage', 'realtime', 'extensions', 'vault',
    'supabase_functions', 'pgbouncer', 'pgsodium',
    'graphql', 'graphql_public'
  ]) AS s
),
cols AS (
  SELECT
    n.nspname                                            AS schema,
    c.relname                                            AS tbl,
    a.attname                                            AS col,
    pg_catalog.format_type(a.atttypid, a.atttypmod)     AS typ,
    a.attnotnull                                         AS notnull,
    pg_get_expr(d.adbin, d.adrelid)                      AS dflt,
    a.attnum                                             AS pos,
    a.attidentity                                        AS identity_type,
    a.attgenerated                                       AS gen,
    col_description(c.oid, a.attnum)                     AS comment
  FROM pg_class c
  JOIN pg_namespace n    ON n.oid = c.relnamespace
  JOIN pg_attribute a    ON a.attrelid = c.oid
                        AND a.attnum > 0
                        AND NOT a.attisdropped
  LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = a.attnum
  WHERE c.relkind IN ('r', 'p')
    AND NOT c.relispartition
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
),
pks AS (
  SELECT
    n.nspname                                          AS schema,
    c.relname                                          AS tbl,
    array_agg(a.attname ORDER BY k.pos)               AS cols
  FROM pg_index ix
  JOIN pg_class c      ON c.oid = ix.indrelid
  JOIN pg_namespace n  ON n.oid = c.relnamespace
  JOIN LATERAL unnest(ix.indkey::smallint[])
       WITH ORDINALITY AS k(attnum, pos)              ON k.attnum > 0
  JOIN pg_attribute a  ON a.attrelid = c.oid
                       AND a.attnum = k.attnum::int2
  WHERE ix.indisprimary
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
  GROUP BY n.nspname, c.relname
),
uqs AS (
  SELECT
    n.nspname                                          AS schema,
    c.relname                                          AS tbl,
    array_agg(a.attname ORDER BY k.pos)               AS cols
  FROM pg_index ix
  JOIN pg_class c      ON c.oid = ix.indrelid
  JOIN pg_namespace n  ON n.oid = c.relnamespace
  JOIN LATERAL unnest(ix.indkey::smallint[])
       WITH ORDINALITY AS k(attnum, pos)              ON k.attnum > 0
  JOIN pg_attribute a  ON a.attrelid = c.oid
                       AND a.attnum = k.attnum::int2
  WHERE ix.indisunique AND NOT ix.indisprimary
    AND ix.indpred IS NULL
    AND ix.indexprs IS NULL
    AND EXISTS (SELECT 1 FROM pg_constraint cc
                WHERE cc.conindid = ix.indexrelid AND cc.contype = 'u')
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
  GROUP BY n.nspname, c.relname, ix.indexrelid
),
fks AS (
  SELECT
    n.nspname                                          AS schema,
    c.relname                                          AS tbl,
    array_agg(a.attname  ORDER BY k.pos)              AS cols,
    fn.nspname                                         AS ref_schema,
    fc.relname                                         AS ref_tbl,
    array_agg(fa.attname ORDER BY k.pos)              AS ref_cols,
    con.confdeltype                                    AS on_delete,
    con.confupdtype                                    AS on_update,
    con.conname                                        AS name,
    min(a.attnum) FILTER (WHERE k.pos = 1)             AS pos
  FROM pg_constraint con
  JOIN pg_class c      ON c.oid  = con.conrelid
  JOIN pg_namespace n  ON n.oid  = c.relnamespace
  JOIN pg_class fc     ON fc.oid = con.confrelid
  JOIN pg_namespace fn ON fn.oid = fc.relnamespace
  JOIN LATERAL unnest(con.conkey)
       WITH ORDINALITY AS k(attnum, pos)              ON TRUE
  JOIN LATERAL unnest(con.confkey)
       WITH ORDINALITY AS fk(attnum, pos)             ON fk.pos = k.pos
  JOIN pg_attribute a  ON a.attrelid  = c.oid  AND a.attnum  = k.attnum
  JOIN pg_attribute fa ON fa.attrelid = fc.oid AND fa.attnum = fk.attnum
  WHERE con.contype = 'f'
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
    AND fn.nspname NOT LIKE 'pg_%'
    AND fn.nspname NOT IN (SELECT s FROM excluded)
  GROUP BY n.nspname, c.relname, fn.nspname, fc.relname,
           con.confdeltype, con.confupdtype, con.conname, con.oid
),
enums AS (
  SELECT
    n.nspname                                              AS schema,
    t.typname                                              AS name,
    array_agg(e.enumlabel ORDER BY e.enumsortorder)       AS vals
  FROM pg_type t
  JOIN pg_namespace n ON n.oid = t.typnamespace
  JOIN pg_enum e      ON e.enumtypid = t.oid
  WHERE t.typtype = 'e'
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
  GROUP BY n.nspname, t.typname
),
tchks AS (
  SELECT
    n.nspname                                              AS schema,
    c.relname                                              AS tbl,
    con.conname                                            AS name,
    pg_get_expr(con.conbin, con.conrelid)                  AS expr
  FROM pg_constraint con
  JOIN pg_class c      ON c.oid = con.conrelid
  JOIN pg_namespace n  ON n.oid = c.relnamespace
  WHERE con.contype = 'c'
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
),
idxs AS (
  SELECT
    n.nspname                                          AS schema,
    c.relname                                          AS tbl,
    ic.relname                                         AS name,
    ix.indisunique                                     AS is_unique,
    array_agg(a.attname ORDER BY k.pos)               AS cols
  FROM pg_index ix
  JOIN pg_class c      ON c.oid = ix.indrelid
  JOIN pg_class ic     ON ic.oid = ix.indexrelid
  JOIN pg_namespace n  ON n.oid = c.relnamespace
  JOIN LATERAL unnest(ix.indkey::smallint[])
       WITH ORDINALITY AS k(attnum, pos)              ON k.attnum > 0
  JOIN pg_attribute a  ON a.attrelid = c.oid
                       AND a.attnum = k.attnum::int2
  WHERE NOT ix.indisprimary
    AND ix.indpred IS NULL
    AND ix.indexprs IS NULL
    AND NOT EXISTS (SELECT 1 FROM pg_constraint cc
                    WHERE cc.conindid = ix.indexrelid)
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
  GROUP BY n.nspname, c.relname, ic.relname, ix.indisunique, ix.indexrelid
),
tcoms AS (
  SELECT
    n.nspname                                          AS schema,
    c.relname                                          AS tbl,
    obj_description(c.oid, 'pg_class')                 AS comment
  FROM pg_class c
  JOIN pg_namespace n  ON n.oid = c.relnamespace
  WHERE c.relkind IN ('r', 'p')
    AND NOT c.relispartition
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
),
tbls AS (
  SELECT DISTINCT schema, tbl FROM cols ORDER BY schema, tbl
)
SELECT json_build_object(
  'tables', (
    SELECT json_agg(
      json_build_object(
        'schema',  t.schema,
        'name',    t.tbl,
        'columns', (
          SELECT json_agg(json_build_object(
            'name',          c.col,
            'type',          c.typ,
            'not_null',      c.notnull,
            'default',       c.dflt,
            'identity_type', c.identity_type,
            'generated',     c.gen,
            'comment',       c.comment
          ) ORDER BY c.pos)
          FROM cols c
          WHERE c.schema = t.schema AND c.tbl = t.tbl
        ),
        'primary_key',        (SELECT p.cols FROM pks p
                                WHERE p.schema = t.schema AND p.tbl = t.tbl),
        'unique_constraints', (SELECT json_agg(u.cols)
                                FROM uqs u
                                WHERE u.schema = t.schema AND u.tbl = t.tbl),
        'foreign_keys',       (SELECT json_agg(json_build_object(
                                  'columns',     f.cols,
                                  'ref_schema',  f.ref_schema,
                                  'ref_table',   f.ref_tbl,
                                  'ref_columns', f.ref_cols,
                                  'on_delete',   f.on_delete,
                                  'on_update',   f.on_update
                                ) ORDER BY
                                  CASE WHEN array_length(f.cols, 1) = 1
                                       THEN f.pos ELSE 32767 END,
                                  f.name)
                                FROM fks f
                                WHERE f.schema = t.schema AND f.tbl = t.tbl),
        'comment',            (SELECT tc.comment FROM tcoms tc
                                WHERE tc.schema = t.schema AND tc.tbl = t.tbl),
        'checks',             (SELECT json_agg(json_build_object(
                                  'name',       ch.name,
                                  'expression', ch.expr
                                ))
                                FROM tchks ch
                                WHERE ch.schema = t.schema AND ch.tbl = t.tbl),
        'indexes',            (SELECT json_agg(json_build_object(
                                  'name',    i.name,
                                  'columns', i.cols,
                                  'unique',  i.is_unique
                                ))
                                FROM idxs i
                                WHERE i.schema = t.schema AND i.tbl = t.tbl)
      )
    )
    FROM tbls t
  ),
  'enums', (
    SELECT json_agg(json_build_object(
      'schema', e.schema,
      'name',   e.name,
      'values', e.vals
    ) ORDER BY e.schema, e.name)
    FROM enums e
  )
) AS result;
  1. Copy the result (right-click the single JSON cell → Copy cell content), paste it below, and click Visualize.

Here's what the result might look like

VibeSchema PNG export of a Supabase database schema diagram

Why Supabase’s Built-In Visualizer Falls Short

Supabase’s built-in visual schema designer only shows one schema at a time. If you organize your database into different schemas, then you cannot see how those schemas relate to one another.

With VibeSchema you can view your full database, giving a complete overview of all your schemas and relationships between them.

Tables and enums are distinguished per schema with distinct header colours.

Privacy statement

VibeSchema never sees your Supabase credentials. The query runs entirely inside your Supabase SQL Editor. The result is a JSON description of your schema structure, no data or auth stuff.