Skip to main content
AI/MLjeremylongshore

supabase-hello-world

"Run your first Supabase query \u2014 insert a row and read it back.\n\

Stars
2,267
Source
jeremylongshore/claude-code-plugins-plus-skills
Updated
2026-05-31
Slug
jeremylongshore--claude-code-plugins-plus-skills--supabase-hello-world
View on GitHubRaw SKILL.md

// install — copy + paste into any project

mkdir -p .claude/skills && curl -fsSL https://raw.githubusercontent.com/jeremylongshore/claude-code-plugins-plus-skills/HEAD/plugins/saas-packs/supabase-pack/skills/supabase-hello-world/SKILL.md -o .claude/skills/supabase-hello-world.md

Drops the SKILL.md into .claude/skills/supabase-hello-world.md. Works with Claude Code, Cursor, and any agent that loads SKILL.md files from .claude/skills/.

Supabase Hello World — First Query

Overview

Execute your first real Supabase query: create a todos table in the dashboard, insert a row with the JS client, and read it back. This validates that your project URL, anon key, and Row Level Security are configured correctly before you build anything else.

Prerequisites

  • Completed supabase-install-auth setup (project URL + anon key in .env)
  • @supabase/supabase-js v2+ installed (npm install @supabase/supabase-js)
  • A Supabase project at supabase.com/dashboard

Instructions

The workflow is three steps: create the table (dashboard SQL), insert a row with the JS client, and read it back to prove the round-trip. The skeleton below is enough to follow along; the fully annotated code for every step — including the error-branch comments and expected console output — lives in the full implementation walkthrough.

Step 1: Create the todos Table

In the Supabase dashboard SQL Editor, create the table, enable Row Level Security (required for anon-key access), and add permissive read + insert policies for the hello-world exercise:

create table public.todos (
  id bigint generated always as identity primary key,
  task text not null,
  is_complete boolean default false,
  inserted_at timestamptz default now()
);

alter table public.todos enable row level security;

create policy "Allow public read" on public.todos
  for select using (true);

create policy "Allow public insert" on public.todos
  for insert with check (true);

Lock these policies down before production. Verify the table appears under Table Editor before continuing.

Step 2: Insert a Row

Create the client from your env vars, then insert. Chain .select() to get the inserted row back — .insert() alone returns { data: null }:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_ANON_KEY!
)

const { data, error } = await supabase
  .from('todos')
  .insert({ task: 'Hello from Supabase!' })
  .select()

Step 3: Read It Back

Select all rows and confirm the row you inserted is present:

const { data: todos } = await supabase.from('todos').select('*')
// [{ id: 1, task: "Hello from Supabase!", is_complete: false, ... }]

Open the Table Editor in the dashboard to visually confirm the row is there. See implementation.md for the fully error-handled version of each step.

Output

  • todos table created with RLS enabled
  • One row inserted via the JS client
  • Same row read back with .select('*')
  • Dashboard confirms the data round-trip

Error Handling

Error Cause Solution
relation "public.todos" does not exist Table not created Run the Step 1 SQL in the dashboard SQL Editor
new row violates row-level security policy RLS blocks the insert Add the permissive insert policy from Step 1
Invalid API key Wrong anon key in .env Copy from Settings > API in the dashboard
FetchError: request to https://... failed Wrong project URL Verify SUPABASE_URL matches dashboard URL
data is null after insert Missing .select() chain Add .select() after .insert()
Empty array returned from select RLS blocks reads Add the select policy from Step 1

Examples

Complete, runnable end-to-end scripts live in references/examples.md — a full TypeScript script (insert with .single(), then an ordered/limited read-back) and the Python equivalent. Minimal TypeScript shape:

const { data } = await supabase
  .from('todos')
  .insert({ task: 'Hello!' })
  .select()
  .single()

Resources

Next Steps

Once the round-trip works, proceed to supabase-local-dev-loop for the local development workflow with the Supabase CLI — running Postgres locally, applying migrations, and syncing schema to your hosted project. Before shipping, replace the permissive using (true) / with check (true) policies from Step 1 with real per-user Row Level Security rules, since the hello-world policies expose the table to anyone holding the anon key.