@react-spa-scaffold/mcp 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +2 -1
  2. package/dist/constants.d.ts +4 -0
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +4 -0
  5. package/dist/constants.js.map +1 -1
  6. package/dist/features/definitions/auth.d.ts +3 -0
  7. package/dist/features/definitions/auth.d.ts.map +1 -0
  8. package/dist/features/definitions/auth.js +17 -0
  9. package/dist/features/definitions/auth.js.map +1 -0
  10. package/dist/features/definitions/core.d.ts.map +1 -1
  11. package/dist/features/definitions/core.js +16 -1
  12. package/dist/features/definitions/core.js.map +1 -1
  13. package/dist/features/definitions/database.d.ts +3 -0
  14. package/dist/features/definitions/database.d.ts.map +1 -0
  15. package/dist/features/definitions/database.js +45 -0
  16. package/dist/features/definitions/database.js.map +1 -0
  17. package/dist/features/definitions/deployment.d.ts +3 -0
  18. package/dist/features/definitions/deployment.d.ts.map +1 -0
  19. package/dist/features/definitions/deployment.js +14 -0
  20. package/dist/features/definitions/deployment.js.map +1 -0
  21. package/dist/features/definitions/forms.d.ts.map +1 -1
  22. package/dist/features/definitions/forms.js +4 -0
  23. package/dist/features/definitions/forms.js.map +1 -1
  24. package/dist/features/definitions/index.d.ts +3 -0
  25. package/dist/features/definitions/index.d.ts.map +1 -1
  26. package/dist/features/definitions/index.js +3 -0
  27. package/dist/features/definitions/index.js.map +1 -1
  28. package/dist/features/definitions/mobile.d.ts.map +1 -1
  29. package/dist/features/definitions/mobile.js +11 -2
  30. package/dist/features/definitions/mobile.js.map +1 -1
  31. package/dist/features/definitions/observability.js +1 -1
  32. package/dist/features/definitions/observability.js.map +1 -1
  33. package/dist/features/definitions/routing.d.ts.map +1 -1
  34. package/dist/features/definitions/routing.js +2 -1
  35. package/dist/features/definitions/routing.js.map +1 -1
  36. package/dist/features/definitions/state.d.ts.map +1 -1
  37. package/dist/features/definitions/state.js +9 -2
  38. package/dist/features/definitions/state.js.map +1 -1
  39. package/dist/features/definitions/testing.d.ts.map +1 -1
  40. package/dist/features/definitions/testing.js +4 -2
  41. package/dist/features/definitions/testing.js.map +1 -1
  42. package/dist/features/registry.d.ts.map +1 -1
  43. package/dist/features/registry.js +4 -1
  44. package/dist/features/registry.js.map +1 -1
  45. package/dist/features/types.test.js +6 -2
  46. package/dist/features/types.test.js.map +1 -1
  47. package/dist/resources/docs.d.ts.map +1 -1
  48. package/dist/resources/docs.js +5 -0
  49. package/dist/resources/docs.js.map +1 -1
  50. package/dist/tools/add-features.js +1 -1
  51. package/dist/tools/add-features.js.map +1 -1
  52. package/dist/utils/docs.d.ts.map +1 -1
  53. package/dist/utils/docs.js +2 -0
  54. package/dist/utils/docs.js.map +1 -1
  55. package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
  56. package/dist/utils/scaffold/claude-md/index.js +3 -1
  57. package/dist/utils/scaffold/claude-md/index.js.map +1 -1
  58. package/dist/utils/scaffold/claude-md/sections.d.ts +2 -0
  59. package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
  60. package/dist/utils/scaffold/claude-md/sections.js +132 -2
  61. package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
  62. package/dist/utils/scaffold/compute.js +1 -1
  63. package/dist/utils/scaffold/compute.js.map +1 -1
  64. package/dist/utils/scaffold/generators.d.ts +2 -2
  65. package/dist/utils/scaffold/generators.d.ts.map +1 -1
  66. package/dist/utils/scaffold/generators.js +64 -22
  67. package/dist/utils/scaffold/generators.js.map +1 -1
  68. package/package.json +1 -1
  69. package/templates/.env.example +44 -10
  70. package/templates/.github/workflows/ci.yml +12 -4
  71. package/templates/.github/workflows/deploy.yml +59 -0
  72. package/templates/CLAUDE.md +251 -2
  73. package/templates/docs/ARCHITECTURE.md +13 -12
  74. package/templates/docs/AUTHENTICATION.md +325 -0
  75. package/templates/docs/CODING_STANDARDS.md +65 -0
  76. package/templates/docs/DEPLOYMENT.md +268 -0
  77. package/templates/docs/E2E_TESTING.md +133 -11
  78. package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
  79. package/templates/docs/TESTING.md +195 -77
  80. package/templates/e2e/auth/auth.setup.ts +60 -0
  81. package/templates/e2e/fixtures/index.ts +24 -2
  82. package/templates/e2e/tests/profile.auth.spec.ts +103 -0
  83. package/templates/e2e/tests/profile.spec.ts +64 -0
  84. package/templates/e2e/tests/register-form.spec.ts +38 -0
  85. package/templates/gitignore +5 -0
  86. package/templates/package.json +15 -3
  87. package/templates/playwright.config.ts +39 -4
  88. package/templates/src/App.tsx +32 -19
  89. package/templates/src/components/layout/Header.test.tsx +17 -1
  90. package/templates/src/components/layout/Header.tsx +13 -1
  91. package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +30 -0
  92. package/templates/src/components/shared/AccountButton/AccountButton.tsx +38 -0
  93. package/templates/src/components/shared/AccountButton/index.ts +1 -0
  94. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.test.tsx +4 -4
  95. package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +55 -53
  96. package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
  97. package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
  98. package/templates/src/components/shared/ProfileSync/index.ts +1 -0
  99. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +43 -0
  100. package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.tsx +35 -0
  101. package/templates/src/components/shared/ProtectedRoute/index.ts +1 -0
  102. package/templates/src/components/shared/index.ts +5 -2
  103. package/templates/src/contexts/clerkContext.tsx +45 -0
  104. package/templates/src/contexts/performanceContext.tsx +3 -3
  105. package/templates/src/contexts/supabaseContext.test.tsx +59 -0
  106. package/templates/src/contexts/supabaseContext.tsx +87 -0
  107. package/templates/src/hooks/index.ts +40 -2
  108. package/templates/src/hooks/supabase/index.ts +12 -0
  109. package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
  110. package/templates/src/hooks/supabase/useProfiles.ts +213 -0
  111. package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
  112. package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
  113. package/templates/src/hooks/useCopyFeedback.test.ts +129 -0
  114. package/templates/src/hooks/useCopyFeedback.ts +41 -0
  115. package/templates/src/hooks/useDebouncedCallback.test.ts +164 -0
  116. package/templates/src/hooks/useDebouncedCallback.ts +47 -0
  117. package/templates/src/hooks/useDocumentTitle.test.ts +59 -0
  118. package/templates/src/hooks/useDocumentTitle.ts +31 -0
  119. package/templates/src/hooks/useIOSViewportReset.test.ts +58 -0
  120. package/templates/src/hooks/useIOSViewportReset.ts +18 -0
  121. package/templates/src/hooks/useKeyboardShortcut.test.ts +86 -0
  122. package/templates/src/hooks/useKeyboardShortcuts.ts +44 -0
  123. package/templates/src/hooks/useLocalStorage.test.ts +111 -0
  124. package/templates/src/hooks/useLocalStorage.ts +77 -0
  125. package/templates/src/hooks/useSyncedFormData.test.ts +75 -0
  126. package/templates/src/hooks/useSyncedFormData.ts +21 -0
  127. package/templates/src/hooks/useSyncedState.test.ts +119 -0
  128. package/templates/src/hooks/useSyncedState.ts +30 -0
  129. package/templates/src/index.css +1 -0
  130. package/templates/src/lib/api.test.ts +30 -38
  131. package/templates/src/lib/api.ts +1 -7
  132. package/templates/src/lib/config.ts +54 -4
  133. package/templates/src/lib/constants.ts +10 -0
  134. package/templates/src/lib/createSelectors.test.ts +136 -0
  135. package/templates/src/lib/createSelectors.ts +31 -0
  136. package/templates/src/lib/env.ts +36 -14
  137. package/templates/src/lib/index.ts +5 -2
  138. package/templates/src/lib/routes.ts +1 -0
  139. package/templates/src/lib/sentry.ts +58 -0
  140. package/templates/src/lib/storage.ts +6 -2
  141. package/templates/src/lib/supabase/client.ts +58 -0
  142. package/templates/src/lib/supabase/index.ts +5 -0
  143. package/templates/src/main.tsx +19 -31
  144. package/templates/src/mocks/constants.ts +31 -0
  145. package/templates/src/mocks/fixtures/index.ts +3 -1
  146. package/templates/src/mocks/fixtures/profiles.ts +55 -0
  147. package/templates/src/mocks/fixtures/users.ts +91 -0
  148. package/templates/src/mocks/handlers/index.ts +2 -1
  149. package/templates/src/mocks/handlers/supabase.ts +64 -0
  150. package/templates/src/mocks/handlers/todos.ts +1 -1
  151. package/templates/src/mocks/index.ts +6 -0
  152. package/templates/src/pages/Profile.test.tsx +263 -0
  153. package/templates/src/pages/Profile.tsx +171 -0
  154. package/templates/src/pages/index.ts +1 -0
  155. package/templates/src/stores/preferencesStore.ts +35 -9
  156. package/templates/src/test/clerkMock.tsx +137 -0
  157. package/templates/src/test/fetchMock.ts +58 -0
  158. package/templates/src/test/index.ts +51 -2
  159. package/templates/src/test/mocks.ts +128 -1
  160. package/templates/src/test/providers.tsx +10 -4
  161. package/templates/src/test/supabaseMock.ts +112 -0
  162. package/templates/src/test-setup.ts +42 -2
  163. package/templates/src/types/database.ts +46 -0
  164. package/templates/src/types/index.ts +1 -0
  165. package/templates/src/types/supabase.ts +167 -0
  166. package/templates/src/vite-env.d.ts +6 -0
  167. package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
  168. package/templates/vitest.config.ts +9 -1
@@ -14,9 +14,14 @@ npm run format # Prettier format
14
14
  npm run test # Vitest once
15
15
  npm run test:watch # Vitest watch mode
16
16
  npm run test:coverage # Coverage (80% threshold)
17
- npm run e2e # Playwright functional E2E tests
17
+ npm run e2e # Playwright E2E (desktop)
18
+ npm run e2e:mobile # Playwright E2E (mobile)
19
+ npm run e2e:all # Playwright E2E (all viewports)
18
20
  npm run e2e:perf # Performance regression tests
19
21
  npm run i18n:extract # Extract translations to .po
22
+ npm run db:types # Generate Supabase TypeScript types
23
+ npm run db:push # Push database migrations
24
+ npm run db:studio # Open Supabase Studio
20
25
  ```
21
26
 
22
27
  ## Project Structure
@@ -50,6 +55,76 @@ e2e/ # Playwright tests
50
55
 
51
56
  See [docs/CODING_STANDARDS.md](docs/CODING_STANDARDS.md) and [docs/COMPONENT_GUIDELINES.md](docs/COMPONENT_GUIDELINES.md).
52
57
 
58
+ ## Custom Hooks
59
+
60
+ ### State & Storage
61
+
62
+ ```tsx
63
+ import { useLocalStorage } from '@/hooks';
64
+
65
+ // localStorage with tab sync and updater functions
66
+ const [value, setValue] = useLocalStorage('key', defaultValue);
67
+ setValue((prev) => newValue);
68
+ ```
69
+
70
+ ### Form State Sync
71
+
72
+ ```tsx
73
+ import { useSyncedFormData, useSyncedState } from '@/hooks';
74
+
75
+ // Sync form data when trigger changes (dialog open, ID changes)
76
+ const [formData, setFormData] = useSyncedFormData(sourceData, syncTrigger);
77
+
78
+ // Sync state but block when actively editing
79
+ const [localValue, setLocalValue] = useSyncedState(externalValue, isEditing);
80
+ ```
81
+
82
+ ### Utilities
83
+
84
+ ```tsx
85
+ import { useCopyFeedback, useDebouncedCallback, useKeyboardShortcut, useDocumentTitle } from '@/hooks';
86
+
87
+ // Copy feedback with auto-reset
88
+ const { isCopied, triggerCopied } = useCopyFeedback(2000);
89
+
90
+ // Debounced callbacks
91
+ const debouncedSearch = useDebouncedCallback(handleSearch, 300);
92
+
93
+ // Keyboard shortcuts
94
+ useKeyboardShortcut('mod+s', handleSave, { preventDefault: true });
95
+
96
+ // Dynamic page titles
97
+ useDocumentTitle('Dashboard');
98
+ ```
99
+
100
+ ### Mobile & iOS
101
+
102
+ ```tsx
103
+ import { useIOSViewportReset, useMobileContext, useTouchSizes } from '@/hooks';
104
+
105
+ // iOS Safari keyboard viewport fix
106
+ const handleBlur = useIOSViewportReset();
107
+ <input onBlur={handleBlur} />;
108
+
109
+ // Responsive breakpoints
110
+ const { isMobile, isTablet, isDesktop } = useMobileContext();
111
+
112
+ // Touch-aware sizes (44px on mobile)
113
+ const sizes = useTouchSizes();
114
+ <Button size={sizes.button}>Click</Button>;
115
+ ```
116
+
117
+ ## TIMING Constants
118
+
119
+ Use centralized timing constants for consistent UX:
120
+
121
+ ```tsx
122
+ import { TIMING } from '@/lib/constants';
123
+
124
+ // TIMING.DEBOUNCE_DELAY = 300ms
125
+ // TIMING.COPY_FEEDBACK_DURATION = 2000ms
126
+ ```
127
+
53
128
  ## UI Components (Shadcn/UI)
54
129
 
55
130
  This project uses **Shadcn/UI** with radix-nova style. Components live in `src/components/ui/`.
@@ -104,6 +179,7 @@ resolve-library-id → get-library-docs
104
179
  - `zod` - Schema validation
105
180
  - `date-fns` - Date formatting
106
181
  - `msw` - Mock service worker setup
182
+ - `@clerk/react-router` - Authentication patterns
107
183
 
108
184
  ### Decision Flow
109
185
 
@@ -140,10 +216,183 @@ import { render, mockMatchMedia, server } from '@/test';
140
216
 
141
217
  MSW handlers auto-reset after each test.
142
218
 
219
+ ## Authentication (Clerk)
220
+
221
+ When the auth feature is enabled, Clerk authentication is required.
222
+
223
+ ### Setup
224
+
225
+ 1. Create an account at [clerk.com](https://clerk.com)
226
+ 2. Get your Publishable Key from the dashboard
227
+ 3. Copy `.env.example` to `.env` and set your key:
228
+ ```
229
+ VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
230
+ ```
231
+
232
+ ### Usage
233
+
234
+ ```tsx
235
+ // Protect routes that require authentication
236
+ import { ProtectedRoute } from '@/components/shared';
237
+
238
+ <Route
239
+ path="/dashboard"
240
+ element={
241
+ <ProtectedRoute>
242
+ <DashboardPage />
243
+ </ProtectedRoute>
244
+ }
245
+ />;
246
+ ```
247
+
248
+ ```tsx
249
+ // Conditional rendering based on auth state
250
+ import { SignedIn, SignedOut, UserButton, SignInButton } from '@clerk/react-router';
251
+
252
+ <SignedIn>
253
+ <UserButton />
254
+ </SignedIn>
255
+ <SignedOut>
256
+ <SignInButton mode="modal">
257
+ <Button>Sign In</Button>
258
+ </SignInButton>
259
+ </SignedOut>
260
+ ```
261
+
262
+ ### Testing
263
+
264
+ Clerk is automatically mocked in tests. Use test utilities to control auth state:
265
+
266
+ ```tsx
267
+ import { setMockClerkSignedIn, resetClerkMocks } from '@/test';
268
+
269
+ beforeEach(() => resetClerkMocks());
270
+
271
+ it('shows sign-in when not authenticated', () => {
272
+ setMockClerkSignedIn(false);
273
+ // ...
274
+ });
275
+ ```
276
+
277
+ ## Database (Supabase)
278
+
279
+ Supabase provides PostgreSQL database with Row Level Security (RLS), integrated with Clerk authentication.
280
+
281
+ ### Setup
282
+
283
+ 1. Create a project at [supabase.com](https://supabase.com)
284
+ 2. Configure Clerk as third-party auth provider:
285
+ - Supabase Dashboard → Authentication → Providers → Third-Party Auth → Add Clerk
286
+ 3. Enable Supabase integration in Clerk:
287
+ - Clerk Dashboard → Integrations → Supabase → Activate
288
+ 4. Set environment variables in `.env`:
289
+ ```
290
+ VITE_SUPABASE_DATABASE_URL=https://your-project.supabase.co
291
+ VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
292
+ ```
293
+
294
+ ### Database Commands
295
+
296
+ ```bash
297
+ npm run db:types # Generate TypeScript types from schema
298
+ npm run db:push # Push migrations to database
299
+ npm run db:reset # Reset database (WARNING: destructive)
300
+ npm run db:studio # Open Supabase Studio
301
+ ```
302
+
303
+ ### Usage
304
+
305
+ ```tsx
306
+ import { useSupabase, useSupabaseQuery, useProfile } from '@/hooks';
307
+
308
+ // Direct client access
309
+ const supabase = useSupabase();
310
+ const { data } = await supabase.from('profiles').select();
311
+
312
+ // TanStack Query wrapper for automatic caching
313
+ const { data, isLoading } = useSupabaseQuery({
314
+ table: 'profiles',
315
+ queryKey: ['current'],
316
+ });
317
+
318
+ // Convenience hook for current user's profile
319
+ const { profile, isLoading, exists } = useProfile();
320
+ ```
321
+
322
+ ### Profile Mutations
323
+
324
+ ```tsx
325
+ import { useUpsertProfile, useUpdateProfile, useDeleteProfile } from '@/hooks';
326
+
327
+ // Create or update profile (upsert)
328
+ const upsertProfile = useUpsertProfile();
329
+ await upsertProfile.mutateAsync({ id: userId, email: 'user@example.com' });
330
+
331
+ // Update current user's profile
332
+ const updateProfile = useUpdateProfile();
333
+ await updateProfile.mutateAsync({ full_name: 'John' });
334
+
335
+ // Delete current user's profile
336
+ const deleteProfile = useDeleteProfile();
337
+ await deleteProfile.mutateAsync();
338
+ ```
339
+
340
+ ### Auto-Sync with ProfileSync
341
+
342
+ ```tsx
343
+ import { ProfileSync } from '@/components/shared';
344
+
345
+ // Add to your app to auto-sync Clerk user data to Supabase
346
+ function App() {
347
+ return (
348
+ <>
349
+ <ProfileSync />
350
+ <Routes>...</Routes>
351
+ </>
352
+ );
353
+ }
354
+ ```
355
+
356
+ ### Row Level Security (RLS)
357
+
358
+ All tables should have RLS enabled. Policies use `auth.uid()` which equals the Clerk user_id:
359
+
360
+ ```sql
361
+ -- Users can only access their own data
362
+ CREATE POLICY "Users can view own profile"
363
+ ON profiles FOR SELECT TO authenticated
364
+ USING (id = auth.uid());
365
+ ```
366
+
367
+ ### Testing
368
+
369
+ Supabase context is mocked in tests with state controls:
370
+
371
+ ```tsx
372
+ import { render, setMockSupabaseData, setMockSupabaseError, createProfile, resetSupabaseMocks } from '@/test';
373
+
374
+ beforeEach(() => resetSupabaseMocks());
375
+
376
+ it('displays profile data', async () => {
377
+ setMockSupabaseData([createProfile({ full_name: 'Test User' })]);
378
+ render(<ProfileCard />);
379
+ // Assert profile is displayed
380
+ });
381
+
382
+ it('handles error', async () => {
383
+ setMockSupabaseError({ message: 'Failed', code: 'ERROR' });
384
+ render(<ProfileCard />);
385
+ // Assert error state
386
+ });
387
+ ```
388
+
143
389
  ## Common Gotchas
144
390
 
145
391
  1. **Node.js >= 22.0.0** required (check `.nvmrc`)
146
392
  2. **Conventional commits** enforced by commitlint
147
- 3. **Context hooks throw** outside provider (e.g., `useMobileContext()`)
393
+ 3. **Context hooks throw** outside provider (e.g., `useMobileContext()`, `useSupabase()`)
148
394
  4. **Barrel exports** in each directory via `index.ts`
149
395
  5. **UI components** import directly: `@/components/ui/button` (no barrel)
396
+ 6. **Clerk auth required** when auth feature is enabled - set `VITE_CLERK_PUBLISHABLE_KEY` in `.env`
397
+ 7. **Supabase requires Clerk** - SupabaseProvider must be inside ClerkProvider
398
+ 8. **RLS policies required** - All Supabase tables should have Row Level Security enabled
@@ -12,6 +12,7 @@ High-level architecture and key decisions. For API details, see [API Reference](
12
12
  | Styling | Tailwind CSS 4 | No runtime cost, scales with team size |
13
13
  | State | Zustand + TanStack Query | Minimal boilerplate, separation of concerns |
14
14
  | Forms | React Hook Form + Zod | Minimal re-renders, type-safe validation |
15
+ | Authentication | Clerk | Modal-based auth, shadcn theme integration |
15
16
  | i18n | Lingui | Smaller runtime, compile-time extraction |
16
17
  | Testing | Vitest + Playwright | Fast, Vite-native, true cross-browser |
17
18
  | Error Tracking | Sentry | Industry standard, lazy-loaded |
@@ -73,22 +74,21 @@ Providers wrap the app in this specific order:
73
74
  ```tsx
74
75
  <StrictMode>
75
76
  <QueryProvider>
76
- {' '}
77
77
  {/* TanStack Query - outermost for global cache */}
78
78
  <I18nProvider>
79
- {' '}
80
79
  {/* Lingui - translations available everywhere */}
81
80
  <BrowserRouter>
82
- {' '}
83
81
  {/* React Router - routing context */}
84
- <MobileProvider>
85
- {' '}
86
- {/* Viewport - depends on router for SSR */}
87
- <ErrorBoundary>
88
- <App />
89
- <Toaster />
90
- </ErrorBoundary>
91
- </MobileProvider>
82
+ <ClerkThemeProvider>
83
+ {/* Clerk - auth inside Router for @clerk/react-router */}
84
+ <MobileProvider>
85
+ {/* Viewport - depends on router for SSR */}
86
+ <ErrorBoundary>
87
+ <App />
88
+ <Toaster />
89
+ </ErrorBoundary>
90
+ </MobileProvider>
91
+ </ClerkThemeProvider>
92
92
  </BrowserRouter>
93
93
  </I18nProvider>
94
94
  </QueryProvider>
@@ -99,7 +99,8 @@ Providers wrap the app in this specific order:
99
99
 
100
100
  - QueryProvider outermost so cache persists across route changes
101
101
  - I18nProvider before Router so route components can use translations
102
- - MobileProvider inside Router for potential SSR viewport detection
102
+ - ClerkThemeProvider inside Router (required by @clerk/react-router declarative mode)
103
+ - MobileProvider inside Clerk for potential SSR viewport detection
103
104
  - ErrorBoundary innermost to catch errors in App without breaking providers
104
105
 
105
106
  ### 2. State Management Separation
@@ -0,0 +1,325 @@
1
+ # Authentication
2
+
3
+ Clerk authentication integration with shadcn theming and Supabase token injection.
4
+ For quick-start usage, see [CLAUDE.md](../CLAUDE.md#authentication-clerk).
5
+
6
+ ---
7
+
8
+ ## Why Clerk
9
+
10
+ This project uses Clerk instead of Supabase Auth for:
11
+
12
+ - **Production-ready UI** - Pre-built `SignInButton`, `UserButton` components with modal flows
13
+ - **Superior OAuth** - 20+ providers vs ~10, dashboard configuration vs code
14
+ - **Automatic session management** - Cross-tab sync, token refresh handled transparently
15
+ - **Separation of concerns** - Clerk = authentication (who), Supabase = authorization + data (what)
16
+
17
+ ---
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ ClerkProvider (ClerkThemeProvider wrapper)
23
+
24
+ ├── useAuth() → { isLoaded, isSignedIn, userId, getToken }
25
+ ├── useUser() → { user: { id, email, fullName, imageUrl } }
26
+ └── useSession() → { session.getToken() } → JWT Token
27
+
28
+
29
+ SupabaseProvider injects token
30
+
31
+
32
+ Supabase validates JWT
33
+ auth.uid() = Clerk user_id
34
+ ```
35
+
36
+ **Authentication Flow:**
37
+
38
+ 1. User clicks Sign In → Clerk modal opens
39
+ 2. User authenticates (email, OAuth, etc.)
40
+ 3. Clerk creates session, `useAuth()` returns `isSignedIn: true`
41
+ 4. `useSession().getToken()` provides JWT for Supabase
42
+ 5. RLS policies grant access via `auth.uid()`
43
+
44
+ ---
45
+
46
+ ## Setup
47
+
48
+ ### 1. Environment Variable
49
+
50
+ ```bash
51
+ VITE_CLERK_PUBLISHABLE_KEY=pk_test_xxxxx
52
+ ```
53
+
54
+ Get from [Clerk Dashboard](https://dashboard.clerk.com) → API Keys.
55
+
56
+ ### 2. Dashboard Setup
57
+
58
+ 1. Create application at [clerk.com](https://clerk.com)
59
+ 2. Configure sign-in methods (Email, Google, GitHub, etc.)
60
+ 3. **For Supabase**: Integrations → Supabase → Activate (adds `role: authenticated` claim)
61
+ 4. Copy Clerk domain → Supabase Dashboard → Authentication → Add Clerk provider
62
+
63
+ ### 3. Provider Hierarchy
64
+
65
+ ```tsx
66
+ // main.tsx - SupabaseProvider MUST be inside ClerkProvider
67
+ <ClerkThemeProvider publishableKey={CLERK_PUBLISHABLE_KEY}>
68
+ <SupabaseProvider>
69
+ <App />
70
+ </SupabaseProvider>
71
+ </ClerkThemeProvider>
72
+ ```
73
+
74
+ ---
75
+
76
+ ## File Structure
77
+
78
+ ```
79
+ src/
80
+ ├── contexts/
81
+ │ └── clerkContext.tsx # ClerkThemeProvider with shadcn theme
82
+ ├── components/shared/
83
+ │ ├── AccountButton/ # Sign in / User button
84
+ │ ├── ProtectedRoute/ # Auth guard wrapper
85
+ │ └── ProfileSync/ # Auto-sync Clerk → Supabase
86
+ ├── test/
87
+ │ └── clerkMock.tsx # Comprehensive Clerk mocks
88
+ ├── mocks/
89
+ │ ├── constants.ts # Mock user/session constants
90
+ │ └── fixtures/users.ts # Mock user factories
91
+ └── index.css # Includes @clerk/themes/shadcn.css
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Theme Configuration
97
+
98
+ ### ClerkThemeProvider
99
+
100
+ ```typescript
101
+ // src/contexts/clerkContext.tsx
102
+ import { ClerkProvider } from '@clerk/react-router';
103
+ import { shadcn } from '@clerk/themes';
104
+
105
+ const appearance: Appearance = {
106
+ baseTheme: shadcn,
107
+ variables: {
108
+ fontFamily: '"Inter Variable", sans-serif',
109
+ borderRadius: '0.45rem',
110
+ },
111
+ elements: {
112
+ modalBackdrop: 'backdrop-blur-sm',
113
+ modalContent: 'sm:max-w-md max-sm:min-h-svh max-sm:min-w-full max-sm:rounded-none',
114
+ card: 'max-sm:rounded-none max-sm:shadow-none',
115
+ },
116
+ };
117
+
118
+ export function ClerkThemeProvider({ children, publishableKey }: Props) {
119
+ return (
120
+ <ClerkProvider publishableKey={publishableKey} afterSignOutUrl="/" appearance={appearance}>
121
+ {children}
122
+ </ClerkProvider>
123
+ );
124
+ }
125
+ ```
126
+
127
+ ### CSS Import
128
+
129
+ ```css
130
+ /* src/index.css */
131
+ @import '@clerk/themes/shadcn.css';
132
+ ```
133
+
134
+ Enables automatic light/dark mode adaptation with shadcn CSS variables.
135
+
136
+ ---
137
+
138
+ ## Components
139
+
140
+ | Component | Location | Purpose |
141
+ | -------------------- | ----------------------------------- | ---------------------------------------------------- |
142
+ | `ClerkThemeProvider` | `contexts/clerkContext.tsx` | Clerk wrapper with shadcn theme |
143
+ | `AccountButton` | `components/shared/AccountButton/` | Sign-in button (logged out) / UserButton (logged in) |
144
+ | `ProtectedRoute` | `components/shared/ProtectedRoute/` | Auth guard, redirects to sign-in |
145
+ | `ProfileSync` | `components/shared/ProfileSync/` | Auto-syncs Clerk user → Supabase profiles |
146
+
147
+ ### AccountButton
148
+
149
+ Shows `SignInButton` when logged out, `UserButton` when logged in. Displays skeleton while loading.
150
+
151
+ ### ProtectedRoute
152
+
153
+ ```tsx
154
+ <Route
155
+ path="/dashboard"
156
+ element={
157
+ <ProtectedRoute>
158
+ <DashboardPage />
159
+ </ProtectedRoute>
160
+ }
161
+ />
162
+ ```
163
+
164
+ Returns `<PageLoading />` while checking auth, `<RedirectToSignIn />` if not authenticated.
165
+
166
+ ### ProfileSync
167
+
168
+ Invisible component that syncs Clerk user data to Supabase on sign-in:
169
+
170
+ - `id` → Clerk user ID
171
+ - `email` → Primary email
172
+ - `full_name` → Full name
173
+ - `avatar_url` → Profile image
174
+
175
+ ---
176
+
177
+ ## Hooks Reference
178
+
179
+ All hooks imported from `@clerk/react-router`:
180
+
181
+ | Hook | Key Returns | Use Case |
182
+ | -------------- | ------------------------------------------------------------- | -------------------------------------------- |
183
+ | `useAuth()` | `isLoaded`, `isSignedIn`, `userId`, `sessionId`, `getToken()` | Auth state without user details |
184
+ | `useUser()` | `isLoaded`, `user` | User profile (id, email, fullName, imageUrl) |
185
+ | `useSession()` | `isLoaded`, `session.getToken()` | JWT token for API calls |
186
+ | `useClerk()` | `signOut({ redirectUrl })` | Programmatic sign-out |
187
+
188
+ ### User Object Properties
189
+
190
+ | Property | Type | Description |
191
+ | ---------------------------------------- | ---------------- | ------------- |
192
+ | `user.id` | `string` | Clerk user ID |
193
+ | `user.primaryEmailAddress?.emailAddress` | `string` | Email |
194
+ | `user.fullName` | `string \| null` | Full name |
195
+ | `user.imageUrl` | `string` | Avatar URL |
196
+
197
+ For usage examples, see [CLAUDE.md](../CLAUDE.md#authentication-clerk).
198
+
199
+ ---
200
+
201
+ ## Supabase Integration
202
+
203
+ Clerk tokens are injected into Supabase for authenticated database access:
204
+
205
+ ```typescript
206
+ // src/contexts/supabaseContext.tsx
207
+ const { session } = useSession();
208
+
209
+ const supabase = useMemo(
210
+ () =>
211
+ createSupabaseClient(async () => {
212
+ if (!session) return null;
213
+ return session.getToken(); // Clerk JWT injected
214
+ }),
215
+ [session?.id], // Only recreate on sign in/out, not every render
216
+ );
217
+ ```
218
+
219
+ **Key points:**
220
+
221
+ - JWT includes `sub` (user ID) and `role: authenticated` claims
222
+ - Supabase `auth.uid()` equals Clerk user ID
223
+ - RLS policies enforce user-scoped access
224
+
225
+ See [SUPABASE_INTEGRATION.md](./SUPABASE_INTEGRATION.md) for full details.
226
+
227
+ ---
228
+
229
+ ## Testing
230
+
231
+ ### Mock Utilities
232
+
233
+ Import from `@/test`:
234
+
235
+ | Utility | Purpose |
236
+ | ---------------------------- | ---------------------------------------- |
237
+ | `setMockClerkSignedIn(bool)` | Set sign-in status |
238
+ | `setMockClerkLoaded(bool)` | Set loading state |
239
+ | `setMockClerkState({ ... })` | Set multiple values |
240
+ | `setMockClerkUser({ ... })` | Customize mock user |
241
+ | `resetClerkMocks()` | Reset to defaults (call in `beforeEach`) |
242
+
243
+ ### Mock Components
244
+
245
+ | Component | Test ID |
246
+ | ------------------ | --------------------- |
247
+ | `SignInButton` | `sign-in-button` |
248
+ | `SignUpButton` | `sign-up-button` |
249
+ | `UserButton` | `user-button` |
250
+ | `RedirectToSignIn` | `redirect-to-sign-in` |
251
+
252
+ ### Mock Constants & Fixtures
253
+
254
+ ```typescript
255
+ // src/mocks/constants.ts
256
+ export const MOCK_USER = {
257
+ id: 'user_123',
258
+ email: 'test@example.com',
259
+ fullName: 'Test User',
260
+ avatarUrl: 'https://example.com/avatar.jpg',
261
+ };
262
+ export const MOCK_SESSION_ID = 'sess_123';
263
+ export const MOCK_AUTH_TOKEN = 'mock-auth-token';
264
+ ```
265
+
266
+ ```typescript
267
+ // src/mocks/fixtures/users.ts
268
+ import { createUser, createUsers } from '@/test';
269
+
270
+ const user = createUser({ fullName: 'Jane Doe' }); // Single user with overrides
271
+ const users = createUsers(3); // Array of mock users
272
+ ```
273
+
274
+ ### E2E Testing
275
+
276
+ For Playwright tests requiring authentication, use `@clerk/testing`:
277
+
278
+ ```bash
279
+ CLERK_SECRET_KEY=sk_test_xxxxx
280
+ E2E_CLERK_USER_USERNAME=test@example.com
281
+ E2E_CLERK_USER_PASSWORD=your-password
282
+ ```
283
+
284
+ See [E2E_TESTING.md](./E2E_TESTING.md#authenticated-testing) for full details.
285
+
286
+ ---
287
+
288
+ ## Troubleshooting
289
+
290
+ | Issue | Cause | Fix |
291
+ | ------------------------------------------- | -------------------------- | ----------------------------------------------------- |
292
+ | "Missing VITE_CLERK_PUBLISHABLE_KEY" | Env var not set | Add to `.env`, restart dev server |
293
+ | UI not matching theme | Missing CSS import | Add `@import '@clerk/themes/shadcn.css'` to index.css |
294
+ | "useAuth must be used within ClerkProvider" | Component outside provider | Check `main.tsx` provider order |
295
+ | Modal not opening | Missing `mode="modal"` | Use `<SignInButton mode="modal">` |
296
+ | Supabase not getting tokens | Wrong provider order | SupabaseProvider must be inside ClerkProvider |
297
+ | User data undefined | Checking before loaded | Wait for `isLoaded === true` before accessing user |
298
+
299
+ ### Debug Auth State
300
+
301
+ ```typescript
302
+ const { isLoaded, isSignedIn, userId } = useAuth();
303
+ console.log({ isLoaded, isSignedIn, userId });
304
+ ```
305
+
306
+ ### Debug Token Claims
307
+
308
+ ```typescript
309
+ const { session } = useSession();
310
+ const token = await session?.getToken();
311
+ if (token) {
312
+ const payload = JSON.parse(atob(token.split('.')[1]));
313
+ console.log(payload); // { sub: "user_xxx", role: "authenticated", ... }
314
+ }
315
+ ```
316
+
317
+ ---
318
+
319
+ ## Resources
320
+
321
+ - [Clerk Documentation](https://clerk.com/docs)
322
+ - [Clerk React Router Integration](https://clerk.com/docs/references/react-router/overview)
323
+ - [Clerk Supabase Integration](https://clerk.com/docs/integrations/databases/supabase)
324
+ - [Clerk Appearance Customization](https://clerk.com/docs/customization/overview)
325
+ - [SUPABASE_INTEGRATION.md](./SUPABASE_INTEGRATION.md) - Database integration with Clerk tokens