Aug 16, 2025

Create Your First Table in Supabase

New to Supabase? Learn how to create your first table—users—with copy-paste SQL. We’ll cover UUIDs, timestamps, unique emails, test inserts, and safe next steps like indexes, updates, and policies.

DevKit

Supabase

SQL

Foundation

Your app needs a place to store data. We’ll create a simple users table with a unique ID, name, email, and timestamp. No jargon—just copy, paste, run, and learn what each line does.

When you start a new app, you need a reliable “home” for your data. In Postgres (the database behind Supabase), that home is a table. Think of a table like a spreadsheet with named columns. Each row is one record—here, one user.

In this lesson, you’ll create a users table that stores:

  • a unique ID (so we can always tell users apart),

  • a name and email (human details),

  • and a created_at timestamp (when the row was added).


We’ll keep the SQL simple and explain every line in plain English. You’ll also learn how to test the table (insert + read data) and how to extend it safely (add a profile picture or points later). By the end, you’ll understand the basics you’ll reuse for every other table in your project.

Implementation

Copy these SQL snippets into Supabase → SQL Editor. Run them in order, test with inserts, and verify the results with a simple SELECT.

0) Prerequisite (UUID generator)

Postgres can generate UUIDs using the pgcrypto extension. Enable it once per database:

-- Enable UUID generator (safe to run multiple times)
CREATE EXTENSION IF NOT EXISTS "pgcrypto";


1) Create the users table

-- Create a basic users table
CREATE TABLE IF NOT EXISTS public.users (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name        TEXT        NOT NULL,
  email       TEXT        NOT NULL UNIQUE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);


What this does (line by line):

  • id UUID PRIMARY KEY DEFAULT gen_random_uuid()
    Each user gets a unique identifier (UUID). The PRIMARY KEY guarantees no duplicates. The DEFAULT generates it automatically.

  • name TEXT NOT NULL
    TEXT = free-form text. NOT NULL means “this field can’t be empty.”

  • email TEXT NOT NULL UNIQUE
    UNIQUE makes sure two users can’t use the same email.

  • created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    Saves the creation time. TIMESTAMPTZ handles time zones correctly. DEFAULT NOW() fills it in automatically.



2) (Optional) Link to Supabase Auth users

If you’re using Supabase Auth, you can keep a separate profile row tied to the authenticated user’s ID:

-- Create a profiles table linked to auth.users (optional pattern)
CREATE TABLE IF NOT EXISTS public.profiles (
  user_id     UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  name        TEXT        NOT NULL,
  email       TEXT        NOT NULL UNIQUE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);



  • REFERENCES auth.users(id) ties the profile to the authenticated user.

  • ON DELETE CASCADE removes the profile if the auth user is deleted.

Which one should I use?

  • Simple projects / demos: just use public.users.

  • Auth-based apps: prefer public.profiles tied to auth.users(id) (cleaner separation of auth vs profile).


3) Test: insert a few rows

Copy-paste to add sample data:

-- Insert sample users (for the simple users table)
INSERT INTO public.users (name, email)
VALUES
  ('Alice Example', 'alice@example.com'),
  ('Bob Builder',   'bob@example.com');

-- Try inserting a duplicate email to see UNIQUE in action (should fail)
-- INSERT INTO public.users (name, email) VALUES ('Dup Email', 'alice@example.com');


4) Read data (verify results)

-- Fetch all users (newest first)
SELECT id, name, email, created_at
FROM public.users
ORDER BY created_at DESC;


You should see your two users. If you try the duplicate email insert (uncomment it), Postgres will block it—this is your UNIQUE constraint working for you.

5) Extend the table: add a profile picture

ALTER TABLE public.users
ADD COLUMN IF NOT EXISTS profile_picture TEXT;


Now you can store a URL (e.g., from Supabase Storage) for each user’s avatar.


6) Useful updates you’ll do often

-- Update a user's name
UPDATE public.users
SET name = 'Alice Example (Updated)'
WHERE email = 'alice@example.com';

-- Change an email (note: must remain unique)
UPDATE public.users
SET email = 'alice@newdomain.com'
WHERE email = 'alice@example.com';


7) Cleanup (if you need to start over)

-- Drop the table (careful: deletes all data)
DROP TABLE IF EXISTS public.users;


Tip:
Use IF NOT EXISTS and IF EXISTS to keep scripts re-runnable. It saves you time when iterating.

Optimization

Make it safer and more future-proof: naming, timestamps, indexes, policies, and an updated_at trigger you’ll reuse everywhere.

1) Naming & types you’ll reuse

  • Use snake_case for table/column names (created_at, not createdAt).

  • For public text, use TEXT. For yes/no flags, use BOOLEAN. For counts/points, use INTEGER or BIGINT.

  • Prefer TIMESTAMPTZ for time columns to avoid timezone bugs.



2) Add an updated_at column + trigger (copy-paste pattern)


-- 1) Column
ALTER TABLE public.users
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ;

-- 2) Trigger function (create once and reuse for any table)
CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 3) Trigger on this table
DROP TRIGGER IF EXISTS trg_users_set_updated_at ON public.users;
CREATE TRIGGER trg_users_set_updated_at
BEFORE UPDATE ON public.users
FOR EACH ROW
EXECUTE FUNCTION public.set_updated_at();


Now updated_at fills automatically on every update—no extra code in your app.

3) Index what you query often

Unique constraints already create indexes for those columns (e.g., email). If you frequently filter by name (search), add an index:


CREATE INDEX IF NOT EXISTS idx_users_name ON public.users (name);


4) RLS (Row Level Security) basics (when you’re ready)

RLS lets you control who can see or edit rows. For a public list (e.g., directory of users without private data), you might allow read-only access while locking down writes. Example (for the simple public.users table):


-- Enable RLS
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;

-- Policy: allow anyone to read (public directory)
CREATE POLICY users_read_all
ON public.users
FOR SELECT
USING (true);

-- Policy: only the authenticated user can update their own row (requires a user_id column)
-- If you use the profiles pattern, tie by user_id = auth.uid()
-- Example policy (for a profiles table):
-- CREATE POLICY profiles_self_update
-- ON public.profiles
-- FOR UPDATE
-- USING (user_id = auth.uid());


Safe default:
Start with RLS disabled while you learn, or use a profiles table tied to auth.users(id) and write strict policies there. Never expose private data (like emails) publicly in production.

5) Migrations & re-runnable scripts


Save your SQL in versioned files (e.g., 001_create_users.sql, 002_add_updated_at.sql). Use IF NOT EXISTS / IF EXISTS so you can run them repeatedly without errors. This habit pays off as your schema grows.

6) Common gotchas (fast fixes)


  • Error: function gen_random_uuid() does not exist → Run CREATE EXTENSION IF NOT EXISTS "pgcrypto";

  • Duplicate key value violates unique constraint → You tried to insert a repeated email. That’s expected—either change it or remove UNIQUE if you truly need duplicates.

  • Timezone confusion → Always use TIMESTAMPTZ and store UTC; format in the app UI.

FAQ

01

What’s the difference between a template and a subscription?

02

Do I need coding skills to use your templates?

03

Can you work on my own app, not just your templates?

04

How many revisions are included?

05

Are template purchases refundable?success?

06

Can I cancel my subscription anytime?

07

What happens to my project if I cancel the subscription?

08

Do you handle App Store & Google Play submission?

Aug 16, 2025

Create Your First Table in Supabase

New to Supabase? Learn how to create your first table—users—with copy-paste SQL. We’ll cover UUIDs, timestamps, unique emails, test inserts, and safe next steps like indexes, updates, and policies.

DevKit

Supabase

SQL

Foundation

Your app needs a place to store data. We’ll create a simple users table with a unique ID, name, email, and timestamp. No jargon—just copy, paste, run, and learn what each line does.

When you start a new app, you need a reliable “home” for your data. In Postgres (the database behind Supabase), that home is a table. Think of a table like a spreadsheet with named columns. Each row is one record—here, one user.

In this lesson, you’ll create a users table that stores:

  • a unique ID (so we can always tell users apart),

  • a name and email (human details),

  • and a created_at timestamp (when the row was added).


We’ll keep the SQL simple and explain every line in plain English. You’ll also learn how to test the table (insert + read data) and how to extend it safely (add a profile picture or points later). By the end, you’ll understand the basics you’ll reuse for every other table in your project.

Implementation

Copy these SQL snippets into Supabase → SQL Editor. Run them in order, test with inserts, and verify the results with a simple SELECT.

0) Prerequisite (UUID generator)

Postgres can generate UUIDs using the pgcrypto extension. Enable it once per database:

-- Enable UUID generator (safe to run multiple times)
CREATE EXTENSION IF NOT EXISTS "pgcrypto";


1) Create the users table

-- Create a basic users table
CREATE TABLE IF NOT EXISTS public.users (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name        TEXT        NOT NULL,
  email       TEXT        NOT NULL UNIQUE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);


What this does (line by line):

  • id UUID PRIMARY KEY DEFAULT gen_random_uuid()
    Each user gets a unique identifier (UUID). The PRIMARY KEY guarantees no duplicates. The DEFAULT generates it automatically.

  • name TEXT NOT NULL
    TEXT = free-form text. NOT NULL means “this field can’t be empty.”

  • email TEXT NOT NULL UNIQUE
    UNIQUE makes sure two users can’t use the same email.

  • created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    Saves the creation time. TIMESTAMPTZ handles time zones correctly. DEFAULT NOW() fills it in automatically.



2) (Optional) Link to Supabase Auth users

If you’re using Supabase Auth, you can keep a separate profile row tied to the authenticated user’s ID:

-- Create a profiles table linked to auth.users (optional pattern)
CREATE TABLE IF NOT EXISTS public.profiles (
  user_id     UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  name        TEXT        NOT NULL,
  email       TEXT        NOT NULL UNIQUE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);



  • REFERENCES auth.users(id) ties the profile to the authenticated user.

  • ON DELETE CASCADE removes the profile if the auth user is deleted.

Which one should I use?

  • Simple projects / demos: just use public.users.

  • Auth-based apps: prefer public.profiles tied to auth.users(id) (cleaner separation of auth vs profile).


3) Test: insert a few rows

Copy-paste to add sample data:

-- Insert sample users (for the simple users table)
INSERT INTO public.users (name, email)
VALUES
  ('Alice Example', 'alice@example.com'),
  ('Bob Builder',   'bob@example.com');

-- Try inserting a duplicate email to see UNIQUE in action (should fail)
-- INSERT INTO public.users (name, email) VALUES ('Dup Email', 'alice@example.com');


4) Read data (verify results)

-- Fetch all users (newest first)
SELECT id, name, email, created_at
FROM public.users
ORDER BY created_at DESC;


You should see your two users. If you try the duplicate email insert (uncomment it), Postgres will block it—this is your UNIQUE constraint working for you.

5) Extend the table: add a profile picture

ALTER TABLE public.users
ADD COLUMN IF NOT EXISTS profile_picture TEXT;


Now you can store a URL (e.g., from Supabase Storage) for each user’s avatar.


6) Useful updates you’ll do often

-- Update a user's name
UPDATE public.users
SET name = 'Alice Example (Updated)'
WHERE email = 'alice@example.com';

-- Change an email (note: must remain unique)
UPDATE public.users
SET email = 'alice@newdomain.com'
WHERE email = 'alice@example.com';


7) Cleanup (if you need to start over)

-- Drop the table (careful: deletes all data)
DROP TABLE IF EXISTS public.users;


Tip:
Use IF NOT EXISTS and IF EXISTS to keep scripts re-runnable. It saves you time when iterating.

Optimization

Make it safer and more future-proof: naming, timestamps, indexes, policies, and an updated_at trigger you’ll reuse everywhere.

1) Naming & types you’ll reuse

  • Use snake_case for table/column names (created_at, not createdAt).

  • For public text, use TEXT. For yes/no flags, use BOOLEAN. For counts/points, use INTEGER or BIGINT.

  • Prefer TIMESTAMPTZ for time columns to avoid timezone bugs.



2) Add an updated_at column + trigger (copy-paste pattern)


-- 1) Column
ALTER TABLE public.users
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ;

-- 2) Trigger function (create once and reuse for any table)
CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 3) Trigger on this table
DROP TRIGGER IF EXISTS trg_users_set_updated_at ON public.users;
CREATE TRIGGER trg_users_set_updated_at
BEFORE UPDATE ON public.users
FOR EACH ROW
EXECUTE FUNCTION public.set_updated_at();


Now updated_at fills automatically on every update—no extra code in your app.

3) Index what you query often

Unique constraints already create indexes for those columns (e.g., email). If you frequently filter by name (search), add an index:


CREATE INDEX IF NOT EXISTS idx_users_name ON public.users (name);


4) RLS (Row Level Security) basics (when you’re ready)

RLS lets you control who can see or edit rows. For a public list (e.g., directory of users without private data), you might allow read-only access while locking down writes. Example (for the simple public.users table):


-- Enable RLS
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;

-- Policy: allow anyone to read (public directory)
CREATE POLICY users_read_all
ON public.users
FOR SELECT
USING (true);

-- Policy: only the authenticated user can update their own row (requires a user_id column)
-- If you use the profiles pattern, tie by user_id = auth.uid()
-- Example policy (for a profiles table):
-- CREATE POLICY profiles_self_update
-- ON public.profiles
-- FOR UPDATE
-- USING (user_id = auth.uid());


Safe default:
Start with RLS disabled while you learn, or use a profiles table tied to auth.users(id) and write strict policies there. Never expose private data (like emails) publicly in production.

5) Migrations & re-runnable scripts


Save your SQL in versioned files (e.g., 001_create_users.sql, 002_add_updated_at.sql). Use IF NOT EXISTS / IF EXISTS so you can run them repeatedly without errors. This habit pays off as your schema grows.

6) Common gotchas (fast fixes)


  • Error: function gen_random_uuid() does not exist → Run CREATE EXTENSION IF NOT EXISTS "pgcrypto";

  • Duplicate key value violates unique constraint → You tried to insert a repeated email. That’s expected—either change it or remove UNIQUE if you truly need duplicates.

  • Timezone confusion → Always use TIMESTAMPTZ and store UTC; format in the app UI.

FAQ

01

What’s the difference between a template and a subscription?

02

Do I need coding skills to use your templates?

03

Can you work on my own app, not just your templates?

04

How many revisions are included?

05

Are template purchases refundable?success?

06

Can I cancel my subscription anytime?

07

What happens to my project if I cancel the subscription?

08

Do you handle App Store & Google Play submission?

Aug 16, 2025

Create Your First Table in Supabase

New to Supabase? Learn how to create your first table—users—with copy-paste SQL. We’ll cover UUIDs, timestamps, unique emails, test inserts, and safe next steps like indexes, updates, and policies.

DevKit

Supabase

SQL

Foundation

Your app needs a place to store data. We’ll create a simple users table with a unique ID, name, email, and timestamp. No jargon—just copy, paste, run, and learn what each line does.

When you start a new app, you need a reliable “home” for your data. In Postgres (the database behind Supabase), that home is a table. Think of a table like a spreadsheet with named columns. Each row is one record—here, one user.

In this lesson, you’ll create a users table that stores:

  • a unique ID (so we can always tell users apart),

  • a name and email (human details),

  • and a created_at timestamp (when the row was added).


We’ll keep the SQL simple and explain every line in plain English. You’ll also learn how to test the table (insert + read data) and how to extend it safely (add a profile picture or points later). By the end, you’ll understand the basics you’ll reuse for every other table in your project.

Implementation

Copy these SQL snippets into Supabase → SQL Editor. Run them in order, test with inserts, and verify the results with a simple SELECT.

0) Prerequisite (UUID generator)

Postgres can generate UUIDs using the pgcrypto extension. Enable it once per database:

-- Enable UUID generator (safe to run multiple times)
CREATE EXTENSION IF NOT EXISTS "pgcrypto";


1) Create the users table

-- Create a basic users table
CREATE TABLE IF NOT EXISTS public.users (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name        TEXT        NOT NULL,
  email       TEXT        NOT NULL UNIQUE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);


What this does (line by line):

  • id UUID PRIMARY KEY DEFAULT gen_random_uuid()
    Each user gets a unique identifier (UUID). The PRIMARY KEY guarantees no duplicates. The DEFAULT generates it automatically.

  • name TEXT NOT NULL
    TEXT = free-form text. NOT NULL means “this field can’t be empty.”

  • email TEXT NOT NULL UNIQUE
    UNIQUE makes sure two users can’t use the same email.

  • created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
    Saves the creation time. TIMESTAMPTZ handles time zones correctly. DEFAULT NOW() fills it in automatically.



2) (Optional) Link to Supabase Auth users

If you’re using Supabase Auth, you can keep a separate profile row tied to the authenticated user’s ID:

-- Create a profiles table linked to auth.users (optional pattern)
CREATE TABLE IF NOT EXISTS public.profiles (
  user_id     UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  name        TEXT        NOT NULL,
  email       TEXT        NOT NULL UNIQUE,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);



  • REFERENCES auth.users(id) ties the profile to the authenticated user.

  • ON DELETE CASCADE removes the profile if the auth user is deleted.

Which one should I use?

  • Simple projects / demos: just use public.users.

  • Auth-based apps: prefer public.profiles tied to auth.users(id) (cleaner separation of auth vs profile).


3) Test: insert a few rows

Copy-paste to add sample data:

-- Insert sample users (for the simple users table)
INSERT INTO public.users (name, email)
VALUES
  ('Alice Example', 'alice@example.com'),
  ('Bob Builder',   'bob@example.com');

-- Try inserting a duplicate email to see UNIQUE in action (should fail)
-- INSERT INTO public.users (name, email) VALUES ('Dup Email', 'alice@example.com');


4) Read data (verify results)

-- Fetch all users (newest first)
SELECT id, name, email, created_at
FROM public.users
ORDER BY created_at DESC;


You should see your two users. If you try the duplicate email insert (uncomment it), Postgres will block it—this is your UNIQUE constraint working for you.

5) Extend the table: add a profile picture

ALTER TABLE public.users
ADD COLUMN IF NOT EXISTS profile_picture TEXT;


Now you can store a URL (e.g., from Supabase Storage) for each user’s avatar.


6) Useful updates you’ll do often

-- Update a user's name
UPDATE public.users
SET name = 'Alice Example (Updated)'
WHERE email = 'alice@example.com';

-- Change an email (note: must remain unique)
UPDATE public.users
SET email = 'alice@newdomain.com'
WHERE email = 'alice@example.com';


7) Cleanup (if you need to start over)

-- Drop the table (careful: deletes all data)
DROP TABLE IF EXISTS public.users;


Tip:
Use IF NOT EXISTS and IF EXISTS to keep scripts re-runnable. It saves you time when iterating.

Optimization

Make it safer and more future-proof: naming, timestamps, indexes, policies, and an updated_at trigger you’ll reuse everywhere.

1) Naming & types you’ll reuse

  • Use snake_case for table/column names (created_at, not createdAt).

  • For public text, use TEXT. For yes/no flags, use BOOLEAN. For counts/points, use INTEGER or BIGINT.

  • Prefer TIMESTAMPTZ for time columns to avoid timezone bugs.



2) Add an updated_at column + trigger (copy-paste pattern)


-- 1) Column
ALTER TABLE public.users
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ;

-- 2) Trigger function (create once and reuse for any table)
CREATE OR REPLACE FUNCTION public.set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 3) Trigger on this table
DROP TRIGGER IF EXISTS trg_users_set_updated_at ON public.users;
CREATE TRIGGER trg_users_set_updated_at
BEFORE UPDATE ON public.users
FOR EACH ROW
EXECUTE FUNCTION public.set_updated_at();


Now updated_at fills automatically on every update—no extra code in your app.

3) Index what you query often

Unique constraints already create indexes for those columns (e.g., email). If you frequently filter by name (search), add an index:


CREATE INDEX IF NOT EXISTS idx_users_name ON public.users (name);


4) RLS (Row Level Security) basics (when you’re ready)

RLS lets you control who can see or edit rows. For a public list (e.g., directory of users without private data), you might allow read-only access while locking down writes. Example (for the simple public.users table):


-- Enable RLS
ALTER TABLE public.users ENABLE ROW LEVEL SECURITY;

-- Policy: allow anyone to read (public directory)
CREATE POLICY users_read_all
ON public.users
FOR SELECT
USING (true);

-- Policy: only the authenticated user can update their own row (requires a user_id column)
-- If you use the profiles pattern, tie by user_id = auth.uid()
-- Example policy (for a profiles table):
-- CREATE POLICY profiles_self_update
-- ON public.profiles
-- FOR UPDATE
-- USING (user_id = auth.uid());


Safe default:
Start with RLS disabled while you learn, or use a profiles table tied to auth.users(id) and write strict policies there. Never expose private data (like emails) publicly in production.

5) Migrations & re-runnable scripts


Save your SQL in versioned files (e.g., 001_create_users.sql, 002_add_updated_at.sql). Use IF NOT EXISTS / IF EXISTS so you can run them repeatedly without errors. This habit pays off as your schema grows.

6) Common gotchas (fast fixes)


  • Error: function gen_random_uuid() does not exist → Run CREATE EXTENSION IF NOT EXISTS "pgcrypto";

  • Duplicate key value violates unique constraint → You tried to insert a repeated email. That’s expected—either change it or remove UNIQUE if you truly need duplicates.

  • Timezone confusion → Always use TIMESTAMPTZ and store UTC; format in the app UI.

FAQ

What’s the difference between a template and a subscription?

Do I need coding skills to use your templates?

Can you work on my own app, not just your templates?

How many revisions are included?

Are template purchases refundable?success?

Can I cancel my subscription anytime?

What happens to my project if I cancel the subscription?

Do you handle App Store & Google Play submission?