The Northwind sample on Supabase

A ready-made, relational public dataset you can host on Supabase in one paste and connect to from the browser. It is the classic Northwind database - categories, customers, products, orders, order_details - plus an order_lines join view, so you get a real multi-table schema, not a single flat table.

Five compact Northwind tables joined by the order_lines view, read-only to the anon role via Row-Level Security, browsed by the grid.

It is the same dataset as the in-browser Northwind on PGlite demo; this page hosts it on real Postgres so the anon key works from any browser app, and so Supabase realtime and Row-Level Security apply.

The public anon key is read-only here (RLS select for anon). That is what makes it safe to expose in a client-side demo. To allow writes, scope a policy to authenticated users - see making it writable.


Step 1 - Create a project

Sign in at supabase.com, click New project, and wait for it to provision. Then open Project Settings -> API and copy the Project URL (https://xxxx.supabase.co) and the anon public key.

Step 2 - Run the setup SQL

Open the SQL Editor, paste the whole script below, and click Run. It creates the tables, seeds them, builds the order_lines view, and opens read-only access to the anon role via RLS.

-- Northwind (compact) + read-only public access. Safe to run more than once.
drop view if exists order_lines;
drop table if exists order_details, orders, products, customers, categories cascade;

create table categories (
  id   integer primary key,
  name text not null
);

create table customers (
  id      integer generated by default as identity primary key,
  company text not null,
  contact text,
  city    text,
  country text
);

create table products (
  id             integer generated by default as identity primary key,
  name           text not null,
  category_id    integer references categories(id),
  unit_price     double precision not null,
  units_in_stock integer not null default 0,
  discontinued   boolean not null default false
);

create table orders (
  id           integer primary key,
  customer_id  integer references customers(id),
  order_date   date not null,
  ship_country text
);

create table order_details (
  id         integer primary key,
  order_id   integer references orders(id),
  product_id integer references products(id),
  quantity   integer not null,
  unit_price double precision not null
);

insert into categories (id, name) values
  (1,'Beverages'),(2,'Condiments'),(3,'Confections'),(4,'Dairy Products'),
  (5,'Grains/Cereals'),(6,'Meat/Poultry'),(7,'Produce'),(8,'Seafood');

insert into customers (id, company, contact, city, country) values
  (1,'Alfreds Futterkiste','Maria Anders','Berlin','Germany'),
  (2,'Ana Trujillo Emparedados','Ana Trujillo','México D.F.','Mexico'),
  (3,'Antonio Moreno Taquería','Antonio Moreno','México D.F.','Mexico'),
  (4,'Around the Horn','Thomas Hardy','London','UK'),
  (5,'Berglunds snabbköp','Christina Berglund','Luleå','Sweden'),
  (6,'Blauer See Delikatessen','Hanna Moos','Mannheim','Germany'),
  (7,'Bólido Comidas preparadas','Martín Sommer','Madrid','Spain'),
  (8,'Bon app''','Laurence Lebihan','Marseille','France'),
  (9,'Bottom-Dollar Markets','Elizabeth Lincoln','Tsawassen','Canada'),
  (10,'B''s Beverages','Victoria Ashworth','London','UK');

insert into products (id, name, category_id, unit_price, units_in_stock, discontinued) values
  (1,'Chai',1,18.00,39,false),(2,'Chang',1,19.00,17,false),
  (3,'Aniseed Syrup',2,10.00,13,false),(4,'Chef Anton''s Cajun Seasoning',2,22.00,53,false),
  (5,'Grandma''s Boysenberry Spread',2,25.00,120,false),(6,'Uncle Bob''s Organic Dried Pears',7,30.00,15,false),
  (7,'Northwoods Cranberry Sauce',2,40.00,6,false),(8,'Mishi Kobe Niku',6,97.00,29,true),
  (9,'Ikura',8,31.00,31,false),(10,'Queso Cabrales',4,21.00,22,false),
  (11,'Konbu',8,6.00,24,false),(12,'Tofu',7,23.25,35,false),
  (13,'Genen Shouyu',2,15.50,39,false),(14,'Pavlova',3,17.45,29,false),
  (15,'Alice Mutton',6,39.00,0,true),(16,'Carnarvon Tigers',8,62.50,42,false);

insert into orders (id, customer_id, order_date, ship_country) values
  (10248,5,'2023-07-04','Sweden'),(10249,6,'2023-07-05','Germany'),
  (10250,3,'2023-07-08','Mexico'),(10251,7,'2023-07-08','Spain'),
  (10252,9,'2023-07-09','Canada'),(10253,3,'2023-07-10','Mexico'),
  (10254,10,'2023-07-11','UK'),(10255,1,'2023-07-12','Germany'),
  (10256,5,'2023-07-15','Sweden'),(10257,4,'2023-07-16','UK');

insert into order_details (id, order_id, product_id, quantity, unit_price) values
  (1,10248,11,12,6.00),(2,10248,14,10,17.45),(3,10248,1,5,18.00),
  (4,10249,8,9,97.00),(5,10249,16,40,62.50),
  (6,10250,9,10,31.00),(7,10250,12,35,23.25),(8,10250,2,15,19.00),
  (9,10251,6,6,30.00),(10,10251,13,15,15.50),
  (11,10252,5,40,25.00),(12,10252,10,25,21.00),
  (13,10253,3,20,10.00),(14,10253,4,42,22.00),(15,10253,1,40,18.00),
  (16,10254,16,15,62.50),(17,10254,11,21,6.00),
  (18,10255,2,20,19.00),(19,10255,9,35,31.00),(20,10255,14,25,17.45),
  (21,10256,7,15,40.00),(22,10256,5,12,25.00),
  (23,10257,12,25,23.25),(24,10257,6,6,30.00);

create view order_lines
  with (security_invoker = on)  -- the view runs with the caller's RLS, so anon's select policies apply
as
  select d.id, d.order_id, o.order_date, c.company as customer,
         p.name as product, cat.name as category,
         d.quantity, d.unit_price, (d.quantity * d.unit_price) as line_total,
         o.ship_country
  from order_details d
  join orders o       on o.id = d.order_id
  join customers c    on c.id = o.customer_id
  join products p     on p.id = d.product_id
  join categories cat on cat.id = p.category_id;

-- Read-only public access: enable RLS and grant the anon role SELECT only.
do $
declare t text;
begin
  foreach t in array array['categories','customers','products','orders','order_details']
  loop
    execute format('alter table %I enable row level security', t);
    execute format('drop policy if exists "public read" on %I', t);
    execute format('create policy "public read" on %I for select to anon using (true)', t);
  end loop;
end $;

Step 3 - Connect

No-code (fastest): open the Supabase demo, paste your Project URL + anon key, and set the table to order_lines to browse the joined data, or products / customers for the base tables. The grid introspects the columns and renders sort / filter / search / paging - all pushed to PostgREST.

In code, it is one createSupabaseDataSource call:

import { createClient } from '@supabase/supabase-js'
import { createServerDataSource } from '@svgrid/grid'
import { createSupabaseDataSource, schemaToColumns } from '@svgrid/enterprise'
import { orderLinesSchema } from './northwind' // the EntitySchema for the view

const client = createClient(import.meta.env.PUBLIC_SUPABASE_URL, import.meta.env.PUBLIC_SUPABASE_ANON_KEY)

const source = createSupabaseDataSource({ client, table: 'order_lines', schema: orderLinesSchema })
const controller = createServerDataSource(source, {
  pageSize: 25,
  getRowId: (r) => String(r.id),
  onChange: (s) => (view = s),
})
controller.refresh()

const columns = schemaToColumns(orderLinesSchema)

The matching EntitySchemas (productsSchema, customersSchema, orderLinesSchema) are the ones the PGlite demo uses - copy them from the demo source, or let the demo's introspectSupabaseTable build them from the live table.

Making it writable

The policies above grant anon select only, so create / update / delete from an unauthenticated browser are blocked by RLS (the grid surfaces the permission error). That is the right default for a public sample. For an app where signed-in users edit their data, replace the read policy with one scoped to authenticated (and add insert/update/delete policies), exactly as in the Supabase CRUD grid tutorial.

Why this over a flat table

The tiny customers table in supabase.md is the fastest way to see CRUD. This Northwind sample exists for the next question: relations. The order_lines view joins five tables into one grid, so you can demo grouping, totals, and filtering across the whole schema - the shape real business data takes. The same dataset runs with zero backend in the Northwind on PGlite demo when you want to show it offline.

See also