Skip to main content
The Tuturuuu platform uses a layered approach to data fetching, choosing the right strategy based on use case, caching requirements, and user experience needs.
The client-side fetch boundary is TanStack Query (React Query v5) hooks backed by @tuturuuu/internal-api helpers, which call the REST /api/v1/... endpoints. tRPC in apps/web/src/trpc is an intentional stub during the TanStack + Rust migration (it exposes only a healthCheck procedure), so this page does not use trpc.* routers for product data. See tRPC Implementation for the scaffold details.
For the dedicated TanStack Start frontend plus Rust backend migration, use platform/architecture/tanstack-rust-migration. TanStack loaders and server functions are allowed as a BFF layer, while product data ownership moves behind Rust endpoints and packages/internal-api facades.

Heavy Dashboards

For queue-heavy dashboards that run expensive row + summary RPCs (for example posts review):
  • Keep the route page server-only for auth, permissions, and canonical URL normalization.
  • Move the actual table + summary fetch into a client shell with TanStack Query.
  • Fetch through @tuturuuu/internal-api helpers, not ad-hoc client fetch calls.
  • Avoid router.refresh() for routine list refreshes; prefer query refetch/invalidation.

Server And Service Orchestration

When a server-side data flow coordinates multiple async resources, expected failures, retry/scheduling policy, or replaceable dependencies, use @tuturuuu/utils/effect inside the server/shared helper and keep the route or client boundary plain. Effect complements this page’s data-fetching strategy; it does not replace React state, TanStack Query, packages/internal-api, Zod, or generated database types. Use Effect for server/service orchestration:
Client components should still call TanStack Query hooks and internal API helpers. Server Components, Server Actions, API routes, cron jobs, and shared packages may use Effect behind their public boundary when it improves typed failure handling or orchestration clarity.

Builder Migration Pattern (Education Module Groups)

For highly interactive editors (for example the course builder with module groups + drag/drop):
  • Keep the route page as a thin server-side auth/permission gate.
  • Move list data loading into a client shell backed by React Query.
  • Use @tuturuuu/internal-api helpers as the client fetch boundary (listWorkspaceCourseModuleGroups, listWorkspaceCourseModules, and reorder helpers).
  • Avoid router.refresh() for routine drag/drop updates; mutate via API and invalidate the corresponding query keys.
  • When a drag/drop move changes both membership and order, await the membership mutation before firing any follow-up reorder mutations, and invalidate only the affected query keys instead of every list in the builder.
  • Keep create/update payload contracts explicit (module_group_id is required for new course modules).

Strategy Preference Order

Choose the earliest applicable strategy:
  1. Pure Server Component (RSC) - Read-only, cacheable, SEO-critical data
  2. Server Action - Mutations returning updated state to RSC
  3. RSC + Client Hydration - When background refresh needed
  4. React Query (Client-Side) - Interactive, rapidly changing state
  5. Realtime Subscriptions - Live updates materially improve UX

1. Pure Server Component (RSC)

When to Use

Best For:
  • Initial page load data
  • SEO-critical content
  • Rarely changing data
  • Read-only operations
  • Database queries that don’t need real-time updates
Not For:
  • Interactive forms
  • Frequent updates
  • Client-side state
  • Real-time data

Implementation

Caching Strategy

Loading States

Error Handling

2. Server Actions

When to Use

Best For:
  • Form submissions
  • Mutations with server-side validation
  • Operations requiring auth checks
  • Redirects after mutations
  • Progressive enhancement
Not For:
  • Read operations (use RSC instead)
  • Complex client-side state management
  • Real-time updates

Implementation

Client Usage

3. RSC + Client Hydration

When to Use

Best For:
  • Initial server render with client updates
  • Data that changes frequently
  • Combining SEO with interactivity
  • Background refresh patterns

Implementation

The server renders initial data and passes it as initialData to a TanStack Query hook. Background refresh is handled by Query’s refetchIntervalnot a useEffect + setInterval loop. AGENTS.md forbids useEffect for data fetching.
The client hook fetches through an @tuturuuu/internal-api helper (which calls the REST /api/v1 endpoint) rather than re-running Supabase queries from the browser. This keeps authorization on the server and gives Query a single, cacheable fetch boundary.

4. TanStack Query (Client-Side)

When to Use

Best For:
  • Interactive dashboards
  • Rapidly changing data
  • Complex client-side state
  • Optimistic updates
  • Automatic background refetching
Not For:
  • SEO-critical content
  • Initial page load (prefer RSC)

Implementation with Internal API helpers

Client queries call @tuturuuu/internal-api helpers, which hit the REST /api/v1/... endpoints where authorization lives. This is the actual fetch boundary in apps/web — there are no trpc.tasks.* / trpc.boards.* routers (tRPC is a stub).

Query Key Conventions

Use stable array query keys:

Thin Server Gate + Client Query Shell

For dense dashboard surfaces that need URL-driven filtering, pagination, and frequent background refreshes, prefer a thin server gate plus a client query shell:
  1. Keep the route page server-side only for auth, permission checks, redirects, and metadata.
  2. Move interactive data loading into client hooks backed by TanStack Query.
  3. Back those hooks with workspace-scoped Internal API helpers instead of ad hoc fetch('/api/...') calls.
  4. Store queryable UI state such as page, pageSize, q, sortBy, and path in nuqs.
  5. Invalidate query keys after mutations instead of relying on router.refresh().
This pattern is now the default for file-explorer style dashboard pages such as Drive, where the user expects immediate client-side updates while the server still owns authorization.

Optimistic Updates

Use useMutation with the queryClient cache helpers. Drive the mutation through an @tuturuuu/internal-api helper so the write goes through the same authorized REST boundary as reads.

Cache Invalidation

Query keys are prefix-matched by default, so invalidating ['workspaces', wsId, 'tasks'] also invalidates every more specific key under it (e.g. ['workspaces', wsId, 'tasks', taskId]). Keep keys ordered broad → narrow so partial invalidation stays predictable.

5. Realtime Subscriptions

When to Use

Best For:
  • Collaborative features
  • Chat applications
  • Live dashboards
  • Multiplayer features
  • Notification systems
Not For:
  • Infrequent updates
  • Static content
  • SEO-critical data

Implementation

The initial fetch is a TanStack Query hook (through an @tuturuuu/internal-api helper), never a useEffect fetch. A useEffect is used only for the subscription lifecycle (a genuine side effect), and the realtime payloads mutate the query cache so the live data and the cached data stay in sync.

TanStack Query Guidelines

The shared QueryClient lives in apps/web/src/trpc/query.ts (makeQueryClient) and ships a default staleTime of 30s plus rate-limit-aware retry handling. Override per-query options only when a surface needs different freshness.

Query Configuration

Prefetching

Parallel Queries

For many dynamic queries, prefer useQueries over hand-listing hooks.

Dependent Queries

The exact helper names above (getWorkspaceTask, listWorkspaceTaskAssignees, getWorkspaceMembers) are illustrative — browse packages/internal-api/src for the real exports before wiring a hook. listWorkspaceTasks and listWorkspaceTaskBoards exist today.

Decision Matrix

Best Practices

✅ DO

  1. Start with RSC
  2. Use specific query keys
  3. Set appropriate staleTime
  4. Implement optimistic updates for better UX
  5. Invalidate narrowly
  6. Fetch through Internal API helpers

❌ DON’T

  1. Don’t use client fetching for SEO content
  2. Don’t skip staleTime
  3. Don’t invalidate globally
  4. Don’t use Realtime for infrequent updates