supabase-test

Test Supabase applications with supabase-test. Use when asked to "test Supabase", "test RLS with Supabase", "write Supabase tests", or when testing applications built on Supabase.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "supabase-test" with this command: npx skills add constructive-io/constructive-skills/constructive-io-constructive-skills-supabase-test

Testing Supabase Applications with supabase-test

TypeScript-native testing for Supabase with ephemeral databases, RLS testing, and multi-user simulation.

When to Apply

Use this skill when:

  • Testing Supabase applications
  • Testing RLS policies with Supabase roles (anon, authenticated)
  • Simulating authenticated users in tests
  • Testing with Supabase's auth.users table

Why supabase-test?

Traditional Supabase testing uses pgTap (SQL-based). supabase-test provides:

  • Pure TypeScript tests (Jest/Vitest)
  • Multi-user RLS simulation
  • Direct Postgres access
  • Instant test isolation
  • CI-ready ephemeral databases

Setup

Install Dependencies

pnpm add -D supabase-test

Initialize Supabase

npx supabase init
npx supabase start

Configure pgpm (Optional)

If using pgpm for schema management:

pgpm init workspace
cd packages/myapp
pgpm init
pgpm install @pgpm/supabase

Core Concepts

Two Database Clients

ClientPurpose
pgSuperuser client for setup/teardown (bypasses RLS)
dbUser client for testing with Supabase roles

Test Isolation

Each test runs in a transaction:

  • beforeEach() starts transaction/savepoint
  • afterEach() rolls back
  • Tests are completely isolated

Basic Test Structure

import { getConnections, PgTestClient } from 'supabase-test';

let pg: PgTestClient;
let db: PgTestClient;
let teardown: () => Promise<void>;

beforeAll(async () => {
  ({ pg, db, teardown } = await getConnections());
});

afterAll(async () => {
  await teardown();
});

beforeEach(async () => {
  await db.beforeEach();
});

afterEach(async () => {
  await db.afterEach();
});

it('queries the database', async () => {
  const result = await db.query('SELECT 1 + 1 AS sum');
  expect(result.rows[0].sum).toBe(2);
});

Creating Test Users

Use insertUser() to create users in auth.users:

import { getConnections, PgTestClient, insertUser } from 'supabase-test';

let pg: PgTestClient;
let db: PgTestClient;
let teardown: () => Promise<void>;

let alice: any;
let bob: any;

beforeAll(async () => {
  ({ pg, db, teardown } = await getConnections());

  // Create users in auth.users (requires superuser)
  alice = await insertUser(pg, 'alice@example.com', '550e8400-e29b-41d4-a716-446655440001');
  bob = await insertUser(pg, 'bob@example.com', '550e8400-e29b-41d4-a716-446655440002');
});

Parameters:

  • pg - Superuser client (required for auth.users)
  • email - User's email
  • id - Optional UUID (auto-generated if omitted)

Setting User Context

Simulate Supabase roles with setContext():

// Authenticated user
db.setContext({
  role: 'authenticated',
  'request.jwt.claim.sub': alice.id
});

// Anonymous user
db.setContext({ role: 'anon' });

// Service role (admin)
db.setContext({ role: 'service_role' });

Testing RLS Policies

User Can Access Own Data

it('user can insert own record', async () => {
  db.setContext({
    role: 'authenticated',
    'request.jwt.claim.sub': alice.id
  });

  const result = await db.one(`
    INSERT INTO app.posts (title, owner_id)
    VALUES ($1, $2)
    RETURNING id, title, owner_id
  `, ['My Post', alice.id]);

  expect(result.title).toBe('My Post');
  expect(result.owner_id).toBe(alice.id);
});

User Cannot Access Others' Data

it('user cannot see other users data', async () => {
  // Bob creates a post
  db.setContext({
    role: 'authenticated',
    'request.jwt.claim.sub': bob.id
  });

  await db.one(`
    INSERT INTO app.posts (title, owner_id)
    VALUES ($1, $2)
    RETURNING id
  `, ['Bob Post', bob.id]);

  // Alice queries - should not see Bob's post
  db.setContext({
    role: 'authenticated',
    'request.jwt.claim.sub': alice.id
  });

  const result = await db.query('SELECT * FROM app.posts');
  expect(result.rows).toHaveLength(0);
});

Testing Permission Denied

Use savepoint pattern for expected failures:

it('anonymous cannot insert', async () => {
  db.setContext({ role: 'anon' });

  const point = 'anon_insert';
  await db.savepoint(point);

  await expect(
    db.query(`INSERT INTO app.posts (title) VALUES ('Hacked')`)
  ).rejects.toThrow(/permission denied/);

  await db.rollback(point);
});

Seeding Test Data

With insertUser()

beforeAll(async () => {
  ({ pg, db, teardown } = await getConnections());

  alice = await insertUser(pg, 'alice@example.com');
  bob = await insertUser(pg, 'bob@example.com');
});

With loadJson()

beforeAll(async () => {
  ({ pg, db, teardown } = await getConnections());

  alice = await insertUser(pg, 'alice@example.com', '550e8400-e29b-41d4-a716-446655440001');

  await pg.loadJson({
    'app.posts': [
      { title: 'Post 1', owner_id: alice.id },
      { title: 'Post 2', owner_id: alice.id }
    ]
  });
});

With loadSql()

import path from 'path';

beforeAll(async () => {
  ({ pg, db, teardown } = await getConnections());

  await pg.loadSql([
    path.join(__dirname, 'fixtures/seed.sql')
  ]);
});

With loadCsv()

import path from 'path';

beforeAll(async () => {
  ({ pg, db, teardown } = await getConnections());

  await pg.loadCsv({
    'app.posts': path.join(__dirname, 'fixtures/posts.csv')
  });
});

Note: loadCsv() bypasses RLS (uses COPY). Always use pg client.

Query Methods

MethodReturnsUse Case
db.query(sql, params){ rows: [...] }Multiple rows
db.one(sql, params)Single row objectExactly one row
db.many(sql, params)Array of rowsMultiple rows (array)
// Multiple rows
const result = await db.query('SELECT * FROM app.posts');
console.log(result.rows);

// Single row (throws if not exactly one)
const post = await db.one('SELECT * FROM app.posts WHERE id = $1', [postId]);
console.log(post.title);

// Array of rows
const posts = await db.many('SELECT * FROM app.posts');
console.log(posts.length);

Complete Example

import { getConnections, PgTestClient, insertUser } from 'supabase-test';

let pg: PgTestClient;
let db: PgTestClient;
let teardown: () => Promise<void>;

let alice: any;
let bob: any;

beforeAll(async () => {
  ({ pg, db, teardown } = await getConnections());

  alice = await insertUser(pg, 'alice@example.com');
  bob = await insertUser(pg, 'bob@example.com');
});

afterAll(async () => {
  await teardown();
});

beforeEach(async () => {
  await db.beforeEach();
});

afterEach(async () => {
  await db.afterEach();
});

describe('RLS policies', () => {
  it('users only see their own posts', async () => {
    // Alice creates a post
    db.setContext({
      role: 'authenticated',
      'request.jwt.claim.sub': alice.id
    });

    await db.one(`
      INSERT INTO app.posts (title, owner_id)
      VALUES ('Alice Post', $1)
      RETURNING id
    `, [alice.id]);

    // Bob creates a post
    db.setContext({
      role: 'authenticated',
      'request.jwt.claim.sub': bob.id
    });

    await db.one(`
      INSERT INTO app.posts (title, owner_id)
      VALUES ('Bob Post', $1)
      RETURNING id
    `, [bob.id]);

    // Alice queries - only sees her post
    db.setContext({
      role: 'authenticated',
      'request.jwt.claim.sub': alice.id
    });

    const result = await db.many('SELECT title FROM app.posts');
    expect(result).toHaveLength(1);
    expect(result[0].title).toBe('Alice Post');
  });
});

Running Tests

# Run all tests
pnpm test

# Watch mode
pnpm test:watch

References

  • Related skill: pgsql-test-rls for general RLS testing patterns
  • Related skill: pgsql-test-seeding for seeding strategies
  • Related skill: pgsql-test-exceptions for handling aborted transactions

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

planning-blueprinting

No summary provided by upstream source.

Repository SourceNeeds Review
General

drizzle-orm

No summary provided by upstream source.

Repository SourceNeeds Review
General

pgsql-parser-testing

No summary provided by upstream source.

Repository SourceNeeds Review