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;- 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
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.