Data Layer

Typed Supabase services, TanStack Query, and the subscription read-model.

The Electron template keeps data access small and replaceable: typed Supabase calls live in app/services/, TanStack Query owns renderer cache behavior, and zustand stores hold session and entitlement state that needs to be read across routes.

Stack

LayerFileRole
Supabase clientapp/lib/supabase/client.tscreateClient<Database>() with secure desktop session storage.
Database typesapp/lib/supabase/database.types.tsHand-maintained Supabase type map used by service modules.
Query clientapp/lib/query/client.tsOne global QueryClient with retry: 1, staleTime: 30000, and refetchOnWindowFocus: false.
Feature servicesapp/services/*.tsPure modules for Supabase tables, Storage, Edge Functions, analytics, and license activation.
Auth storeapp/stores/auth-store.tsSession, user, profile, auth-state subscription, and sign-out.
Subscription storeapp/stores/subscription-store.tsAccount-bound entitlement read model when billing is enabled.

Use services as the swap point for your own backend. Components should call a service through a query, mutation, or store action instead of scattering supabase.from(...) calls through UI code.

Todos example

Todos are an Example feature built to show the end-to-end pattern:

PieceFileNotes
Serviceapp/services/todos.tsLists, creates, toggles, and deletes rows from public.todos.
Query keytodosQueryKey(userId) in app/lib/query/client.tsKeeps per-user cache entries separate.
UIapp/routes/showcase/demos/TodosDemo.tsxRequires a signed-in user, uses useQuery and useMutation, and updates the cache on mutation success.
RLSSupabase Setup SQLUsers can only select, insert, update, and delete rows where user_id = auth.uid().

listTodos() orders by sort_order and then newest created_at. Mutations throw on Supabase errors so UI code can show localized failure toasts.

Profile and avatar example

Profile data uses app/services/profile.ts, while avatar uploads use app/services/avatar.ts.

  • getProfile(userId) selects one profiles row by id.
  • updateProfile(userId, patch) writes profile fields and updates updated_at.
  • uploadAvatar() validates PNG/JPEG/WebP and 5 MB max size, uploads to avatars/<user-id>/avatar.<ext>, then stores that path in profiles.avatar_url.
  • getAvatarUrl() converts the stored path into a seven-day signed Storage URL.
  • removeAvatar() deletes the Storage object and clears avatar_url.

These services depend on the inline schema and Storage policies from Supabase Setup.

Subscription read model

Billing uses a read model instead of trusting local purchase state:

PieceFileRule
Table readapp/services/subscription.tsReads the signed-in user's subscriptions row with maybeSingle().
Storeapp/stores/subscription-store.tsDerives free, active, expired, unknown, or disabled.
Product mappingshared/price-config.tsMaps Creem product IDs to free, pro, or lifetime.
WritesSupabase Edge FunctionsClients never write subscriptions or payments; RLS grants SELECT only.

When VITE_BILLING_ENABLED=false, the subscription store enters disabled and does not read Supabase. When a session or network read fails, it uses unknown, not free, so transient failures do not incorrectly downgrade paid users or unlock paid features.

The subscriptions and payments shape matches the account-bound entitlement read model used by the mobile templates' RevenueCat webhook, so teams that own both can share one backend concept.

Add a feature

Add SQL and RLS

Create the table, indexes, triggers, and owner-scoped policies. For desktop client writes, always assume the renderer is public and let RLS enforce access.

Update database types

Regenerate or hand-edit app/lib/supabase/database.types.ts so services can import Tables<'your_table'>, TablesInsert<'your_table'>, and TablesUpdate<'your_table'>.

Create a service module

Put Supabase table, Storage, or Edge Function calls under app/services/. Normalize errors there and keep component code focused on UI state.

Add query and mutation usage

Add stable query keys in app/lib/query/client.ts when shared keys are useful, then call the service from useQuery, useMutation, or a small store action.

Render the route

Add the route, sidebar or command palette entry, i18n strings, and signed-out state. If the feature touches the main process, expose it through Conveyor instead of importing Electron APIs in the renderer.

Offline and network behavior

The current query client is simple: one retry, 30 seconds of freshness, and no window-focus refetch. It does not persist TanStack Query cache to disk.

app/hooks/use-online-status.ts wraps browser online and offline events with useSyncExternalStore. This reflects the OS network interface, not a guaranteed Supabase reachability check, and is best used as a warning signal.

For critical paid or account data, follow the subscription store's pattern: represent transport failures as unknown so the UI can hold a neutral state instead of pretending the user is free or entitled.

On this page