Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:29:26 +08:00
commit a0db888440
20 changed files with 1989 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"name": "research",
"description": "API research and documentation retrieval using Firecrawl and Context7, with multi-agent synthesis capabilities",
"version": "1.0.0",
"author": {
"name": "Grey Haven Studio"
},
"skills": [
"./skills/tanstack-patterns"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# research
API research and documentation retrieval using Firecrawl and Context7, with multi-agent synthesis capabilities

109
plugin.lock.json Normal file
View File

@@ -0,0 +1,109 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:greyhaven-ai/claude-code-config:grey-haven-plugins/research",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "b3bffd9e1fab2ecee15cfa1bec100fa1b61bac4f",
"treeHash": "89dd07fce270d0d921de5a2a891fd36c9d887f3bb8309745ab7cf8e8e305c7b6",
"generatedAt": "2025-11-28T10:17:04.728062Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "research",
"description": "API research and documentation retrieval using Firecrawl and Context7, with multi-agent synthesis capabilities",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "b40766f40603bd6d8e6aba6fe61f8fcc68054adf96baf38555c4ab7440ef282e"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "5975e88997aa6a60d88f89b30e632b3d435b9d48f69f3dd129e823e78ecd3abd"
},
{
"path": "skills/tanstack-patterns/SKILL.md",
"sha256": "2bd490214227f62052755ebbfd9ea48222dff31933b00ef58233384c09628ed2"
},
{
"path": "skills/tanstack-patterns/checklists/tanstack-checklist.md",
"sha256": "7bb07854406175e05dc0142885ee7e1505c1942badd55d911d8f904ae8ff465b"
},
{
"path": "skills/tanstack-patterns/examples/advanced-patterns.md",
"sha256": "40df56b26a5e02ecf4733029497b8352ffa232ae3decf71460f8a6372ea1ee9d"
},
{
"path": "skills/tanstack-patterns/examples/query-patterns.md",
"sha256": "6ac4d217bab14a47267e56f26eecf2be528764352ed7c6eb22f23ad83f503706"
},
{
"path": "skills/tanstack-patterns/examples/INDEX.md",
"sha256": "255a468e9a02da8193a396dffe1ab9daea10ace4980067c90d2b73e237423112"
},
{
"path": "skills/tanstack-patterns/examples/router-patterns.md",
"sha256": "f78b5303a920c9ef878b5952b64e9cbcb9090e2da0483ce2ac3e7d8364e9ea6a"
},
{
"path": "skills/tanstack-patterns/examples/server-functions.md",
"sha256": "ad0ec65c7769ffdf73a5b66f5cc478e92a189d4e37d79ef0aa8b586444f109cc"
},
{
"path": "skills/tanstack-patterns/templates/root-route.tsx",
"sha256": "199f2cb0735917d0e1ed05479caefcf8831f27d6fb7be9eeec84946b25e81634"
},
{
"path": "skills/tanstack-patterns/templates/server-function.ts",
"sha256": "d8c5a40a38e8db43fb3a5f8233b50017c5e73c90a1abb746060cb52cbc0bae40"
},
{
"path": "skills/tanstack-patterns/templates/page-route.tsx",
"sha256": "a66080d09814ba4c9f7476597b55157c93f7834e61429c25fa19d819c07c25db"
},
{
"path": "skills/tanstack-patterns/templates/auth-layout.tsx",
"sha256": "c4bb295962127ea9d5b8a170256318397da55e23c427931eda83e7e9e656967d"
},
{
"path": "skills/tanstack-patterns/templates/custom-hook.ts",
"sha256": "d60dbeff75d5bb9c2c02b0291c3ec143f2ed60960765bb5e970aa636c62b620b"
},
{
"path": "skills/tanstack-patterns/reference/query-config.md",
"sha256": "63b778c225e846492c2fd96138c158aa17f7ca85b86d28b36d2111af989332fe"
},
{
"path": "skills/tanstack-patterns/reference/caching-strategy.md",
"sha256": "2ad1c6ca68f0957b7ae8bb16460b1258301acc01bf2332d92d007b4cd4f1d8d7"
},
{
"path": "skills/tanstack-patterns/reference/multi-tenant.md",
"sha256": "a4cfdac5361daff03791edb103f3e921fc94bcc7c2654096da7cb988e3e7deca"
},
{
"path": "skills/tanstack-patterns/reference/router-config.md",
"sha256": "5c0cdb6754b88cb71d3dea49bfde836c6b88fb44d5d0cc4711ab19b1cba47dcf"
},
{
"path": "skills/tanstack-patterns/reference/INDEX.md",
"sha256": "850cb804818614298c4a4bb78b0faf2d06e208195143a621b513ca15755b4d10"
}
],
"dirSha256": "89dd07fce270d0d921de5a2a891fd36c9d887f3bb8309745ab7cf8e8e305c7b6"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,199 @@
---
name: grey-haven-tanstack-patterns
description: Apply Grey Haven's TanStack ecosystem patterns - Router file-based routing, Query data fetching with staleTime, and Start server functions. Use when building React applications with TanStack Start.
---
# Grey Haven TanStack Patterns
Follow Grey Haven Studio's patterns for TanStack Start, Router, and Query in React 19 applications.
## TanStack Stack Overview
Grey Haven uses the complete TanStack ecosystem:
- **TanStack Start**: Full-stack React framework with server functions
- **TanStack Router**: Type-safe file-based routing with loaders
- **TanStack Query**: Server state management with caching
- **TanStack Table** (optional): Data grids and tables
- **TanStack Form** (optional): Type-safe form handling
## Critical Patterns
### 1. File-Based Routing Structure
```
src/routes/
├── __root.tsx # Root layout (wraps all routes)
├── index.tsx # Homepage (/)
├── _authenticated/ # Protected routes group (underscore prefix)
│ ├── _layout.tsx # Auth layout wrapper
│ ├── dashboard.tsx # /dashboard
│ └── settings/
│ └── index.tsx # /settings
└── users/
├── index.tsx # /users
└── $userId.tsx # /users/:userId (dynamic param)
```
**Key conventions**:
- `__root.tsx` - Root layout with QueryClient provider
- `_authenticated/` - Protected route groups (underscore prefix)
- `_layout.tsx` - Layout wrapper for route groups
- `$param.tsx` - Dynamic route parameters
### 2. TanStack Query Defaults
```typescript
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60000, // 1 minute default
retry: 1,
refetchOnWindowFocus: false,
},
},
});
```
### 3. Query Key Patterns
```typescript
// ✅ CORRECT - Specific to general
queryKey: ["user", userId]
queryKey: ["users", { tenantId, page: 1 }]
queryKey: ["organizations", orgId, "teams"]
// ❌ WRONG
queryKey: [userId] // Missing resource type
queryKey: ["getUser", userId] // Don't include function name
queryKey: [{ id: userId }] // Object first is confusing
```
### 4. Server Functions with Multi-Tenant
```typescript
// ALWAYS include tenant_id parameter
export const getUserById = createServerFn("GET", async (
userId: string,
tenantId: string
) => {
const user = await db.query.users.findFirst({
where: and(
eq(users.id, userId),
eq(users.tenant_id, tenantId) // Multi-tenant isolation!
),
});
if (!user) throw new Error("User not found");
return user;
});
```
### 5. Route Loaders
```typescript
export const Route = createFileRoute("/_authenticated/dashboard")({
// Loader fetches data on server before rendering
loader: async ({ context }) => {
const tenantId = context.session.tenantId;
return await getDashboardData(tenantId);
},
component: DashboardPage,
});
function DashboardPage() {
const data = Route.useLoaderData(); // Type-safe loader data
return <div>...</div>;
}
```
### 6. Mutations with Cache Invalidation
```typescript
const mutation = useMutation({
mutationFn: (data: UserUpdate) => updateUser(userId, data),
// Always invalidate queries after mutation
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["user", userId] });
},
});
```
## Caching Strategy
Grey Haven uses these staleTime defaults:
| Data Type | staleTime | Use Case |
|-----------|-----------|----------|
| Auth data | 5 minutes | User sessions, tokens |
| User profiles | 1 minute | User details |
| Lists | 1 minute | Data tables, lists |
| Static/config | 10 minutes | Settings, configs |
| Realtime | 0 (always refetch) | Notifications |
```typescript
const STALE_TIMES = {
auth: 5 * 60 * 1000, // 5 minutes
user: 1 * 60 * 1000, // 1 minute
list: 1 * 60 * 1000, // 1 minute
static: 10 * 60 * 1000, // 10 minutes
realtime: 0, // Always refetch
};
```
## Supporting Documentation
All supporting files are under 500 lines per Anthropic best practices:
- **[examples/](examples/)** - Complete code examples
- [router-patterns.md](examples/router-patterns.md) - File-based routing, layouts, navigation
- [query-patterns.md](examples/query-patterns.md) - Queries, mutations, infinite queries
- [server-functions.md](examples/server-functions.md) - Creating and using server functions
- [advanced-patterns.md](examples/advanced-patterns.md) - Dependent queries, parallel queries, custom hooks
- [INDEX.md](examples/INDEX.md) - Examples navigation
- **[reference/](reference/)** - Configuration references
- [router-config.md](reference/router-config.md) - Router setup and configuration
- [query-config.md](reference/query-config.md) - QueryClient configuration
- [caching-strategy.md](reference/caching-strategy.md) - Detailed caching patterns
- [multi-tenant.md](reference/multi-tenant.md) - Multi-tenant patterns with RLS
- [INDEX.md](reference/INDEX.md) - Reference navigation
- **[templates/](templates/)** - Copy-paste ready templates
- [root-route.tsx](templates/root-route.tsx) - Root layout template
- [auth-layout.tsx](templates/auth-layout.tsx) - Protected layout template
- [page-route.tsx](templates/page-route.tsx) - Basic page route template
- [server-function.ts](templates/server-function.ts) - Server function template
- [custom-hook.ts](templates/custom-hook.ts) - Custom query hook template
- **[checklists/](checklists/)** - Pre-PR validation
- [tanstack-checklist.md](checklists/tanstack-checklist.md) - TanStack patterns checklist
## When to Apply This Skill
Use this skill when:
- Building TanStack Start applications
- Implementing routing with TanStack Router
- Managing server state with TanStack Query
- Creating server functions for data fetching
- Optimizing query performance with caching
- Implementing multi-tenant data access
- Setting up authentication flows with route protection
- Building data-heavy React applications
## Template Reference
These patterns are from Grey Haven's production template:
- **cvi-template**: TanStack Start + Router + Query + React 19
## Critical Reminders
1. **staleTime**: Default 60000ms (1 minute) for queries
2. **Query keys**: Specific to general (["user", userId], not [userId])
3. **Server functions**: Always include tenant_id parameter
4. **Multi-tenant**: Filter by tenant_id in all server functions
5. **Loaders**: Use for server-side data fetching before render
6. **Mutations**: Invalidate queries after successful mutation
7. **Prefetching**: Use for performance on hover/navigation
8. **Error handling**: Always handle error state in queries
9. **RLS**: Server functions use RLS-enabled database connection
10. **File-based routing**: Underscore prefix (_) for route groups/layouts

View File

@@ -0,0 +1,83 @@
# TanStack Patterns Checklist
**Use before creating PR for TanStack Start/Router/Query code.**
## TanStack Router
- [ ] File structure follows conventions (`__root.tsx`, `_layout.tsx`, `$param.tsx`)
- [ ] Root route includes QueryClient provider
- [ ] Protected routes use `beforeLoad` for auth checks
- [ ] Loaders use `loader` for data fetching
- [ ] Route params are type-safe
- [ ] Navigation uses type-safe `Link` component
- [ ] Redirects include return URL in search params
## TanStack Query
- [ ] QueryClient configured with Grey Haven defaults (staleTime: 60000)
- [ ] Query keys follow pattern: specific to general (["user", userId])
- [ ] Query keys don't include function names
- [ ] staleTime set appropriately for data type
- [ ] Error states handled in components
- [ ] Loading states handled in components
- [ ] Mutations invalidate queries after success
- [ ] Optimistic updates use `onMutate` and rollback on error
## Server Functions
- [ ] All server functions include `tenant_id` parameter
- [ ] Server functions use correct HTTP method (GET/POST/DELETE)
- [ ] Multi-tenant isolation with `tenant_id` filtering
- [ ] Error handling with appropriate error messages
- [ ] Return types are type-safe
- [ ] Server functions used with TanStack Query hooks
## Multi-Tenant
- [ ] `tenant_id` included in all server functions
- [ ] `tenant_id` included in all query keys
- [ ] `useTenant()` hook used for consistent tenant access
- [ ] Queries use `enabled: !!tenantId` guard
- [ ] RLS policies applied when using RLS pattern
- [ ] Test cases verify tenant isolation
## Caching Strategy
- [ ] Auth data: 5 minutes staleTime
- [ ] User profiles: 1 minute staleTime
- [ ] Lists: 1 minute staleTime
- [ ] Static/config: 10 minutes staleTime
- [ ] Realtime: 0 staleTime (always refetch)
- [ ] Prefetching used for performance optimization
## Performance
- [ ] Prefetching on hover/navigation
- [ ] Parallel queries for independent data
- [ ] Dependent queries use `enabled` option
- [ ] Infinite queries for pagination
- [ ] Background refetching configured appropriately
## Code Quality
- [ ] Custom hooks created for reusable query logic
- [ ] Query logic separated from component logic
- [ ] Type-safe throughout (params, return types, mutations)
- [ ] DevTools enabled in development
- [ ] No console errors or warnings
## Testing
- [ ] Route loaders tested
- [ ] Server functions tested
- [ ] Query hooks tested
- [ ] Mutation logic tested
- [ ] Tenant isolation tested
- [ ] Error handling tested
## Documentation
- [ ] Complex query logic documented
- [ ] Custom hooks documented
- [ ] Server functions documented with JSDoc
- [ ] Route loaders documented

View File

@@ -0,0 +1,48 @@
# TanStack Patterns Examples
Complete code examples for TanStack Start, Router, and Query.
## Available Examples
### [router-patterns.md](router-patterns.md)
File-based routing, layouts, dynamic routes, and navigation.
- Root layout with QueryClient
- Protected route layouts with auth
- Page routes with loaders
- Dynamic routes with params
- Type-safe navigation
### [query-patterns.md](query-patterns.md)
Queries, mutations, infinite queries, and prefetching.
- Query basics with Grey Haven defaults
- Query key patterns (correct vs wrong)
- Mutations with optimistic updates
- Infinite queries for pagination
- Prefetching for performance
- Error handling
### [server-functions.md](server-functions.md)
Creating and using TanStack Start server functions.
- GET/POST/DELETE server functions
- Using server functions in components
- Auth context in server functions
- Multi-tenant isolation patterns
- RLS with server functions
### [advanced-patterns.md](advanced-patterns.md)
Dependent queries, parallel queries, and custom hooks.
- Dependent queries with `enabled`
- Parallel queries for dashboards
- Custom query hooks
- Query composition
- Background refetching
- Suspense mode
- Placeholders and initial data
- Query cancellation
## Quick Navigation
**Need routing?** → [router-patterns.md](router-patterns.md)
**Need data fetching?** → [query-patterns.md](query-patterns.md)
**Need server-side code?** → [server-functions.md](server-functions.md)
**Need advanced patterns?** → [advanced-patterns.md](advanced-patterns.md)

View File

@@ -0,0 +1,271 @@
# Advanced TanStack Query Patterns
Complete examples for dependent queries, parallel queries, and custom hooks.
## Dependent Queries
```typescript
function UserOrganization({ userId }: { userId: string }) {
// First query: get user
const { data: user } = useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
});
// Second query: get organization (depends on user)
const { data: organization } = useQuery({
queryKey: ["organization", user?.organization_id],
queryFn: () => getOrganizationById(user!.organization_id),
enabled: !!user?.organization_id, // Only run if user exists
});
if (!user) return <LoadingSpinner />;
return (
<div>
<h2>{user.name}</h2>
{organization && <p>Organization: {organization.name}</p>}
</div>
);
}
```
## Parallel Queries
```typescript
function Dashboard() {
// Run multiple queries in parallel
const userQuery = useQuery({
queryKey: ["user", "current"],
queryFn: () => getCurrentUser(),
});
const statsQuery = useQuery({
queryKey: ["stats", "dashboard"],
queryFn: () => getDashboardStats(),
});
const recentQuery = useQuery({
queryKey: ["recent", "activity"],
queryFn: () => getRecentActivity(),
});
// All queries run simultaneously (parallel fetching)
const isLoading = userQuery.isLoading || statsQuery.isLoading || recentQuery.isLoading;
if (isLoading) return <LoadingSpinner />;
return (
<div>
<UserHeader user={userQuery.data} />
<StatsCards stats={statsQuery.data} />
<ActivityFeed activity={recentQuery.data} />
</div>
);
}
```
## Custom Query Hooks
```typescript
// src/lib/hooks/use-user.ts
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getUserById, updateUser } from "~/lib/server/functions/users";
export function useUser(userId: string) {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
staleTime: 60000,
});
const updateMutation = useMutation({
mutationFn: (data: UserUpdate) => updateUser(userId, data),
onSuccess: (updatedUser) => {
queryClient.setQueryData(["user", userId], updatedUser);
},
});
return {
user: query.data,
isLoading: query.isLoading,
error: query.error,
update: updateMutation.mutate,
isUpdating: updateMutation.isPending,
};
}
```
```typescript
// Using custom hook
function UserProfile({ userId }: { userId: string }) {
const { user, isLoading, update, isUpdating } = useUser(userId);
if (isLoading) return <LoadingSpinner />;
return (
<div>
<h1>{user.name}</h1>
<button
onClick={() => update({ name: "New Name" })}
disabled={isUpdating}
>
{isUpdating ? "Updating..." : "Update"}
</button>
</div>
);
}
```
## Query Composition
```typescript
// Base hook for fetching user
function useUserQuery(userId: string) {
return useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
staleTime: 60000,
});
}
// Composed hook with additional functionality
function useUserWithPermissions(userId: string) {
const userQuery = useUserQuery(userId);
const permissionsQuery = useQuery({
queryKey: ["user", userId, "permissions"],
queryFn: () => getUserPermissions(userId),
enabled: !!userQuery.data,
staleTime: 60000,
});
return {
user: userQuery.data,
permissions: permissionsQuery.data,
isLoading: userQuery.isLoading || permissionsQuery.isLoading,
error: userQuery.error || permissionsQuery.error,
};
}
```
## Background Refetching
```typescript
function RealtimeNotifications() {
const { data: notifications } = useQuery({
queryKey: ["notifications"],
queryFn: () => getNotifications(),
staleTime: 0, // Always stale
refetchInterval: 30000, // Refetch every 30 seconds
refetchIntervalInBackground: true, // Even when tab is not focused
});
return (
<div>
{notifications?.map((notif) => (
<NotificationItem key={notif.id} notification={notif} />
))}
</div>
);
}
```
## Suspense Mode
```typescript
import { Suspense } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
function UserProfile({ userId }: { userId: string }) {
// Suspense mode - no loading state needed
const { data: user } = useSuspenseQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
});
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<UserProfile userId="123" />
</Suspense>
);
}
```
## Placeholders and Initial Data
```typescript
function UserProfile({ userId }: { userId: string }) {
const queryClient = useQueryClient();
const { data: user } = useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
// Placeholder data while loading
placeholderData: () => {
// Try to find user in list cache
const users = queryClient.getQueryData<User[]>(["users"]);
return users?.find(u => u.id === userId);
},
staleTime: 60000,
});
return <div>{user?.name}</div>;
}
```
## Query Cancellation
```typescript
import { useQuery } from "@tanstack/react-query";
function SearchResults({ query }: { query: string }) {
const { data, isLoading } = useQuery({
queryKey: ["search", query],
queryFn: async ({ signal }) => {
// Pass AbortSignal to fetch
const response = await fetch(`/api/search?q=${query}`, { signal });
return response.json();
},
staleTime: 60000,
// Query automatically cancelled when query key changes
});
return <div>...</div>;
}
```
## Key Patterns
### When to Use Custom Hooks
- Reusing query logic across components
- Combining multiple queries
- Adding business logic to queries
- Simplifying component code
### When to Use Dependent Queries
- Second query needs data from first query
- Use `enabled` option to control execution
### When to Use Parallel Queries
- Multiple independent data sources
- No dependencies between queries
- Want to show loading state for all together
### When to Use Suspense
- React 18+ with Suspense boundaries
- Want declarative loading states
- Component tree can suspend
### Performance Tips
- Use `placeholderData` for instant UI feedback
- Use `staleTime` to reduce unnecessary refetches
- Use `refetchInterval` for real-time updates
- Cancel queries when component unmounts (automatic)

View File

@@ -0,0 +1,239 @@
# TanStack Query Patterns
Complete examples for queries, mutations, and infinite queries.
## Query Basics
```typescript
import { useQuery } from "@tanstack/react-query";
import { getUserById } from "~/lib/server/functions/users";
function UserProfile({ userId }: { userId: string }) {
const { data: user, isLoading, error } = useQuery({
queryKey: ["user", userId], // Array key for cache
queryFn: () => getUserById(userId),
staleTime: 60000, // Grey Haven default: 1 minute
// Data is "fresh" for 60 seconds, no refetch during this time
});
if (isLoading) return <LoadingSpinner />;
if (error) return <ErrorMessage error={error} />;
if (!user) return <NotFound />;
return <div>{user.name}</div>;
}
```
## Query Key Patterns
Use consistent query key structure:
```typescript
// ✅ Good query keys (specific to general)
queryKey: ["user", userId] // Single user
queryKey: ["users", { tenantId, page: 1 }] // List with filters
queryKey: ["organizations", orgId, "teams"] // Nested resource
// ❌ Bad query keys (inconsistent, not cacheable)
queryKey: [userId] // Missing resource type
queryKey: ["getUser", userId] // Don't include function name
queryKey: [{ id: userId, type: "user" }] // Object first is confusing
```
## Mutations with Optimistic Updates
```typescript
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { updateUser } from "~/lib/server/functions/users";
function EditUserForm({ user }: { user: User }) {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data: UserUpdate) => updateUser(user.id, data),
// Optimistic update (immediate UI feedback)
onMutate: async (newData) => {
// Cancel ongoing queries
await queryClient.cancelQueries({ queryKey: ["user", user.id] });
// Snapshot previous value
const previousUser = queryClient.getQueryData(["user", user.id]);
// Optimistically update cache
queryClient.setQueryData(["user", user.id], (old: User) => ({
...old,
...newData,
}));
return { previousUser };
},
// On error, rollback
onError: (err, newData, context) => {
queryClient.setQueryData(["user", user.id], context.previousUser);
},
// Always refetch after mutation
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["user", user.id] });
},
});
const handleSubmit = (data: UserUpdate) => {
mutation.mutate(data);
};
return (
<form onSubmit={(e) => {
e.preventDefault();
handleSubmit({ name: "Updated Name" });
}}>
<button disabled={mutation.isPending}>
{mutation.isPending ? "Saving..." : "Save"}
</button>
</form>
);
}
```
## Infinite Queries (Pagination)
```typescript
import { useInfiniteQuery } from "@tanstack/react-query";
import { listUsers } from "~/lib/server/functions/users";
function UsersList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: ["users"],
queryFn: ({ pageParam = 0 }) => listUsers({
limit: 50,
offset: pageParam,
}),
getNextPageParam: (lastPage, allPages) => {
// Return next offset or undefined if no more pages
return lastPage.length === 50 ? allPages.length * 50 : undefined;
},
initialPageParam: 0,
staleTime: 60000,
});
return (
<div>
{data?.pages.map((page, i) => (
<div key={i}>
{page.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
))}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? "Loading more..."
: hasNextPage
? "Load More"
: "No more users"}
</button>
</div>
);
}
```
## Prefetching (Performance Optimization)
```typescript
import { useQueryClient } from "@tanstack/react-query";
import { getUserById } from "~/lib/server/functions/users";
function UsersList({ users }: { users: User[] }) {
const queryClient = useQueryClient();
// Prefetch user details on hover
const handleMouseEnter = (userId: string) => {
queryClient.prefetchQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
staleTime: 60000,
});
};
return (
<div>
{users.map((user) => (
<Link
key={user.id}
to="/users/$userId"
params={{ userId: user.id }}
onMouseEnter={() => handleMouseEnter(user.id)}
>
{user.name}
</Link>
))}
</div>
);
}
```
## Query Error Handling
```typescript
import { useQuery } from "@tanstack/react-query";
import { Alert } from "~/lib/components/ui/alert";
function DataComponent() {
const { data, error, isLoading } = useQuery({
queryKey: ["data"],
queryFn: () => fetchData(),
retry: 1, // Retry once on failure
staleTime: 60000,
});
if (isLoading) return <LoadingSpinner />;
if (error) {
return (
<Alert variant="destructive">
<h3>Error loading data</h3>
<p>{error.message}</p>
</Alert>
);
}
return <DataDisplay data={data} />;
}
```
## Key Patterns
### Query States
- `isLoading` - First time loading
- `isFetching` - Background refetch
- `isPending` - No cached data yet
- `isError` - Query failed
- `isSuccess` - Query succeeded
### Mutation States
- `isPending` - Mutation in progress
- `isSuccess` - Mutation succeeded
- `isError` - Mutation failed
### Cache Invalidation
```typescript
// Invalidate all user queries
queryClient.invalidateQueries({ queryKey: ["users"] });
// Invalidate specific user
queryClient.invalidateQueries({ queryKey: ["user", userId] });
// Refetch immediately
queryClient.invalidateQueries({
queryKey: ["users"],
refetchType: "active"
});
```

View File

@@ -0,0 +1,224 @@
# TanStack Router Patterns
Complete examples for file-based routing, layouts, and navigation.
## File-Based Routing Structure
```
src/routes/
├── __root.tsx # Root layout (wraps all routes)
├── index.tsx # Homepage (/)
├── _authenticated/ # Protected routes group (underscore prefix)
│ ├── _layout.tsx # Auth layout wrapper
│ ├── dashboard.tsx # /dashboard
│ ├── profile.tsx # /profile
│ └── settings/
│ ├── index.tsx # /settings
│ └── billing.tsx # /settings/billing
├── auth/
│ ├── login.tsx # /auth/login
│ └── signup.tsx # /auth/signup
└── users/
├── index.tsx # /users
└── $userId.tsx # /users/:userId (dynamic param)
```
## Root Layout (__root.tsx)
```typescript
// src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// Create QueryClient with Grey Haven defaults
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60000, // 1 minute default stale time
retry: 1,
refetchOnWindowFocus: false,
},
},
});
export const Route = createRootRoute({
component: RootComponent,
});
function RootComponent() {
return (
<QueryClientProvider client={queryClient}>
<div className="min-h-screen bg-background">
<Outlet /> {/* Child routes render here */}
</div>
<ReactQueryDevtools initialIsOpen={false} />
<TanStackRouterDevtools position="bottom-right" />
</QueryClientProvider>
);
}
```
## Route Layouts (_layout.tsx)
```typescript
// src/routes/_authenticated/_layout.tsx
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { Header } from "~/lib/components/layout/Header";
import { Sidebar } from "~/lib/components/layout/Sidebar";
import { getSession } from "~/lib/server/functions/auth";
export const Route = createFileRoute("/_authenticated/_layout")({
// Loader runs on server for data fetching
beforeLoad: async ({ context }) => {
const session = await getSession();
if (!session) {
throw redirect({
to: "/auth/login",
search: {
redirect: context.location.href,
},
});
}
return { session };
},
component: AuthenticatedLayout,
});
function AuthenticatedLayout() {
const { session } = Route.useRouteContext();
return (
<div className="flex min-h-screen">
<Sidebar user={session.user} />
<div className="flex-1">
<Header user={session.user} />
<main className="p-6">
<Outlet /> {/* Child routes render here */}
</main>
</div>
</div>
);
}
```
## Page Routes with Loaders
```typescript
// src/routes/_authenticated/dashboard.tsx
import { createFileRoute } from "@tanstack/react-router";
import { getDashboardData } from "~/lib/server/functions/dashboard";
import { DashboardStats } from "~/lib/components/dashboard/DashboardStats";
export const Route = createFileRoute("/_authenticated/dashboard")({
// Loader fetches data on server before rendering
loader: async ({ context }) => {
const tenantId = context.session.tenantId;
return await getDashboardData(tenantId);
},
component: DashboardPage,
});
function DashboardPage() {
const data = Route.useLoaderData(); // Type-safe loader data
return (
<div>
<h1 className="text-2xl font-bold">Dashboard</h1>
<DashboardStats data={data} />
</div>
);
}
```
## Dynamic Routes ($param.tsx)
```typescript
// src/routes/users/$userId.tsx
import { createFileRoute } from "@tanstack/react-router";
import { getUserById } from "~/lib/server/functions/users";
import { UserProfile } from "~/lib/components/users/UserProfile";
export const Route = createFileRoute("/users/$userId")({
// Access route params in loader
loader: async ({ params, context }) => {
const { userId } = params;
const tenantId = context.session.tenantId;
return await getUserById(userId, tenantId);
},
component: UserPage,
});
function UserPage() {
const user = Route.useLoaderData();
const { userId } = Route.useParams(); // Also available in component
return (
<div>
<h1 className="text-2xl font-bold">{user.name}</h1>
<UserProfile user={user} />
</div>
);
}
```
## Navigation
```typescript
import { Link, useNavigate } from "@tanstack/react-router";
function Navigation() {
const navigate = useNavigate();
return (
<nav>
{/* Type-safe Link component */}
<Link to="/" className="...">
Home
</Link>
<Link
to="/users/$userId"
params={{ userId: "123" }}
className="..."
>
User Profile
</Link>
{/* Programmatic navigation */}
<button
onClick={() => {
navigate({
to: "/dashboard",
replace: true, // Replace history entry
});
}}
>
Go to Dashboard
</button>
</nav>
);
}
```
## Key Patterns
### Underscore Prefix for Groups
- `_authenticated/` - Route group (doesn't add to URL)
- `_layout.tsx` - Layout wrapper for group
### beforeLoad vs loader
- `beforeLoad` - Auth checks, redirects
- `loader` - Data fetching
### Type Safety
- Route params are type-safe
- Loader data is type-safe
- Navigation is type-safe

View File

@@ -0,0 +1,220 @@
# TanStack Start Server Functions
Complete examples for creating and using server functions.
## Creating Server Functions
```typescript
// src/lib/server/functions/users.ts
import { createServerFn } from "@tanstack/start";
import { db } from "~/lib/server/db";
import { users } from "~/lib/server/schema/users";
import { eq, and } from "drizzle-orm";
// GET server function (automatic caching)
export const getUserById = createServerFn("GET", async (
userId: string,
tenantId: string
) => {
// Server-side code with database access
const user = await db.query.users.findFirst({
where: and(
eq(users.id, userId),
eq(users.tenant_id, tenantId) // Multi-tenant isolation!
),
});
if (!user) {
throw new Error("User not found");
}
return user;
});
// POST server function (mutations)
export const createUser = createServerFn("POST", async (
data: { name: string; email: string },
tenantId: string
) => {
const user = await db.insert(users).values({
...data,
tenant_id: tenantId,
}).returning();
return user[0];
});
// DELETE server function
export const deleteUser = createServerFn("DELETE", async (
userId: string,
tenantId: string
) => {
await db.delete(users).where(
and(
eq(users.id, userId),
eq(users.tenant_id, tenantId)
)
);
return { success: true };
});
```
## Using Server Functions in Components
```typescript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { getUserById, createUser, deleteUser } from "~/lib/server/functions/users";
function UserManagement({ tenantId }: { tenantId: string }) {
const queryClient = useQueryClient();
// Query using server function
const { data: user } = useQuery({
queryKey: ["user", "123"],
queryFn: () => getUserById("123", tenantId),
});
// Mutation using server function
const createMutation = useMutation({
mutationFn: (data: { name: string; email: string }) =>
createUser(data, tenantId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
const deleteMutation = useMutation({
mutationFn: (userId: string) => deleteUser(userId, tenantId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
return <div>...</div>;
}
```
## Server Functions with Auth Context
```typescript
// src/lib/server/functions/auth.ts
import { createServerFn } from "@tanstack/start";
import { getSession } from "~/lib/server/auth";
export const getAuthenticatedUser = createServerFn("GET", async () => {
const session = await getSession();
if (!session) {
throw new Error("Not authenticated");
}
// Automatically includes tenant_id from session
return {
user: session.user,
tenantId: session.tenantId,
};
});
```
```typescript
// Using in a component
function ProfilePage() {
const { data: authData } = useQuery({
queryKey: ["auth", "current-user"],
queryFn: () => getAuthenticatedUser(),
staleTime: 300000, // 5 minutes for auth data
});
return <div>Welcome, {authData?.user.name}!</div>;
}
```
## Multi-Tenant Server Functions
```typescript
// ALWAYS include tenant_id in server functions
export const listOrganizations = createServerFn("GET", async (tenantId: string) => {
return await db.query.organizations.findMany({
where: eq(organizations.tenant_id, tenantId),
});
});
// Use in component with tenant context
function OrganizationsList() {
const { tenantId } = useTenant(); // Custom hook for tenant context
const { data: orgs } = useQuery({
queryKey: ["organizations", tenantId],
queryFn: () => listOrganizations(tenantId),
staleTime: 60000,
});
return <div>...</div>;
}
```
## RLS with Server Functions
```typescript
// Server function uses RLS-enabled database connection
export const getUsers = createServerFn("GET", async () => {
// Uses authenticated database connection with RLS
// tenant_id automatically filtered by RLS policies
return await db.query.users.findMany();
});
```
## Key Patterns
### HTTP Methods
- **GET**: Read operations (automatic caching)
- **POST**: Create operations
- **PUT**: Update operations
- **DELETE**: Delete operations
### Multi-Tenant Isolation
Always include `tenant_id` parameter:
```typescript
export const someFunction = createServerFn("GET", async (
param: string,
tenantId: string // REQUIRED
) => {
// Filter by tenant_id
});
```
### Error Handling
```typescript
export const getUser = createServerFn("GET", async (userId: string) => {
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
});
if (!user) {
throw new Error("User not found"); // Automatically returns 500
}
return user;
});
```
### Type Safety
Server functions are fully type-safe:
```typescript
// Server function
export const updateUser = createServerFn("POST", async (
userId: string,
data: UserUpdate // Type-safe parameter
): Promise<User> => { // Type-safe return
// ...
});
// Client usage (types inferred)
const mutation = useMutation({
mutationFn: (data: UserUpdate) => updateUser(userId, data),
// TypeScript knows the return type is Promise<User>
});
```

View File

@@ -0,0 +1,43 @@
# TanStack Patterns Reference
Configuration and pattern references for TanStack ecosystem.
## Available References
### [router-config.md](router-config.md)
Router setup and configuration.
- File structure conventions
- Root route setup
- Route naming conventions
- beforeLoad vs loader
### [query-config.md](query-config.md)
QueryClient configuration.
- Default configuration
- Query options reference
- Mutation options reference
- Per-query overrides
- DevTools setup
### [caching-strategy.md](caching-strategy.md)
Detailed caching patterns.
- Grey Haven staleTime standards
- Fresh vs stale behavior
- Cache invalidation strategies
- Manual cache updates
- Cache persistence
### [multi-tenant.md](multi-tenant.md)
Multi-tenant patterns with RLS.
- Server function patterns
- Query key patterns
- Tenant context hook
- Row Level Security
- Best practices
## Quick Reference
**Need router setup?** → [router-config.md](router-config.md)
**Need query config?** → [query-config.md](query-config.md)
**Need caching strategy?** → [caching-strategy.md](caching-strategy.md)
**Need multi-tenant patterns?** → [multi-tenant.md](multi-tenant.md)

View File

@@ -0,0 +1,98 @@
# TanStack Query Caching Strategy
Detailed caching patterns for Grey Haven projects.
## Grey Haven staleTime Standards
```typescript
const STALE_TIMES = {
auth: 5 * 60 * 1000, // 5 minutes (auth data)
user: 1 * 60 * 1000, // 1 minute (user profiles)
list: 1 * 60 * 1000, // 1 minute (lists)
static: 10 * 60 * 1000, // 10 minutes (static/config data)
realtime: 0, // 0 (always refetch, e.g., notifications)
};
```
## Cache Behavior
### Fresh vs Stale
```typescript
// Data is "fresh" for staleTime duration
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
staleTime: 60000, // Fresh for 60 seconds
});
// During "fresh" period:
// - No background refetch
// - Instant return from cache
// - New component mounts get cached data immediately
// After "stale" period:
// - Background refetch on mount
// - Cached data shown while refetching
// - UI updates when new data arrives
```
## Cache Invalidation
```typescript
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
// Invalidate all user queries
queryClient.invalidateQueries({ queryKey: ["users"] });
// Invalidate specific user
queryClient.invalidateQueries({ queryKey: ["user", userId] });
// Invalidate and refetch immediately
queryClient.invalidateQueries({
queryKey: ["users"],
refetchType: "active" // Only refetch active queries
});
```
## Manual Cache Updates
```typescript
// Update cache directly (optimistic update)
queryClient.setQueryData(["user", userId], (old: User) => ({
...old,
name: "New Name"
}));
// Get cached data
const cachedUser = queryClient.getQueryData<User>(["user", userId]);
```
## Cache Persistence
TanStack Query cache is in-memory only. For persistence:
```typescript
import { persistQueryClient } from "@tanstack/react-query-persist-client";
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
const persister = createSyncStoragePersister({
storage: window.localStorage,
});
persistQueryClient({
queryClient,
persister,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
});
```
## Best Practices
1. **Use staleTime**: Always set appropriate staleTime
2. **Invalidate after mutations**: Use `onSuccess` to invalidate
3. **Specific keys**: Use specific query keys for targeted invalidation
4. **Prefetch**: Prefetch data on hover/navigation
5. **Background refetch**: Let queries refetch in background

View File

@@ -0,0 +1,97 @@
# Multi-Tenant Patterns with TanStack
Multi-tenant isolation patterns for TanStack Start applications.
## Server Function Pattern
**ALWAYS include tenant_id parameter:**
```typescript
// ✅ CORRECT
export const getUsers = createServerFn("GET", async (tenantId: string) => {
return await db.query.users.findMany({
where: eq(users.tenant_id, tenantId),
});
});
// ❌ WRONG - Missing tenant_id
export const getUsers = createServerFn("GET", async () => {
return await db.query.users.findMany();
});
```
## Query Key Pattern
Include tenant_id in query keys:
```typescript
// ✅ CORRECT
const { data } = useQuery({
queryKey: ["users", tenantId],
queryFn: () => getUsers(tenantId),
});
// ❌ WRONG - Missing tenant_id in key
const { data } = useQuery({
queryKey: ["users"],
queryFn: () => getUsers(tenantId),
});
```
## Tenant Context Hook
```typescript
// src/lib/hooks/use-tenant.ts
import { useQuery } from "@tanstack/react-query";
import { getAuthenticatedUser } from "~/lib/server/functions/auth";
export function useTenant() {
const { data: authData } = useQuery({
queryKey: ["auth", "current-user"],
queryFn: () => getAuthenticatedUser(),
staleTime: 5 * 60 * 1000, // 5 minutes
});
return {
tenantId: authData?.tenantId,
user: authData?.user,
};
}
```
## Usage in Components
```typescript
function UsersList() {
const { tenantId } = useTenant();
const { data: users } = useQuery({
queryKey: ["users", tenantId],
queryFn: () => getUsers(tenantId!),
enabled: !!tenantId, // Only run when tenantId is available
});
return <div>...</div>;
}
```
## Row Level Security (RLS)
With RLS, tenant_id filtering is automatic:
```typescript
// Server function with RLS-enabled connection
export const getUsers = createServerFn("GET", async () => {
// Uses authenticated database connection
// RLS policies automatically filter by tenant_id
return await db.query.users.findMany();
});
```
## Best Practices
1. **Always include tenant_id**: In server functions and query keys
2. **Use tenant context**: Create `useTenant()` hook for consistency
3. **Enable guards**: Use `enabled: !!tenantId` for queries
4. **RLS when possible**: Prefer RLS over manual filtering
5. **Test isolation**: Verify tenant isolation in tests

View File

@@ -0,0 +1,67 @@
# TanStack Query Configuration
QueryClient configuration reference for Grey Haven projects.
## Default Configuration
```typescript
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60000, // 1 minute
retry: 1,
refetchOnWindowFocus: false,
},
},
});
```
## Configuration Options
### Query Options
| Option | Default | Description |
|--------|---------|-------------|
| `staleTime` | 60000ms | How long data stays fresh |
| `retry` | 1 | Number of retry attempts |
| `refetchOnWindowFocus` | false | Refetch when window gains focus |
| `refetchInterval` | false | Auto-refetch interval |
| `enabled` | true | Whether query runs automatically |
### Mutation Options
| Option | Default | Description |
|--------|---------|-------------|
| `retry` | 0 | Number of retry attempts |
| `onSuccess` | undefined | Success callback |
| `onError` | undefined | Error callback |
| `onSettled` | undefined | Always runs after success/error |
## Per-Query Configuration
```typescript
const { data } = useQuery({
queryKey: ["user", userId],
queryFn: () => getUserById(userId),
staleTime: 300000, // Override default (5 minutes)
retry: 3, // Override default
refetchOnWindowFocus: true, // Override default
});
```
## DevTools Setup
```typescript
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
function RootComponent() {
return (
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
```

View File

@@ -0,0 +1,66 @@
# TanStack Router Configuration
Router setup and configuration reference.
## File Structure
```
src/routes/
├── __root.tsx # Root layout (required)
├── index.tsx # Homepage
├── _layout/ # Route group (underscore prefix)
│ ├── _layout.tsx # Group layout
│ └── page.tsx # /page
└── $param.tsx # Dynamic route
```
## Root Route Setup
```typescript
// src/routes/__root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { QueryClientProvider } from "@tanstack/react-query";
export const Route = createRootRoute({
component: RootComponent,
});
function RootComponent() {
return (
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
);
}
```
## Route Naming Conventions
| File | URL | Description |
|------|-----|-------------|
| `index.tsx` | `/` | Homepage |
| `about.tsx` | `/about` | Static route |
| `_layout.tsx` | - | Layout wrapper (no URL) |
| `$userId.tsx` | `/:userId` | Dynamic param |
| `_authenticated/` | - | Route group (no URL) |
## beforeLoad vs loader
- **beforeLoad**: Auth checks, redirects, context setup
- **loader**: Data fetching
```typescript
export const Route = createFileRoute("/_authenticated/_layout")({
beforeLoad: async () => {
const session = await getSession();
if (!session) throw redirect({ to: "/login" });
return { session };
},
});
export const Route = createFileRoute("/dashboard")({
loader: async ({ context }) => {
return await getDashboardData(context.session.tenantId);
},
});
```

View File

@@ -0,0 +1,46 @@
// Grey Haven Studio - Protected Route Layout Template
// Copy this template for _authenticated/_layout.tsx
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { Header } from "~/lib/components/layout/Header";
import { Sidebar } from "~/lib/components/layout/Sidebar";
import { getSession } from "~/lib/server/functions/auth";
// TODO: Update route path to match your file location
export const Route = createFileRoute("/_authenticated/_layout")({
// Auth check runs before loading
beforeLoad: async ({ context }) => {
const session = await getSession();
// Redirect to login if not authenticated
if (!session) {
throw redirect({
to: "/auth/login",
search: {
redirect: context.location.href, // Save redirect URL
},
});
}
// Make session available to child routes
return { session };
},
component: AuthenticatedLayout,
});
function AuthenticatedLayout() {
const { session } = Route.useRouteContext();
return (
<div className="flex min-h-screen">
{/* TODO: Update layout structure */}
<Sidebar user={session.user} />
<div className="flex-1">
<Header user={session.user} />
<main className="p-6">
<Outlet /> {/* Child routes render here */}
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,39 @@
// Grey Haven Studio - Custom Query Hook Template
// Copy this template for reusable query logic
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
// TODO: Import your server functions
// import { getResource, updateResource } from "~/lib/server/functions/resources";
// TODO: Update hook name and parameter types
export function useResource(resourceId: string, tenantId: string) {
const queryClient = useQueryClient();
// Query for fetching data
const query = useQuery({
queryKey: ["resource", resourceId], // Include resourceId in key
queryFn: () => getResource(resourceId, tenantId),
staleTime: 60000, // Grey Haven default: 1 minute
});
// Mutation for updating data
const updateMutation = useMutation({
mutationFn: (data: ResourceUpdate) => updateResource(resourceId, data, tenantId),
onSuccess: (updatedResource) => {
// Update cache with new data
queryClient.setQueryData(["resource", resourceId], updatedResource);
},
});
// Return simplified interface
return {
resource: query.data,
isLoading: query.isLoading,
error: query.error,
update: updateMutation.mutate,
isUpdating: updateMutation.isPending,
};
}
// Usage in component:
// const { resource, isLoading, update, isUpdating } = useResource(id, tenantId);

View File

@@ -0,0 +1,30 @@
// Grey Haven Studio - Page Route Template
// Copy this template for any page route
import { createFileRoute } from "@tanstack/react-router";
// TODO: Import your server functions
// import { getPageData } from "~/lib/server/functions/page";
// TODO: Update route path to match your file location
export const Route = createFileRoute("/_authenticated/page")({
// Loader fetches data on server before rendering
loader: async ({ context }) => {
const tenantId = context.session.tenantId;
// TODO: Replace with your data fetching
// return await getPageData(tenantId);
return { data: "Replace me" };
},
component: PageComponent,
});
function PageComponent() {
const data = Route.useLoaderData(); // Type-safe loader data
return (
<div>
{/* TODO: Build your page UI */}
<h1 className="text-2xl font-bold">Page Title</h1>
<div>{JSON.stringify(data)}</div>
</div>
);
}

View File

@@ -0,0 +1,35 @@
// Grey Haven Studio - TanStack Router Root Route Template
// Copy this template for __root.tsx
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
// Create QueryClient with Grey Haven defaults
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 60000, // 1 minute default
retry: 1,
refetchOnWindowFocus: false,
},
},
});
export const Route = createRootRoute({
component: RootComponent,
});
function RootComponent() {
return (
<QueryClientProvider client={queryClient}>
<div className="min-h-screen bg-background">
<Outlet /> {/* Child routes render here */}
</div>
{/* Dev tools (only in development) */}
<ReactQueryDevtools initialIsOpen={false} />
<TanStackRouterDevtools position="bottom-right" />
</QueryClientProvider>
);
}

View File

@@ -0,0 +1,61 @@
// Grey Haven Studio - Server Function Template
// Copy this template for creating server functions
import { createServerFn } from "@tanstack/start";
import { db } from "~/lib/server/db";
// TODO: Import your database schema
// import { resources } from "~/lib/server/schema/resources";
import { eq, and } from "drizzle-orm";
// GET server function (for fetching data)
// TODO: Update function name and return type
export const getResource = createServerFn("GET", async (
resourceId: string,
tenantId: string // ALWAYS include tenant_id!
) => {
// TODO: Replace with your query
const resource = await db.query.resources.findFirst({
where: and(
eq(resources.id, resourceId),
eq(resources.tenant_id, tenantId) // Multi-tenant isolation!
),
});
if (!resource) {
throw new Error("Resource not found");
}
return resource;
});
// POST server function (for creating data)
// TODO: Update function name and parameters
export const createResource = createServerFn("POST", async (
data: { name: string; description?: string },
tenantId: string // ALWAYS include tenant_id!
) => {
// TODO: Replace with your insert
const resource = await db.insert(resources).values({
...data,
tenant_id: tenantId, // Include tenant_id in insert!
}).returning();
return resource[0];
});
// DELETE server function (for deleting data)
// TODO: Update function name
export const deleteResource = createServerFn("DELETE", async (
resourceId: string,
tenantId: string // ALWAYS include tenant_id!
) => {
// TODO: Replace with your delete
await db.delete(resources).where(
and(
eq(resources.id, resourceId),
eq(resources.tenant_id, tenantId) // Ensure tenant isolation!
)
);
return { success: true };
});