Database client
The app queries Supabase through a typed client and React Query hooks. Supabase handles database queries; React Query handles caching, refetching and loading state.
Each feature under src/features/ keeps its Supabase queries in a repository.ts and
its React Query hooks in hooks/. Components use the hooks; the hooks manage loading
and caching, and the repository functions contain the queries. Keep new queries in the
same structure so screens do not need to manage database requests.
Files
- supabase.ts - Typed Supabase client
- database.types.ts - Generated schema types
- repository.ts - Scan queries and result types
- hooks/scans.ts - Scan queries and mutations
- repository.ts - Generated image queries
- hooks/generations.ts - Generated image queries and mutations
- repository.ts - Conversation and message queries
- hooks/thread.ts - Conversation queries and mutations
Repository functions
Each repository file exports plain functions that throw on error, leaving the calling hook nothing to unwrap.
The scan repository defines Scan and toScan to give the result JSON column its
scan result shape. These functions use those definitions:
import { supabase } from '@/lib/supabase';
// Keep the Scan type and toScan helper defined in this repository.
export const getScans = async (): Promise<Scan[]> => {
const { data, error } = await supabase.from('scans').select().order('created_at', { ascending: false });
if (error) throw error;
return data.map(toScan);
};
export const deleteScan = async (id: string): Promise<void> => {
const { error } = await supabase.from('scans').delete().eq('id', id);
if (error) throw error;
};There’s no where user_id = … filter to write. Postgres row-level security handles
that already, returning only the signed-in user’s rows.
React Query hooks
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getScans, deleteScan } from '@/features/scan/repository';
export const useScans = () => useQuery({ queryKey: ['scans'], queryFn: getScans });
export const useDeleteScan = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteScan,
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ['scans'] });
},
});
};Mutations invalidate the matching query key, and the list refreshes itself.
Usage
Import useScans from @/features/scan/hooks/scans and use its data, isLoading
and error fields to render the list, loading state and errors. Call useDeleteScan()
at the top of your component, then call the returned mutation’s mutate(scanId) from
an event handler to delete a scan.
Add your own entity
- Create and apply a migration, including RLS policies.
- Regenerate the database types.
- Add the query functions to the owning feature’s
src/features/<feature>/repository.ts, or to a new feature’s repository. Queries shared by the whole app, such as the user profile, belong insrc/lib/. - Add the React Query wrappers under
src/features/<feature>/hooks/, or insrc/hooks/when they are shared.
Verify
Create a record in the app, reopen the screen and confirm the record is still there. Check that mutations refresh the corresponding query. Sign in with a second test account to confirm the first account’s private records are hidden.