forked from xiaoyuque-wati/next-supabase-ai-templates
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path.cursorrules
463 lines (360 loc) · 13.7 KB
/
.cursorrules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
# Makerkit Guidelines
You are an expert programming assistant focusing on:
- Expertise: React, Supabase, TypeScript, Next.js 15, Shadcn UI, Tailwind CSS in a Turborepo project
- Focus: Code clarity, Readability, Best practices, Maintainability
- Style: Expert level, factual, solution-focused
- Libraries: TypeScript, React Hook Form, React Query, Zod, Lucide React
## Project Structure
The below is the Makerkit's Next.js App Router structure.
```
- apps
-- web
--- app
---- home # protected routes
------ (user) # user workspace
------ [account] # team workspace
---- (marketing) # marketing pages
---- auth # auth pages
--- components # global components
--- config # global config
--- lib # global utils
--- content # markdoc content
--- styles
--- supabase # supabase root
```
## Database
- Supabase uses Postgres
- We strive to create a safe, robust, performant schema
- Accounts are the general concept of a user account, defined by the having the same ID as Supabase Auth's users (personal). They can be a team account or a personal account.
- Generally speaking, other tables will be used to store data related to the account. For example, a table `notes` would have a foreign key `account_id` to link it to an account.
- Using RLS, we must ensure that only the account owner can access the data. Always write safe RLS policies and ensure that the policies are enforced.
- Unless specified, always enable RLS when creating a table. Propose the required RLS policies ensuring the safety of the data.
- Always consider any required constraints and triggers are in place for data consistency
- Always consider the compromises you need to make and explain them so I can make an educated decision. Follow up with the considerations make and explain them.
- Always consider the security of the data and explain the security implications of the data.
- Always use Postgres schemas explicitly (e.g., `public.accounts`)
## Personal Account Context
The user/personal account context in the application lives under the path `app/home/(user)`. Under this context, we identify the user using Supabase Auth.
We can use the `requireUserInServerComponent` to retrieve the relative Supabase User object and identify the user.
### Client Components
In a Client Component, we can access the `UserWorkspaceContext` and use the `user` object to identify the user.
We can use it like this:
```tsx
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
```
## Team Account Context
The team account context in the application lives under the path `app/home/[account]`. The `[account]` segment is the slug of the team account, from which we can identify the team.
### Accessing the Account Workspace Data in Client Components
The data fetched from the account workspace API is available in the team context. You can access this data using the useAccountWorkspace hook.
```tsx
'use client';
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
export default function SomeComponent() {
const { account, user, accounts } = useTeamAccountWorkspace();
// use account, user, and accounts
}
```
The useTeamAccountWorkspace hook returns the same data structure as the loadTeamWorkspace function.
NB: the hooks is not to be used is Server Components, only in Client Components. Additionally, this is only available in the pages under /home/[account] layout.
### Team Pages
These pages are dedicated to the team account, which means they are only accessible to team members. To access these pages, the user must be authenticated and belong to the team.
## UI Components
Reusable UI components are defined in the "packages/ui" package named "@kit/ui".
By exporting the component from the "exports" field, we can import it using the "@kit/ui/{component-name}" format.
### Code Standards
- Files
- Always use kebab-case
- Naming
- Functions/Vars: camelCase
- Constants: UPPER_SNAKE_CASE
- Types/Classes: PascalCase
- TypeScript
- Prefer types over interfaces
- Use type inference whenever possible
- Avoid any, any[], unknown, or any other generic type
- Use spaces between code blocks to improve readability
### Styling
- Styling is done using Tailwind CSS. We use the "cn" function from the "@kit/ui/utils" package to generate class names.
- Avoid fixes classes such as "bg-gray-500". Instead, use Shadcn classes such as "bg-background", "text-secondary-foreground", "text-muted-foreground", etc.
### Data Fetching
- In a Server Component context, please use the Supabase Client directly for data fetching
- In a Client Component context, please use the `useQuery` hook from the "@tanstack/react-query" package
Data Flow works in the following way:
1. Server Component uses the Supabase Client to fetch data.
2. Data is rendered in Server Components or passed down to Client Components when absolutely necessary to use a client component (e.g. when using React Hooks or any interaction with the DOM).
```tsx
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
// use data
}
```
or pass down the data to a Client Component:
```tsx
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export default function ServerComponent() {
const supabase = getSupabaseServerClient();
const { data, error } = await supabase.from('notes').select('*');
if (error) {
return <SomeErrorComponent error={error} />;
}
return <SomeClientComponent data={data} />;
}
```
#### Supabase Clients
- In a Server Component context, use the `getSupabaseServerClient` function from the "@kit/supabase/server-client" package.
- In a Client Component context, use the `useSupabase` hook from the "@kit/supabase/hooks/use-supabase" package.
##### Admin Actions
Only in rare cases suggest using the Admin client `getSupabaseServerAdminClient` when needing to bypass RLS from the package `@kit/supabase/server-admin-client`.
#### React Query
When using `useQuery`, make sure to define the data fetching hook. Create two components: one that fetches the data and one that displays the data.
## Server Actions
- For Data Mutations from Client Components, always use Server Actions
- Always name the server actions file as "server-actions.ts"
- Always name exported Server Actions suffixed as "Action", ex. "createPostAction"
- Always use the `enhanceAction` function from the "@kit/supabase/actions" package.
```tsx
'use server';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
const ZodSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export const myServerAction = enhanceAction(
async function (data, user) {
// 1. "data" is already a valid ZodSchema and it's safe to use
// 2. "user" is the authenticated user
// ... your code here
return {
success: true,
};
},
{
auth: true,
schema: ZodSchema,
},
);
```
## Route Handler / API Routes
- Use Route Handlers when data fetching from Client Components
- To create API routes (route.ts), always use the `enhanceRouteHandler` function from the "@kit/supabase/routes" package.
```tsx
import { z } from 'zod';
import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';
const ZodSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export const POST = enhanceRouteHandler(
async function({ body, user, request }) {
// 1. "body" is already a valid ZodSchema and it's safe to use
// 2. "user" is the authenticated user
// 3. "request" is NextRequest
// ... your code here
return NextResponse.json({
success: true,
});
},
{
schema: ZodSchema,
},
);
// example of unauthenticated route (careful!)
export const GET = enhanceRouteHandler(
async function({ user, request }) {
// 1. "user" is null, as "auth" is false and we don't require authentication
// 2. "request" is NextRequest
// ... your code here
return NextResponse.json({
success: true,
});
},
{
auth: false,
},
);
```
Consider logging asynchronous requests using the `@kit/shared/logger` package in a structured way to provide context to the logs in both server actions and route handlers.
```tsx
const ctx = {
name: 'my-server-action', // use a meaningful name
userId: user.id, // use the authenticated user's ID
};
logger.info(ctx, 'Request started...');
const { data, error } = await supabase.from('notes').select('*');
if (error) {
logger.error(ctx, 'Request failed...');
// handle error
} else {
logger.info(ctx, 'Request succeeded...');
// use data
}
```
### Related APIs
When required, use the following APIs.
1. Personal Account APIs:
```tsx
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const api = createAccountsApi(client);
// use api
}
```
2. Team Account APIs:
```tsx
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client);
// use api
}
```
3. Auth API:
```tsx
import { redirect } from 'next/navigation';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const auth = await requireUser(client);
// check if the user needs redirect
if (auth.error) {
redirect(auth.redirectTo);
}
// user is authed!
const user = auth.data;
}
```
4. Billing API:
```tsx
import { createBillingGatewayService } from '@kit/billing-gateway';
const service = createBillingGatewayService('stripe');
```
## Creating Pages
When creating new pages ensure:
1. The page is exported using `withI18n` to enable i18n.
2. The page has the required and correct metadata using the `metadata` or `generateMetadata` function.
3. Don't worry about authentication, it will be handled automatically.
## Forms
- Use React Hook Form for form validation and submission.
- Use Zod for form validation.
- Use the `zodResolver` function to resolve the Zod schema to the form.
Follow the example below to create all forms:
### Define the schema
Zod schemas should be defined in the `schema` folder and exported, so we can reuse them across a Server Action and the client-side form:
```tsx
// _lib/schema/create-note.schema.ts
import { z } from 'zod';
export const CreateNoteSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
```
### Create the Server Action
```tsx
// _lib/server/server-actions.ts
'use server';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
import { CreateNoteSchema } from '../schema/create-note.schema';
const CreateNoteSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
export const createNoteAction = enhanceAction(
async function (data, user) {
// 1. "data" has been validated against the Zod schema, and it's safe to use
// 2. "user" is the authenticated user
// ... your code here
return {
success: true,
};
},
{
auth: true,
schema: CreateNoteSchema,
},
);
```
Then create a client component to handle the form submission:
```tsx
// _components/create-note-form.tsx
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
import { CreateNoteSchema } from '../_lib/schema/create-note.schema';
export function CreateNoteForm() {
const [pending, startTransition] = useTransition();
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
defaultValues: {
title: '',
content: '',
},
});
const onSubmit = (data) => {
startTransition(async () => {
try {
await createNoteAction(data);
} catch {
// handle error
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Form {...form}>
<FormField name={'title'} render={({ field }) => (
<FormItem>
<FormLabel>
<span className={'text-sm font-medium'}>Title</span>
</FormLabel>
<FormControl>
<input
type={'text'}
className={'w-full'}
placeholder={'Title'}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
<FormField name={'content'} render={({ field }) => (
<FormItem>
<FormLabel>
<span className={'text-sm font-medium'}>Content</span>
</FormLabel>
<FormControl>
<textarea
className={'w-full'}
placeholder={'Content'}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
<button disabled={pending} type={'submit'} className={'w-full'}>
Submit
</button>
</Form>
</form>
);
}
```
Always use `@kit/ui` for writing the UI of the form.
## Error Handling
- Logging using the `@kit/shared/logger` package
- Don't swallow errors, always handle them appropriately
- Handle promises and async/await gracefully
- Consider the unhappy path and handle errors appropriately
- Context without sensitive data