Supasheet

Quickstart

Build your first feature with Supasheet in 5 minutes

Build Your First Admin Interface

Let's create a simple task management interface to demonstrate Supasheet's SQL-first approach.

Create a Migration

npx supabase migration new create_tasks_table

This creates a new migration file in /supabase/migrations/.

Define Your Schema

Edit the migration file and add:

-- Step 1: Create custom types
create type task_status as enum ('pending', 'in_progress', 'completed');
create type task_priority as enum ('low', 'medium', 'high');

-- Step 2: Add permissions to the system
alter type supasheet.app_permission add value 'public.tasks:select';
alter type supasheet.app_permission add value 'public.tasks:insert';
alter type supasheet.app_permission add value 'public.tasks:update';
alter type supasheet.app_permission add value 'public.tasks:delete';

-- Step 3: Create tasks table
CREATE TABLE public.tasks (
  id UUID PRIMARY KEY DEFAULT extensions.uuid_generate_v4(),
  title TEXT NOT NULL,
  description TEXT,
  status task_status DEFAULT 'pending',
  priority task_priority DEFAULT 'medium',
  due_date TIMESTAMPTZ,

  -- User association
  account_id UUID REFERENCES supasheet.accounts(id) ON DELETE CASCADE,

  -- Audit fields
  created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);

-- Create indexes
create index idx_tasks_account_id on tasks (account_id);
create index idx_tasks_status on tasks (status);

-- Step 4: Enable Row Level Security with permission checks
ALTER TABLE public.tasks ENABLE ROW LEVEL SECURITY;

-- Revoke default permissions
revoke all on table tasks from authenticated, service_role;

-- Grant basic permissions
grant select, insert, update, delete on table tasks to authenticated;

-- Create RLS policies with permission checks
create policy tasks_select on tasks
  for select
  to authenticated
  using (account_id = auth.uid() and supasheet.has_permission('public.tasks:select'));

create policy tasks_insert on tasks
  for insert
  to authenticated
  with check (supasheet.has_permission('public.tasks:insert'));

create policy tasks_update on tasks
  for update
  to authenticated
  using (account_id = auth.uid() and supasheet.has_permission('public.tasks:update'))
  with check (account_id = auth.uid() and supasheet.has_permission('public.tasks:update'));

create policy tasks_delete on tasks
  for delete
  to authenticated
  using (account_id = auth.uid() and supasheet.has_permission('public.tasks:delete'));

-- Step 5: Grant permissions to the 'user' role
insert into supasheet.role_permissions (role, permission) values
  ('user', 'public.tasks:select'),
  ('user', 'public.tasks:insert'),
  ('user', 'public.tasks:update'),
  ('user', 'public.tasks:delete');

-- Step 6: Update meta tables (required for UI discovery)
INSERT INTO supasheet.columns SELECT * FROM supasheet.generate_columns('public')
ON CONFLICT (id) DO UPDATE SET schema = EXCLUDED.schema, table_id = EXCLUDED.table_id, name = EXCLUDED.name;

INSERT INTO supasheet.tables SELECT * FROM supasheet.generate_tables('public')
ON CONFLICT (id) DO UPDATE SET schema = EXCLUDED.schema, name = EXCLUDED.name, rls_enabled = EXCLUDED.rls_enabled;

INSERT INTO supasheet.views SELECT * FROM supasheet.generate_views('public')
ON CONFLICT (id) DO UPDATE SET schema = EXCLUDED.schema, name = EXCLUDED.name;

Apply the Migration

npx supabase db push

This applies your migration to the local database without resetting existing data.

Generate Types

npm run typegen

This creates TypeScript types from your new schema.

View Your Admin Interface

That's it! Supasheet automatically:

  • ✅ Creates a CRUD interface at /home/resource/tasks
  • ✅ Generates forms based on your schema
  • ✅ Applies your RLS policies for security
  • ✅ Handles pagination, filtering, and sorting
  • ✅ Shows only the tasks the user created (via RLS)

Navigate to the Resources section in Supasheet and select "tasks" to see your new interface.

Schema Exposure (Configuration)

For Supasheet to work with your database, certain schemas must be exposed via the PostgREST API. This is already configured in /supabase/config.toml:

[api]
schemas = ["public", "supasheet", "reports", "dashboards", "charts"]

What this means:

  • public - Your application tables (automatically available)
  • supasheet - Internal Supasheet tables (accounts, roles, permissions, etc.)
  • reports - Views for report generation
  • dashboards - Views for dashboard widgets
  • charts - Views for data visualizations

This configuration is already set up in Supasheet. You don't need to modify it unless you're adding custom schemas.

Add a View (Optional)

You can create a view to show tasks with enhanced data:

-- Create a view with joined account information
create or replace view public.vw_tasks
with(security_invoker = true) as
select
  a.name as account_name,
  t.*
from tasks t
join supasheet.accounts a on t.account_id = a.id;

-- Grant permissions
revoke all on public.vw_tasks from authenticated, service_role;
grant select on public.vw_tasks to authenticated;

-- Update meta tables for the view
INSERT INTO supasheet.views SELECT * FROM supasheet.generate_views('public')
ON CONFLICT (id) DO UPDATE SET schema = EXCLUDED.schema, name = EXCLUDED.name;

Apply the changes:

npx supabase db push
npm run typegen

What Just Happened?

With just SQL, you:

  1. Added permissions - Defined access control at the database level
  2. Created a full CRUD interface - No React components needed
  3. Implemented authorization - Using PostgreSQL RLS policies with permission checks
  4. Got type safety - Automatically generated TypeScript types

This is the power of Supasheet's SQL-first philosophy!

Next Steps