Skip to Content
DatabaseDatabase Client

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:

src/features/scan/repository.ts
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

src/features/scan/hooks/scans.ts
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

  1. Create and apply a migration, including RLS policies.
  2. Regenerate the database types.
  3. 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 in src/lib/.
  4. Add the React Query wrappers under src/features/<feature>/hooks/, or in src/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.

Last updated on