Supabase uses PostgreSQL's native jsonb column type to store JSON data. Create a column with the jsonb type, insert JSON objects directly using the Supabase JS client, and query nested fields with the -> and ->> operators in SQL or the arrow syntax in the JS client. Index frequently queried JSON paths with GIN indexes for fast lookups.
Storing and Querying JSON Data in Supabase
PostgreSQL's jsonb type lets you store structured JSON data alongside relational columns. This is useful for flexible schemas like user preferences, metadata, form responses, or any data that varies in structure between rows. This tutorial covers creating jsonb columns, inserting JSON data, querying nested fields, and adding indexes for performance.
Prerequisites
- A Supabase project with access to the SQL Editor
- Basic understanding of JSON structure
- @supabase/supabase-js installed in your project
Step-by-step guide
Create a table with a jsonb column
Create a table with a jsonb column
Use the SQL Editor in the Supabase Dashboard to create a table with a jsonb column. The jsonb type stores JSON in a binary format that supports indexing and efficient querying. Always prefer jsonb over json — it is faster for reads, supports indexing, and removes duplicate keys. You can also add a jsonb column to an existing table with ALTER TABLE.
1-- Create a new table with a jsonb column2create table public.products (3 id bigint generated always as identity primary key,4 name text not null,5 metadata jsonb default '{}',6 created_at timestamptz default now()7);89-- Enable RLS (required for any table accessed via the API)10alter table public.products enable row level security;1112-- Allow authenticated users to read products13create policy "Authenticated users can read products"14 on public.products for select15 to authenticated16 using (true);Expected result: A products table with a jsonb metadata column is created with RLS enabled.
Insert JSON data using the Supabase JS client
Insert JSON data using the Supabase JS client
Pass a JavaScript object to the jsonb column and Supabase automatically serializes it to JSON. You can store nested objects, arrays, numbers, strings, and booleans. There is no need to call JSON.stringify — the client handles serialization. You can also insert multiple rows with different JSON structures in the same column since jsonb is schemaless.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY6)78// Insert a product with JSON metadata9const { data, error } = await supabase10 .from('products')11 .insert({12 name: 'Wireless Keyboard',13 metadata: {14 brand: 'Logitech',15 color: 'black',16 weight_grams: 450,17 features: ['bluetooth', 'rechargeable', 'backlit'],18 dimensions: { width: 43, height: 2, depth: 13 }19 }20 })21 .select()Expected result: A new product row is inserted with the full JSON metadata object stored in the metadata column.
Update specific JSON fields without overwriting the entire object
Update specific JSON fields without overwriting the entire object
To update individual fields inside a jsonb column without replacing the whole object, use the PostgreSQL jsonb concatenation operator (||) via an RPC function, or use the Supabase client's update method with a merged object. The simplest approach for the JS client is to read the current value, merge locally, and write back. For atomic updates, create a database function that uses the || operator.
1-- SQL function for atomic JSON field update2create or replace function update_product_metadata(3 product_id bigint,4 new_fields jsonb5)6returns void7language plpgsql8security invoker9as $$10begin11 update public.products12 set metadata = metadata || new_fields13 where id = product_id;14end;15$$;1617-- Call from JavaScript18const { data, error } = await supabase.rpc('update_product_metadata', {19 product_id: 1,20 new_fields: { color: 'white', on_sale: true }21})Expected result: The metadata column is updated with the new fields merged in, preserving all existing fields that were not specified.
Query JSON fields using arrow operators
Query JSON fields using arrow operators
PostgreSQL provides two operators for accessing JSON fields: -> returns a JSON value and ->> returns a text value. Use -> for traversing nested objects and ->> for the final value you want to compare. In the Supabase JS client, you can filter on JSON fields using the arrow syntax in column names.
1-- SQL: Query products by a JSON field2select name, metadata->>'brand' as brand3from public.products4where metadata->>'brand' = 'Logitech';56-- SQL: Query nested JSON fields7select name, metadata->'dimensions'->>'width' as width8from public.products9where (metadata->'dimensions'->>'width')::int > 40;1011-- JavaScript: Filter by JSON field12const { data, error } = await supabase13 .from('products')14 .select('name, metadata->brand')15 .eq('metadata->>brand', 'Logitech')1617// JavaScript: Check if JSON array contains a value18const { data, error } = await supabase19 .from('products')20 .select('*')21 .contains('metadata->features', '["bluetooth"]')Expected result: Queries return only products matching the specified JSON field values, with nested fields accessible through chained arrow operators.
Add GIN indexes on frequently queried JSON paths
Add GIN indexes on frequently queried JSON paths
Without indexes, querying JSON fields requires a full table scan. PostgreSQL's GIN (Generalized Inverted Index) is optimized for jsonb and supports containment operators (@>, ?, ?&, ?|). Create a GIN index on the entire jsonb column for general queries, or create a btree index on a specific extracted path for exact-match queries on a single field.
1-- GIN index on the entire jsonb column (supports @> containment queries)2create index idx_products_metadata on public.products using gin (metadata);34-- Btree index on a specific JSON path (faster for exact-match queries)5create index idx_products_brand on public.products6 using btree ((metadata->>'brand'));78-- Now this query uses the GIN index:9select * from public.products10where metadata @> '{"brand": "Logitech"}';1112-- And this query uses the btree index:13select * from public.products14where metadata->>'brand' = 'Logitech';Expected result: Queries on indexed JSON paths use index scans instead of sequential scans, significantly improving performance on large tables.
Complete working example
1// Complete example: Storing and querying JSON data in Supabase2import { createClient } from '@supabase/supabase-js'34const supabase = createClient(5 process.env.NEXT_PUBLIC_SUPABASE_URL!,6 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!7)89// Insert a product with JSON metadata10async function createProduct() {11 const { data, error } = await supabase12 .from('products')13 .insert({14 name: 'Wireless Keyboard',15 metadata: {16 brand: 'Logitech',17 color: 'black',18 weight_grams: 450,19 features: ['bluetooth', 'rechargeable', 'backlit'],20 dimensions: { width: 43, height: 2, depth: 13 }21 }22 })23 .select()24 return { data, error }25}2627// Query products by JSON field28async function getProductsByBrand(brand: string) {29 const { data, error } = await supabase30 .from('products')31 .select('id, name, metadata->brand')32 .eq('metadata->>brand', brand)33 return { data, error }34}3536// Update a JSON field atomically via RPC37async function updateProductMetadata(38 productId: number,39 fields: Record<string, unknown>40) {41 const { data, error } = await supabase.rpc('update_product_metadata', {42 product_id: productId,43 new_fields: fields44 })45 return { data, error }46}4748// Query products containing a specific feature49async function getProductsWithFeature(feature: string) {50 const { data, error } = await supabase51 .from('products')52 .select('*')53 .contains('metadata->features', JSON.stringify([feature]))54 return { data, error }55}Common mistakes when storing JSON Data in Supabase
Why it's a problem: Using the json type instead of jsonb, which does not support indexing or efficient queries
How to avoid: Always use jsonb for new columns. If you have existing json columns, migrate them: ALTER TABLE products ALTER COLUMN metadata TYPE jsonb USING metadata::jsonb;
Why it's a problem: Calling JSON.stringify() on objects before inserting them, resulting in double-encoded strings
How to avoid: Pass JavaScript objects directly to the Supabase client. It handles serialization automatically. JSON.stringify would store the data as a JSON string literal instead of a JSON object.
Why it's a problem: Not adding indexes on JSON paths used in WHERE clauses, causing slow queries on large tables
How to avoid: Add a GIN index for general containment queries or a btree index on the specific extracted path: CREATE INDEX idx_name ON table USING btree ((column->>'field'));
Best practices
- Use jsonb instead of json for better performance, indexing support, and duplicate key handling
- Set a default value of '{}' on jsonb columns to avoid null handling complexity
- Use -> for traversing JSON objects and ->> for extracting the final text value for comparisons
- Add GIN indexes on jsonb columns that are frequently queried with containment operators
- Create database functions with the || operator for atomic JSON field updates instead of read-modify-write patterns
- Keep JSON structures relatively flat — deeply nested data is harder to query, index, and validate
- Consider using separate relational columns for fields that are always present and frequently filtered
- Enable RLS on tables with jsonb columns and write policies that consider the JSON content if needed
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Supabase table called products with a jsonb column called metadata. Show me how to insert JSON data, query by a nested field like metadata.brand, update a single field without overwriting the entire object, and add a GIN index for performance.
Create a products table with a jsonb metadata column, enable RLS, and write a database function that uses the || operator to atomically merge new fields into the metadata column. Include a GIN index on the metadata column.
Frequently asked questions
What is the difference between json and jsonb in PostgreSQL?
The json type stores data as a text string and parses it on every access. The jsonb type stores data in a binary format that supports indexing, is faster for reads, and automatically removes duplicate keys. Always use jsonb in Supabase.
Is there a size limit for jsonb columns in Supabase?
PostgreSQL supports jsonb values up to 1 GB per field. However, very large JSON objects degrade query performance. For large data, consider using Supabase Storage for files and storing only references in the database.
Can I validate the structure of JSON data before inserting?
PostgreSQL does not enforce JSON schemas natively. You can use a check constraint with a database function to validate structure, or validate on the client side before inserting. Supabase Edge Functions are a good place for server-side validation.
How do I remove a key from a jsonb object?
Use the - operator in SQL: UPDATE products SET metadata = metadata - 'key_to_remove' WHERE id = 1; For nested keys, use jsonb_set with null or the #- operator for path-based removal.
Can I use jsonb with RLS policies?
Yes, you can reference jsonb fields in RLS policies. For example: USING (metadata->>'owner_id' = (select auth.uid())::text). However, this is slower than using a dedicated column for access control.
Should I use jsonb or separate columns for structured data?
Use separate columns for data that is always present, frequently filtered, and has a fixed schema. Use jsonb for optional metadata, user preferences, or data that varies between rows. Combining both approaches in the same table is common and recommended.
Can RapidDev help design my Supabase database schema with JSON columns?
Yes, RapidDev can help you design an optimal schema that balances relational columns for performance-critical data with jsonb columns for flexible metadata, including proper indexing and RLS policies.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation