Skip to content

Web Data Layer

The web app does not define its own separate entity layer.

Instead:

  • entity types, collections, queries, actions, schema, and PowerSync helpers come from @life-manager/db
  • shared workflows, timer engine, formatting, gradients, and helper logic come from @life-manager/shared
  • apps/web provides the browser-specific bridge around those packages

That bridge is concentrated under src/db/:

src/db/
confirmations/
useWebConfirmationAdapter.tsx
powersync/
Connector.ts
db.ts
PowerSyncProvider.tsx
queries/
use-profile-query.ts
index.ts
supabase/
supabaseAdminClient.ts
supabaseServerClient.ts

src/routes/__root.tsx calls getAuthUser() in beforeLoad() and exposes the authenticated user in router context.

2. Web creates a browser PowerSync database singleton

Section titled “2. Web creates a browser PowerSync database singleton”

src/db/powersync/db.ts creates:

  • a PowerSyncDatabase from @powersync/web
  • a browser SupabaseConnector
  • eager initCollections(db) so collection hooks do not race provider mounting

src/db/powersync/PowerSyncProvider.tsx is the web runtime wrapper around the shared PowerSync context:

  • initializes the connector
  • connects only once even if multiple calls race
  • tracks status: idle | initializing | ready | error
  • disconnects on sign-out
  • reconnects when the browser comes back online

4. WebCollectionsProvider injects web adapters

Section titled “4. WebCollectionsProvider injects web adapters”

src/components/WebCollectionsProvider.tsx passes web-specific dependencies into CollectionsProvider from @life-manager/db/core:

  • userId from route context
  • success, error, and overlap toast functions
  • browser confirmation adapter

That is the key point where the shared collection layer becomes web-aware.

Most reads should come from package hooks:

import { useWorkProjects, useTimeTrackers } from "@life-manager/db/queries";

These hooks are powered by the shared collection registry and the active PowerSync runtime.

Writes usually go through package actions:

import { useTimeTrackerActions } from "@life-manager/db/actions";

Those actions can trigger:

  • collection updates
  • shared validation and workflows
  • PowerSync sync to Supabase
  • web notifications and confirmations through the adapters injected by WebCollectionsProvider

src/db/queries/use-profile-query.ts is the main example of a web-only convenience hook:

  • it reads user.id from TanStack Router context
  • then delegates to useProfileByUserId() from @life-manager/db/queries

This is the preferred pattern for web wrappers: thin, context-aware, and without duplicating domain logic.

Example Flow: Timer Stop To Persisted Work Entry

Section titled “Example Flow: Timer Stop To Persisted Work Entry”

One representative write flow in the current app is the time tracker:

  1. useTimeTracker(timer) receives a persisted TimeTracker entity.
  2. It passes shared queries, actions, settings, and browser callbacks into useTimeTrackerEngine().
  3. On submit or stop, the engine uses package actions to create workTimeEntries, update linked appointments, and stop the tracker.
  4. The collection layer syncs those changes through PowerSync to Supabase.
  5. The web layer only keeps computed display state in timeTrackerManagerStore.

This is the broader design: persisted truth in the shared DB layer, browser-specific presentation and runtime state in apps/web.

There are two separate persistence mechanisms in the web app.

  • database file: powersync.db
  • storage backend: browser-side PowerSync/Web storage
  • purpose: synced domain data available offline
  • storage backend: localStorage
  • purpose: UI preferences and view state
  • examples: settings colors, selected finance tab, calendar zoom

These layers are intentionally independent.

src/lib/cleanupOnLogout.ts clears both persistence layers:

  • resets local Zustand stores
  • disconnects and clears PowerSync data
  • removes relevant localStorage keys
  • attempts to delete IndexedDB databases

That prevents one user’s local browser state from leaking into another session.

Direct Supabase usage in apps/web is narrow and infrastructure-focused:

  • browser auth and session handling in src/db/powersync/Connector.ts
  • SSR and admin helpers under src/db/supabase/
  • auth-oriented server actions and endpoints under src/actions/ and src/routes/api/

Feature code should normally stay at the @life-manager/db/queries and @life-manager/db/actions level rather than talking to Supabase directly.