Repository navigation
Supabase Database Setup
- 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
anonJWT key to the newsb_publishable_...key. Both are public client keys. The secret /service_rolekey must never go into the Flutter app or the repo.
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");
| 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 lineisdate), keep what works. Only the names are a hard requirement.
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. -
usingthis decides which existing rows you may touch andwith checkdecides what you may write. -
(select auth.uid())instead of bareauth.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). -
Policyare the rules andRLSare 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.
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_idfilter 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 aTask_idit does not have locally. - In a DELETE payload the new record is empty. Read
oldRecord['Task_id']directly. Do not callfromSupabaseMapon it, becauseDateTime.parse(null)throws.
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.