@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.
- package/README.md +2 -1
- package/dist/constants.d.ts +4 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -1
- package/dist/features/definitions/auth.d.ts +3 -0
- package/dist/features/definitions/auth.d.ts.map +1 -0
- package/dist/features/definitions/auth.js +17 -0
- package/dist/features/definitions/auth.js.map +1 -0
- package/dist/features/definitions/core.d.ts.map +1 -1
- package/dist/features/definitions/core.js +16 -1
- package/dist/features/definitions/core.js.map +1 -1
- package/dist/features/definitions/database.d.ts +3 -0
- package/dist/features/definitions/database.d.ts.map +1 -0
- package/dist/features/definitions/database.js +45 -0
- package/dist/features/definitions/database.js.map +1 -0
- package/dist/features/definitions/deployment.d.ts +3 -0
- package/dist/features/definitions/deployment.d.ts.map +1 -0
- package/dist/features/definitions/deployment.js +14 -0
- package/dist/features/definitions/deployment.js.map +1 -0
- package/dist/features/definitions/forms.d.ts.map +1 -1
- package/dist/features/definitions/forms.js +4 -0
- package/dist/features/definitions/forms.js.map +1 -1
- package/dist/features/definitions/index.d.ts +3 -0
- package/dist/features/definitions/index.d.ts.map +1 -1
- package/dist/features/definitions/index.js +3 -0
- package/dist/features/definitions/index.js.map +1 -1
- package/dist/features/definitions/mobile.d.ts.map +1 -1
- package/dist/features/definitions/mobile.js +11 -2
- package/dist/features/definitions/mobile.js.map +1 -1
- package/dist/features/definitions/observability.js +1 -1
- package/dist/features/definitions/observability.js.map +1 -1
- package/dist/features/definitions/routing.d.ts.map +1 -1
- package/dist/features/definitions/routing.js +2 -1
- package/dist/features/definitions/routing.js.map +1 -1
- package/dist/features/definitions/state.d.ts.map +1 -1
- package/dist/features/definitions/state.js +9 -2
- package/dist/features/definitions/state.js.map +1 -1
- package/dist/features/definitions/testing.d.ts.map +1 -1
- package/dist/features/definitions/testing.js +4 -2
- package/dist/features/definitions/testing.js.map +1 -1
- package/dist/features/registry.d.ts.map +1 -1
- package/dist/features/registry.js +4 -1
- package/dist/features/registry.js.map +1 -1
- package/dist/features/types.test.js +6 -2
- package/dist/features/types.test.js.map +1 -1
- package/dist/resources/docs.d.ts.map +1 -1
- package/dist/resources/docs.js +5 -0
- package/dist/resources/docs.js.map +1 -1
- package/dist/tools/add-features.js +1 -1
- package/dist/tools/add-features.js.map +1 -1
- package/dist/utils/docs.d.ts.map +1 -1
- package/dist/utils/docs.js +2 -0
- package/dist/utils/docs.js.map +1 -1
- package/dist/utils/scaffold/claude-md/index.d.ts.map +1 -1
- package/dist/utils/scaffold/claude-md/index.js +3 -1
- package/dist/utils/scaffold/claude-md/index.js.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.d.ts +2 -0
- package/dist/utils/scaffold/claude-md/sections.d.ts.map +1 -1
- package/dist/utils/scaffold/claude-md/sections.js +132 -2
- package/dist/utils/scaffold/claude-md/sections.js.map +1 -1
- package/dist/utils/scaffold/compute.js +1 -1
- package/dist/utils/scaffold/compute.js.map +1 -1
- package/dist/utils/scaffold/generators.d.ts +2 -2
- package/dist/utils/scaffold/generators.d.ts.map +1 -1
- package/dist/utils/scaffold/generators.js +64 -22
- package/dist/utils/scaffold/generators.js.map +1 -1
- package/package.json +1 -1
- package/templates/.env.example +44 -10
- package/templates/.github/workflows/ci.yml +12 -4
- package/templates/.github/workflows/deploy.yml +59 -0
- package/templates/CLAUDE.md +251 -2
- package/templates/docs/ARCHITECTURE.md +13 -12
- package/templates/docs/AUTHENTICATION.md +325 -0
- package/templates/docs/CODING_STANDARDS.md +65 -0
- package/templates/docs/DEPLOYMENT.md +268 -0
- package/templates/docs/E2E_TESTING.md +133 -11
- package/templates/docs/SUPABASE_INTEGRATION.md +310 -0
- package/templates/docs/TESTING.md +195 -77
- package/templates/e2e/auth/auth.setup.ts +60 -0
- package/templates/e2e/fixtures/index.ts +24 -2
- package/templates/e2e/tests/profile.auth.spec.ts +103 -0
- package/templates/e2e/tests/profile.spec.ts +64 -0
- package/templates/e2e/tests/register-form.spec.ts +38 -0
- package/templates/gitignore +5 -0
- package/templates/package.json +15 -3
- package/templates/playwright.config.ts +39 -4
- package/templates/src/App.tsx +32 -19
- package/templates/src/components/layout/Header.test.tsx +17 -1
- package/templates/src/components/layout/Header.tsx +13 -1
- package/templates/src/components/shared/AccountButton/AccountButton.test.tsx +30 -0
- package/templates/src/components/shared/AccountButton/AccountButton.tsx +38 -0
- package/templates/src/components/shared/AccountButton/index.ts +1 -0
- package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.test.tsx +4 -4
- package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +55 -53
- package/templates/src/components/shared/ProfileSync/ProfileSync.test.tsx +44 -0
- package/templates/src/components/shared/ProfileSync/ProfileSync.tsx +104 -0
- package/templates/src/components/shared/ProfileSync/index.ts +1 -0
- package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.test.tsx +43 -0
- package/templates/src/components/shared/ProtectedRoute/ProtectedRoute.tsx +35 -0
- package/templates/src/components/shared/ProtectedRoute/index.ts +1 -0
- package/templates/src/components/shared/index.ts +5 -2
- package/templates/src/contexts/clerkContext.tsx +45 -0
- package/templates/src/contexts/performanceContext.tsx +3 -3
- package/templates/src/contexts/supabaseContext.test.tsx +59 -0
- package/templates/src/contexts/supabaseContext.tsx +87 -0
- package/templates/src/hooks/index.ts +40 -2
- package/templates/src/hooks/supabase/index.ts +12 -0
- package/templates/src/hooks/supabase/useProfiles.test.tsx +207 -0
- package/templates/src/hooks/supabase/useProfiles.ts +213 -0
- package/templates/src/hooks/supabase/useSupabaseQuery.test.tsx +150 -0
- package/templates/src/hooks/supabase/useSupabaseQuery.ts +91 -0
- package/templates/src/hooks/useCopyFeedback.test.ts +129 -0
- package/templates/src/hooks/useCopyFeedback.ts +41 -0
- package/templates/src/hooks/useDebouncedCallback.test.ts +164 -0
- package/templates/src/hooks/useDebouncedCallback.ts +47 -0
- package/templates/src/hooks/useDocumentTitle.test.ts +59 -0
- package/templates/src/hooks/useDocumentTitle.ts +31 -0
- package/templates/src/hooks/useIOSViewportReset.test.ts +58 -0
- package/templates/src/hooks/useIOSViewportReset.ts +18 -0
- package/templates/src/hooks/useKeyboardShortcut.test.ts +86 -0
- package/templates/src/hooks/useKeyboardShortcuts.ts +44 -0
- package/templates/src/hooks/useLocalStorage.test.ts +111 -0
- package/templates/src/hooks/useLocalStorage.ts +77 -0
- package/templates/src/hooks/useSyncedFormData.test.ts +75 -0
- package/templates/src/hooks/useSyncedFormData.ts +21 -0
- package/templates/src/hooks/useSyncedState.test.ts +119 -0
- package/templates/src/hooks/useSyncedState.ts +30 -0
- package/templates/src/index.css +1 -0
- package/templates/src/lib/api.test.ts +30 -38
- package/templates/src/lib/api.ts +1 -7
- package/templates/src/lib/config.ts +54 -4
- package/templates/src/lib/constants.ts +10 -0
- package/templates/src/lib/createSelectors.test.ts +136 -0
- package/templates/src/lib/createSelectors.ts +31 -0
- package/templates/src/lib/env.ts +36 -14
- package/templates/src/lib/index.ts +5 -2
- package/templates/src/lib/routes.ts +1 -0
- package/templates/src/lib/sentry.ts +58 -0
- package/templates/src/lib/storage.ts +6 -2
- package/templates/src/lib/supabase/client.ts +58 -0
- package/templates/src/lib/supabase/index.ts +5 -0
- package/templates/src/main.tsx +19 -31
- package/templates/src/mocks/constants.ts +31 -0
- package/templates/src/mocks/fixtures/index.ts +3 -1
- package/templates/src/mocks/fixtures/profiles.ts +55 -0
- package/templates/src/mocks/fixtures/users.ts +91 -0
- package/templates/src/mocks/handlers/index.ts +2 -1
- package/templates/src/mocks/handlers/supabase.ts +64 -0
- package/templates/src/mocks/handlers/todos.ts +1 -1
- package/templates/src/mocks/index.ts +6 -0
- package/templates/src/pages/Profile.test.tsx +263 -0
- package/templates/src/pages/Profile.tsx +171 -0
- package/templates/src/pages/index.ts +1 -0
- package/templates/src/stores/preferencesStore.ts +35 -9
- package/templates/src/test/clerkMock.tsx +137 -0
- package/templates/src/test/fetchMock.ts +58 -0
- package/templates/src/test/index.ts +51 -2
- package/templates/src/test/mocks.ts +128 -1
- package/templates/src/test/providers.tsx +10 -4
- package/templates/src/test/supabaseMock.ts +112 -0
- package/templates/src/test-setup.ts +42 -2
- package/templates/src/types/database.ts +46 -0
- package/templates/src/types/index.ts +1 -0
- package/templates/src/types/supabase.ts +167 -0
- package/templates/src/vite-env.d.ts +6 -0
- package/templates/supabase/migrations/20260104000000_create_profiles_table.sql +67 -0
- package/templates/vitest.config.ts +9 -1
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
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
|
-
<
|
|
85
|
-
{
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
-
|
|
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
|