Supabase stores JSON data in PostgreSQL jsonb columns, which you can query using both the JavaScript client and raw SQL. The JS client uses arrow notation in select() to extract nested values, and filter methods like .eq() and .contains() to query inside JSON objects. In SQL, use -> to get a JSON element and ->> to get a text value. Add GIN indexes on jsonb columns for fast lookups on nested fields.
Querying JSONB Data in Supabase
PostgreSQL's jsonb type lets you store flexible, schema-less data alongside your structured columns. Supabase exposes powerful JSONB query capabilities through both its JavaScript client and SQL. This tutorial shows you how to select specific fields from JSON columns, filter rows based on nested values, and add indexes to keep JSON queries fast as your data grows.
Prerequisites
- A Supabase project with a table containing a jsonb column
- @supabase/supabase-js v2+ installed in your project
- Basic understanding of JSON structure and PostgreSQL data types
Step-by-step guide
Create a table with a jsonb column
Create a table with a jsonb column
Before querying JSON data, you need a table with a jsonb column. The jsonb type stores JSON in a decomposed binary format that is faster to query than the plain json type. Use jsonb for any column that will be filtered or indexed. Add some sample data with nested objects to practice querying.
1-- Create a table with a jsonb metadata column2CREATE TABLE products (3 id uuid PRIMARY KEY DEFAULT gen_random_uuid(),4 name text NOT NULL,5 metadata jsonb DEFAULT '{}',6 created_at timestamptz DEFAULT now()7);89-- Enable RLS10ALTER TABLE products ENABLE ROW LEVEL SECURITY;1112-- Allow authenticated users to read13CREATE POLICY "Authenticated users can read products"14 ON products FOR SELECT TO authenticated USING (true);1516-- Insert sample data with nested JSON17INSERT INTO products (name, metadata) VALUES18 ('Widget A', '{"color": "red", "weight": 1.5, "tags": ["sale", "new"], "dimensions": {"width": 10, "height": 5}}'),19 ('Widget B', '{"color": "blue", "weight": 2.0, "tags": ["popular"], "dimensions": {"width": 15, "height": 8}}'),20 ('Widget C', '{"color": "red", "weight": 0.8, "tags": ["sale", "popular"], "dimensions": {"width": 8, "height": 3}}');Expected result: A products table with sample JSON data in the metadata column.
Select nested JSON values with the JS client
Select nested JSON values with the JS client
The Supabase JS client lets you extract specific JSON fields in the select() method using arrow notation. Use -> to navigate into nested objects. Supabase translates this into PostgreSQL's -> and ->> operators behind the scenes. You can select multiple nested fields and alias them for cleaner results.
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// Select specific JSON fields9const { data } = await supabase10 .from('products')11 .select('name, metadata->color, metadata->weight')1213// Select nested fields (dimensions.width)14const { data: nested } = await supabase15 .from('products')16 .select('name, metadata->dimensions->width')1718// Result: [{ name: 'Widget A', width: 10 }, ...]Expected result: Each row contains only the selected JSON fields alongside the regular columns.
Filter rows by JSON field values
Filter rows by JSON field values
You can filter rows based on values inside JSON columns using the standard filter methods. Use the arrow path syntax to reference nested fields in .eq(), .gt(), .lt(), and other filters. For checking if a JSON array contains a value, use the .contains() method. For checking if a JSON object contains a specific key-value pair, also use .contains() with a JSON object.
1// Filter by a top-level JSON field2const { data: redProducts } = await supabase3 .from('products')4 .select('*')5 .eq('metadata->color', '"red"') // Note: JSON string values need quotes67// Filter by a nested JSON field8const { data: wideProducts } = await supabase9 .from('products')10 .select('*')11 .gt('metadata->weight', 1.0)1213// Check if a JSON array contains a value14const { data: saleProducts } = await supabase15 .from('products')16 .select('*')17 .contains('metadata->tags', '["sale"]')1819// Check if JSON contains a key-value pair20const { data: matched } = await supabase21 .from('products')22 .select('*')23 .contains('metadata', '{"color": "red"}')Expected result: Only rows matching the JSON field condition are returned.
Query JSON with raw SQL operators
Query JSON with raw SQL operators
For complex JSON queries, use the SQL Editor or supabase.rpc() with PostgreSQL's native JSON operators. The -> operator returns a JSON element, ->> returns a text value, @> checks containment, and ? checks if a key exists. These operators give you full control for queries that the JS client cannot express directly.
1-- Select a JSON field as text (no quotes)2SELECT name, metadata->>'color' AS color3FROM products;45-- Filter by nested value6SELECT * FROM products7WHERE metadata->'dimensions'->>'width' > '10';89-- Check containment (does metadata contain this object?)10SELECT * FROM products11WHERE metadata @> '{"color": "red"}';1213-- Check if a key exists14SELECT * FROM products15WHERE metadata ? 'weight';1617-- Check if any of these keys exist18SELECT * FROM products19WHERE metadata ?| array['color', 'size'];2021-- Access array element by index22SELECT name, metadata->'tags'->>0 AS first_tag23FROM products;Expected result: SQL queries return the expected filtered and extracted JSON data.
Add GIN indexes for fast JSON queries
Add GIN indexes for fast JSON queries
Without indexes, JSON queries perform a sequential scan on every row. Add a GIN index on the jsonb column to speed up containment (@>), key existence (?), and array operations. For queries that filter on a specific JSON path, create a targeted btree index on the extracted value for even better performance.
1-- General-purpose GIN index for @> and ? operators2CREATE INDEX idx_products_metadata ON products USING gin (metadata);34-- Targeted btree index for a specific field5-- Best for .eq() and comparison filters on one path6CREATE INDEX idx_products_color ON products7 USING btree ((metadata->>'color'));89-- Index for numeric comparisons on a JSON field10CREATE INDEX idx_products_weight ON products11 USING btree (((metadata->>'weight')::numeric));1213-- Verify the index is being used14EXPLAIN (ANALYZE)15SELECT * FROM products16WHERE metadata @> '{"color": "red"}';Expected result: EXPLAIN ANALYZE shows Index Scan instead of Seq Scan, with significantly faster query execution.
Complete working example
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// ---- Selecting JSON fields ----910// Select specific nested values11async function getProductColors() {12 const { data, error } = await supabase13 .from('products')14 .select('name, metadata->color, metadata->dimensions->width')15 return { data, error }16}1718// ---- Filtering by JSON values ----1920// Filter by JSON string field21async function getProductsByColor(color: string) {22 const { data, error } = await supabase23 .from('products')24 .select('*')25 .eq('metadata->color', `"${color}"`)26 return { data, error }27}2829// Filter by JSON numeric field30async function getProductsHeavierThan(weight: number) {31 const { data, error } = await supabase32 .from('products')33 .select('*')34 .gt('metadata->weight', weight)35 return { data, error }36}3738// Filter by JSON array containment39async function getProductsWithTag(tag: string) {40 const { data, error } = await supabase41 .from('products')42 .select('*')43 .contains('metadata->tags', `["${tag}"]`)44 return { data, error }45}4647// Filter by JSON object containment48async function getProductsMatching(criteria: Record<string, any>) {49 const { data, error } = await supabase50 .from('products')51 .select('*')52 .contains('metadata', JSON.stringify(criteria))53 return { data, error }54}5556// ---- Usage examples ----5758async function main() {59 const colors = await getProductColors()60 console.log('Colors:', colors.data)6162 const redProducts = await getProductsByColor('red')63 console.log('Red products:', redProducts.data)6465 const heavy = await getProductsHeavierThan(1.0)66 console.log('Heavy products:', heavy.data)6768 const onSale = await getProductsWithTag('sale')69 console.log('On sale:', onSale.data)70}7172main()Common mistakes when querying JSON Fields in Supabase
Why it's a problem: Forgetting to wrap JSON string values in double quotes when using .eq()
How to avoid: JSON stores strings with quotes. Use .eq('metadata->color', '"red"') with the inner quotes. Numeric values like .gt('metadata->weight', 1.5) do not need quotes.
Why it's a problem: Using the json column type instead of jsonb
How to avoid: Always use jsonb for columns you query or index. The json type stores raw text and cannot be indexed with GIN. Alter existing columns: ALTER TABLE products ALTER COLUMN metadata TYPE jsonb USING metadata::jsonb;
Why it's a problem: Comparing ->> text output to numbers without casting in SQL
How to avoid: The ->> operator returns text. For numeric comparisons, cast explicitly: WHERE (metadata->>'weight')::numeric > 1.5. Without the cast, '9' > '10' because text comparison is alphabetical.
Why it's a problem: Not adding indexes on jsonb columns, leading to full table scans
How to avoid: Add a GIN index for containment queries or a btree index on extracted paths for equality/range queries. Without indexes, every JSON query scans every row.
Best practices
- Use jsonb instead of json for all columns that need querying or indexing
- Add a GIN index on the jsonb column for general-purpose containment and key-existence queries
- Use targeted btree indexes on specific extracted paths for high-frequency equality or range filters
- Use -> to get JSON elements and ->> to get text values in SQL queries
- Cast ->> text output to the appropriate type for numeric or date comparisons
- Keep JSON structure consistent across rows — inconsistent keys make filtering unreliable
- Consider extracting frequently queried JSON fields into dedicated columns for better performance and type safety
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Supabase products table with a jsonb metadata column containing color, weight, tags array, and dimensions object. Show me how to query by color, filter by weight range, check if tags contains a value, and access nested dimensions fields using both the JS client and raw SQL.
Write TypeScript functions using the Supabase JS client to select specific fields from a jsonb column, filter by nested JSON values, and check JSON array containment. Include the SQL for creating GIN and btree indexes on the jsonb column.
Frequently asked questions
What is the difference between -> and ->> in PostgreSQL?
The -> operator returns a JSON element (preserving the JSON type), while ->> returns the value as plain text. Use -> for navigating nested objects and ->> when you need the final text value for comparisons or display.
Can I update a single field inside a jsonb column without replacing the entire object?
Yes. Use the jsonb_set function in SQL: UPDATE products SET metadata = jsonb_set(metadata, '{color}', '"green"') WHERE id = 'some-id'. Via the JS client, you currently need to read the full JSON, modify it, and write it back.
Should I use jsonb columns or separate tables for structured data?
Use separate tables for data with a consistent schema that you frequently query and join. Use jsonb for truly flexible or variable data like user preferences, feature flags, or third-party API responses where the structure varies.
Does RLS work on jsonb columns?
Yes. You can reference jsonb fields in RLS policies. For example: USING (metadata->>'tenant_id' = (select auth.jwt()->>'tenant_id')). However, complex JSON operations in RLS policies can slow down every query, so keep policies simple.
How do I query a jsonb array for partial matches?
Use the @> containment operator. For example, to find rows where tags contains 'sale': WHERE metadata->'tags' @> '["sale"]'. This works efficiently with a GIN index on the jsonb column.
Can RapidDev help design my Supabase database schema with JSON columns?
Yes. RapidDev can help you decide where to use jsonb versus relational tables, design proper indexes for JSON queries, and implement efficient query patterns for your specific data model.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation