Example Features
Todos and profile as replaceable patterns for your own features.
Two example features ship wired end to end so you can copy the pattern rather than invent it: a Todos CRUD list and a profile + avatar editor. Both are marked Example — working demos meant to be replaced with your own product data. Both need the Supabase tables from Supabase Setup.
Todos (Example)
Todos is the worked example of the full data path — service → TanStack Query →
component. The service (app/services/todos.ts) is a set of pure functions over
the typed Supabase client, with no component imports:
// app/services/todos.ts
export const listTodos = async (): Promise<Todo[]> => {
const { data, error } = await supabase
.from('todos')
.select(TODO_COLUMNS)
.order('sort_order', { ascending: true })
.order('created_at', { ascending: false })
if (error) throw new Error(error.message)
return (data ?? []) as Todo[]
}
// createTodo, toggleTodo, deleteTodo …TodosDemo (app/routes/showcase/demos/TodosDemo.tsx) consumes those services
through TanStack Query — useQuery for the list, useMutation for create/toggle/
delete — and owns cache invalidation in the UI layer. RLS enforces that a user
only sees their own rows (see Data Layer), so the
service never filters by user id on read.
The service is the swap point. To back Todos with your own API instead of
Supabase, rewrite app/services/todos.ts and leave the query hooks and
component untouched.
Profile + avatar (Example)
The Account page (/account, AccountPage.tsx) edits the signed-in user's
profile. Two services back it:
app/services/profile.ts—getProfile/updateProfileover theprofilestable (full_name,bio,phone,location,avatar_url).app/services/avatar.ts— the avatar upload/read cycle against theavatarsstorage bucket.
The avatar flow is a useful pattern in itself:
- Validate the picked image client-side —
guardAvatarInputrejects empty files, anything over 5 MB, or a MIME type outside PNG/JPEG/WebP. - Upload to
avatars/<userId>/avatar.<ext>withupsert: true, then store that path on the profile row. - Read back through
getAvatarUrl, which mints a signed URL (7-day TTL) and appends a cache-busting key fromupdated_at, so a re-upload shows immediately.
The bucket is private; reads go through signed URLs rather than public links. Provision the bucket and its storage policies as shown in Supabase Setup.
What's intentionally not here
This is a desktop shell, not a SaaS UI kit. There is deliberately:
- no dashboard-with-charts,
- no admin panel or user-management tables,
- no CRM/content-management scaffolding.
The examples exist to demonstrate the shape of a feature — a typed service,
query hooks, a page, RLS — not to be a component library. For building blocks
(buttons, dialogs, forms, tables, and the rest of shadcn/ui), open the
Components gallery at /showcase/components. See
Showcase.
Copy the pattern
To add your own CRUD feature end to end, mirror Todos:
SQL + RLS
Create the table with an owner column and owner-only RLS policies (see Data Layer).
Types
Regenerate or hand-edit app/lib/supabase/database.types.ts.
Service
Add app/services/<feature>.ts as pure functions over the typed client.
Query hooks + page
Wrap the service in useQuery/useMutation and render it in a route (see
UI & Pages).
