463 lines
14 KiB
Plaintext
463 lines
14 KiB
Plaintext
# 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 |