Supabase Enums: ALTER TYPE ADD VALUE in Migrations
Step‑by‑step guide to adding a PostgreSQL enum type and column in Supabase, including verification and common pitfalls.
You're building an e-commerce order flow — or retrofitting an existing orders table — and the status field should only ever hold "pending", "shipped" or "delivered". You write the migration, run it, and PostgreSQL answers:
ERROR: type "status_enum" does not exist
LINE 1: ALTER TABLE orders ADD COLUMN status status_enum;The error reproduces identically in the Supabase dashboard SQL editor, locally with psql, and in CI pipelines that run supabase db push.
ERROR: type "status_enum" does not exist — what PostgreSQL is telling you#
Supabase is a thin wrapper around PostgreSQL, and PostgreSQL does not allow you to declare a column as an enum without first defining the enum type itself. The CREATE TYPE … AS ENUM statement registers a new type in the database catalog. If that type does not exist at the moment you run ALTER TABLE … ADD COLUMN …, PostgreSQL raises the "type does not exist" error shown above. Supabase's UI only wraps this raw SQL — there is no dashboard shortcut that creates the type for you.
So the workflow is always two ordered operations: register the type, then attach it to a column.
The migration SQL: type first, column second#
Below is the minimal, production-ready set of SQL statements you can paste into the Supabase SQL editor or add to a migration file. The steps are shown split into two files — a common style preference for keeping type definitions separate from table changes, though both statements can also live in a single migration file.
Register the type:
-- migrations/20240602_create_status_enum.sql
CREATE TYPE public.status_enum AS ENUM ('pending', 'shipped', 'delivered');-- Expected output (psql)
CREATE TYPEThen attach the column:
-- migrations/20240602_add_status_to_orders.sql
ALTER TABLE public.orders
ADD COLUMN status status_enum NOT NULL DEFAULT 'pending';-- Expected output (psql)
ALTER TABLEIf you need to create the table from scratch, you can combine the enum creation and table definition in a single file, but keep the CREATE TYPE statement first:
-- migrations/20240602_create_orders_with_enum.sql
CREATE TYPE public.status_enum AS ENUM ('pending', 'shipped', 'delivered');
CREATE TABLE public.orders (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id),
status status_enum NOT NULL DEFAULT 'pending',
created_at timestamp with time zone DEFAULT now()
);-- Expected output (psql)
CREATE TYPE
CREATE TABLEIn the dashboard: open SQL editor, click New migration, run the CREATE TYPE file first, then the ALTER TABLE (or the combined file if you're starting fresh), and confirm the column appears in the Table editor with the enum values selectable.
Retrofitting an existing text column#
If the orders table already has a status column of type text, you can migrate it safely — the USING clause casts existing rows:
-- migrations/20240602_migrate_status_to_enum.sql
-- 1. Create the enum type (if not already present)
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'status_enum') THEN
CREATE TYPE public.status_enum AS ENUM ('pending', 'shipped', 'delivered');
END IF;
END$$;
-- 2. Convert existing text values to the enum
ALTER TABLE public.orders
ALTER COLUMN status TYPE status_enum
USING status::status_enum;
-- 3. Add a default if needed
ALTER TABLE public.orders
ALTER COLUMN status SET DEFAULT 'pending';-- Expected output (psql)
DO
ALTER TABLE
ALTER TABLETwo situations complicate the retrofit:
Rows contain values outside the allowed set. The cast will fail. Clean the data first, then run the ALTER COLUMN … TYPE statement:
UPDATE public.orders
SET status = 'pending'
WHERE status NOT IN ('pending', 'shipped', 'delivered');The enum already exists under a different name. If another developer created order_status instead of status_enum, point the column at the existing type instead of creating a duplicate:
ALTER TABLE public.orders
ALTER COLUMN status TYPE order_status
USING status::order_status;Checking the catalog: information_schema and pg_enum#
Confirm the migrations ran:
supabase migration list --linkedOr check directly with SQL:
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'orders';-- Expected output
column_name | data_type
-------------+-----------
id | uuid
user_id | uuid
status | USER-DEFINED
created_at | timestamp with time zoneThe data_type for status should be USER-DEFINED, which indicates it is using a custom enum type. To see the actual enum values:
SELECT enumlabel
FROM pg_enum
WHERE enumtypid = 'public.status_enum'::regtype
ORDER BY enumsortorder;-- Expected output
enumlabel
-----------
pending
shipped
deliveredIf you still see the original error, double-check that the enum type name matches exactly (status_enum) and that you ran the migrations in the correct order.
The one real transaction restriction: ALTER TYPE … ADD VALUE#
CREATE TYPE and ALTER TABLE can safely coexist in the same migration file and even the same transaction — the type is immediately visible within the session once created. The one genuine restriction sits elsewhere: per PostgreSQL's own ALTER TYPE docs, "if ALTER TYPE ... ADD VALUE (the form that adds a new value to an enum type) is executed inside a transaction block, the new value cannot be used until after the transaction has been committed." Extend the enum, commit, and only then write rows that use the new label.
This does not affect the initial creation workflow at all; it only bites when you later grow the enum. To avoid any ordering confusion in shared codebases, a common practice is to wrap CREATE TYPE in a DO block that checks for existence, as shown in the retrofit migration above.
Two further guardrails worth wiring in: a lint rule in your CI pipeline that scans migration files for CREATE TYPE without a preceding comment can catch accidental omissions early, and enabling Supabase's Database > Settings > Enable RLS with a simple audit trigger that logs any attempt to insert a value not in the enum gives you runtime safety while you're still iterating on the schema.
For a broader view on managing schema changes without downtime, see my guide on Zero‑Downtime Supabase Migrations and the related checklist in Supabase RLS Policy Design Patterns Beyond the Basics.
Further reading: Select the First Row per Group in Supabase Postgres. Also see Fix Supabase CHARINDEX does not exist (SQLSTATE 42883).
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Continue Reading
PostgreSQL SHOW TABLES / DESCRIBE TABLE (psql + Supabase)
Coming from MySQL you type `SHOW TABLES` or `DESCRIBE table` and PostgreSQL throws a syntax error — both are MySQL commands. The psql equivalents are `\dt` (list tables) and `\d table_name` (describe a table); the portable SQL equivalents are `information_schema.tables` and `information_schema.columns`. Here is exactly what to run in psql, the Supabase SQL editor, Drizzle, or any client, plus why your query returns zero rows.
Fix Supabase CHARINDEX does not exist (SQLSTATE 42883)
Repair the email-to-username expression without adding columns or hiding trigger failures, with a PostgreSQL reproduction and signature checks.
Extract date (yyyy/mm/dd) from a timestamp in PostgreSQL
Confused by extracting date from a timestamp in PostgreSQL? Learn how to cast to ::date, use date_trunc, and verify column types for straightforward table insertion.


