Skip to content

Supabase Database Setup

Dharanesh V edited this page Oct 4, 2026 · 3 revisions

Create the project

  • Go to the Supabase dashboard → New project.
  • Pick a name and a strong database password and the region nearest your users.
  • Wait for provisioning to finish. Note the Project URL (https://tvmzihkedajzixziyjhc.supabase.co) will be something like this

Key naming: Supabase is moving from the legacy anon JWT key to the new sb_publishable_... key. Both are public client keys. The secret / service_role key must never go into the Flutter app or the repo.

Create the table

Open SQL Editor → New query, paste, and run.

The column names must match TaskModel.toSupabaseMap() exactly with the code, including the capital letters. Postgres folds unquoted names to lowercase, so the quotes are required.

create table public.focus_hub (
  "Task_id"      uuid        primary key,
  "User_id"      uuid        not null default auth.uid()
                             references auth.users (id) on delete cascade,
  "Task_name"    text        not null,
  "Deadline"     timestamp,                    -- date-only in the UI, nullable
  "Category"     text,
  "Priority"     text        not null default 'Medium'
                             check ("Priority" in ('High', 'Medium', 'Low')),
  is_complete    boolean     not null default false,
  "Created_date" timestamptz not null default now(),
  "Updated_at"   timestamptz not null default now()
);

create index focus_hub_user_idx on public.focus_hub ("User_id");

Resoning

Choice Reason
"Task_id" uuid primary key The app generates the UUID with Uuid().v4(). The primary key makes upsert work: if the app gives the same Task_id means update, new Task_id means insert.
"User_id" why it references auth.users Ties every row to a real logged-in user. on delete cascade removes their tasks if the account is deleted.
What is index on "User_id" Every query and every RLS check filters by User_id.

Verify against your own table. Compare the columns above with Table Editor in your project. If your existing types differ (for example Dead line is date), keep what works. Only the names are a hard requirement.

Enable Row Level Security (the real security boundary)

A Flutter Web app ships its client key inside the JavaScript bundle, so anyone can read it from the browser. The key is not a secret. RLS is what protects the data. Go to the SQL Editor



alter table public.focus_hub enable row level security;
create policy "Users see own tasks only"
on public.focus_hub    
for all                         
to authenticated
using ( (select auth.uid()) = "User_id" )
with check ( (select auth.uid()) = "User_id" );

  • auth.uid() this is the id from the JWT of the person making the request from the web.
  • using this decides which existing rows you may touch and with check decides what you may write.
  • (select auth.uid()) instead of bare auth.uid() lets Postgres evaluate it once per query instead of once per row.
  • The app uses upsert, which needs both the insert and update policies (and select for the returned row).
  • Policy are the rules and RLS are like switchs that enables security check for our table.
  • Even if with RLS on and no policy, nobody gets any rows. That is the safe default.

Test it: in the dashboard, open the Table Editor, we can see the table and one RLS policy.

For realtime update

The app listens for INSERT / UPDATE / DELETE action with a build-in function onPostgresChanges. Two settings are required, so again open the SQL editor


alter publication supabase_realtime add table public.focus_hub;
alter table public.focus_hub replica identity full;

You can also toggle the first one in Database → Publications → supabase_realtime. The second line can't be toggle in the UI, so run it in SQL. Without it, DELETE events arrive without the full old row, which can break the delete handling in task_view.dart.

Known limits:

  • DELETE events cannot be filtered by column. The User_id filter in the app applies to INSERT/UPDATE only, so a DELETE payload may arrive for other users' rows. UUIDs are unguessable, so this leaks nothing useful, but the app should handle a Task_id it does not have locally.
  • In a DELETE payload the new record is empty. Read oldRecord['Task_id'] directly. Do not call fromSupabaseMap on it, because DateTime.parse(null) throws.

Timestamps and time zones

TaskModel currently sends DateTime.now().toIso8601String(). For a local time this has no Z / offset, and Postgres reads it as UTC. Users in other time zones then get shifted values, and the updatedAt.isAfter(...) conflict check in pullTasksFromSupabase compares unlike things.

Recommended fix in task_model.dart:

'Created_date': createdAt.toUtc().toIso8601String(),
'Updated_at':   updatedAt.toUtc().toIso8601String(),

and when reading, convert back with DateTime.parse(row['Updated_at']).toLocal(). Keep Deadline as a plain local date, because it is a calendar day, not an instant.

Clone this wiki locally