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 = 'r'
    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 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
  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.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
),
chks AS (
  SELECT
    n.nspname                                              AS schema,
    c.relname                                              AS tbl,
    a.attname                                              AS col,
    string_agg(pg_get_expr(con.conbin, con.conrelid), ' AND ') AS expr
  FROM pg_constraint con
  JOIN pg_class c      ON c.oid = con.conrelid
  JOIN pg_namespace n  ON n.oid = c.relnamespace
  JOIN LATERAL unnest(con.conkey) AS k(attnum)            ON TRUE
  JOIN pg_attribute a  ON a.attrelid = c.oid AND a.attnum = k.attnum
  WHERE con.contype = 'c'
    AND array_length(con.conkey, 1) = 1
    AND n.nspname NOT LIKE 'pg_%'
    AND n.nspname NOT IN (SELECT s FROM excluded)
  GROUP BY n.nspname, c.relname, a.attname
),
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,
            'check',         (SELECT ch.expr FROM chks ch
                                WHERE ch.schema = t.schema
                                  AND ch.tbl    = t.tbl
                                  AND ch.col    = c.col)
          ) 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
                                ))
                                FROM fks f
                                WHERE f.schema = t.schema AND f.tbl = t.tbl)
      )
    )
    FROM tbls t
  ),
  'enums', (
    SELECT json_agg(json_build_object(
      'schema', e.schema,
      'name',   e.name,
      'values', e.vals
    ))
    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.