Web Data Layer
What Actually Lives In apps/web
Section titled “What Actually Lives In apps/web”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/webprovides 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.tsRuntime Stack
Section titled “Runtime Stack”1. Root route resolves the user
Section titled “1. Root route resolves the user”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
PowerSyncDatabasefrom@powersync/web - a browser
SupabaseConnector - eager
initCollections(db)so collection hooks do not race provider mounting
3. PowerSyncProvider manages lifecycle
Section titled “3. PowerSyncProvider manages lifecycle”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:
userIdfrom route context- success, error, and overlap toast functions
- browser confirmation adapter
That is the key point where the shared collection layer becomes web-aware.
Read And Write Paths
Section titled “Read And Write Paths”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
Section titled “Writes”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
Web-Specific Query Wrapper
Section titled “Web-Specific Query Wrapper”src/db/queries/use-profile-query.ts is the main example of a web-only convenience hook:
- it reads
user.idfrom 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:
useTimeTracker(timer)receives a persistedTimeTrackerentity.- It passes shared queries, actions, settings, and browser callbacks into
useTimeTrackerEngine(). - On submit or stop, the engine uses package actions to create
workTimeEntries, update linked appointments, and stop the tracker. - The collection layer syncs those changes through PowerSync to Supabase.
- 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.
Offline Persistence
Section titled “Offline Persistence”There are two separate persistence mechanisms in the web app.
PowerSync persistence
Section titled “PowerSync persistence”- database file:
powersync.db - storage backend: browser-side PowerSync/Web storage
- purpose: synced domain data available offline
Zustand persistence
Section titled “Zustand persistence”- storage backend:
localStorage - purpose: UI preferences and view state
- examples: settings colors, selected finance tab, calendar zoom
These layers are intentionally independent.
Logout Cleanup
Section titled “Logout Cleanup”src/lib/cleanupOnLogout.ts clears both persistence layers:
- resets local Zustand stores
- disconnects and clears PowerSync data
- removes relevant
localStoragekeys - attempts to delete IndexedDB databases
That prevents one user’s local browser state from leaking into another session.
Supabase Touchpoints
Section titled “Supabase Touchpoints”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/andsrc/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.