refactor: consolidate AGENTS.md and CLAUDE.md files, update tech stac… (#444)
* refactor: consolidate AGENTS.md and CLAUDE.md files, update tech stack and architecture details - Merged content from CLAUDE.md into AGENTS.md for better organization. - Updated tech stack section to reflect the current technologies used, including Next.js, Supabase, and Tailwind CSS. - Enhanced monorepo structure documentation with detailed directory purposes. - Streamlined multi-tenant architecture explanation and essential commands. - Added key patterns for naming conventions and server actions. - Removed outdated agent files related to Playwright and PostgreSQL, ensuring a cleaner codebase. - Bumped version to 2.23.7 to reflect changes.
This commit is contained in:
committed by
GitHub
parent
bebd56238b
commit
cfa137795b
126
.claude/skills/server-action-builder/SKILL.md
Normal file
126
.claude/skills/server-action-builder/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: server-action-builder
|
||||
description: Create Next.js Server Actions with enhanceAction, Zod validation, and service patterns. Use when implementing mutations, form submissions, or API operations that need authentication and validation. Invoke with /server-action-builder.
|
||||
---
|
||||
|
||||
# Server Action Builder
|
||||
|
||||
You are an expert at creating type-safe server actions for Makerkit following established patterns.
|
||||
|
||||
## Workflow
|
||||
|
||||
When asked to create a server action, follow these steps:
|
||||
|
||||
### Step 1: Create Zod Schema
|
||||
|
||||
Create validation schema in `_lib/schemas/`:
|
||||
|
||||
```typescript
|
||||
// _lib/schemas/feature.schema.ts
|
||||
import { z } from 'zod';
|
||||
|
||||
export const CreateFeatureSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
accountId: z.string().uuid('Invalid account ID'),
|
||||
});
|
||||
|
||||
export type CreateFeatureInput = z.infer<typeof CreateFeatureSchema>;
|
||||
```
|
||||
|
||||
### Step 2: Create Service Layer
|
||||
|
||||
Create service in `_lib/server/`:
|
||||
|
||||
```typescript
|
||||
// _lib/server/feature.service.ts
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
import type { CreateFeatureInput } from '../schemas/feature.schema';
|
||||
|
||||
export function createFeatureService() {
|
||||
return new FeatureService();
|
||||
}
|
||||
|
||||
class FeatureService {
|
||||
async create(data: CreateFeatureInput) {
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { data: result, error } = await client
|
||||
.from('features')
|
||||
.insert({
|
||||
name: data.name,
|
||||
account_id: data.accountId,
|
||||
})
|
||||
.select()
|
||||
.single();
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create Server Action
|
||||
|
||||
Create action in `_lib/server/server-actions.ts`:
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { CreateFeatureSchema } from '../schemas/feature.schema';
|
||||
import { createFeatureService } from './feature.service';
|
||||
|
||||
export const createFeatureAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
const logger = await getLogger();
|
||||
const ctx = { name: 'create-feature', userId: user.id };
|
||||
|
||||
logger.info(ctx, 'Creating feature');
|
||||
|
||||
const service = createFeatureService();
|
||||
const result = await service.create(data);
|
||||
|
||||
logger.info({ ...ctx, featureId: result.id }, 'Feature created');
|
||||
|
||||
revalidatePath('/home/[account]/features');
|
||||
|
||||
return { success: true, data: result };
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: CreateFeatureSchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
|
||||
1. **Schema in separate file** - Reusable between client and server
|
||||
2. **Service layer** - Business logic isolated from action
|
||||
3. **Logging** - Always log before and after operations
|
||||
4. **Revalidation** - Use `revalidatePath` after mutations
|
||||
5. **Trust RLS** - Don't add manual auth checks (RLS handles it)
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
feature/
|
||||
├── _lib/
|
||||
│ ├── schemas/
|
||||
│ │ └── feature.schema.ts
|
||||
│ └── server/
|
||||
│ ├── feature.service.ts
|
||||
│ └── server-actions.ts
|
||||
└── _components/
|
||||
└── feature-form.tsx
|
||||
```
|
||||
|
||||
## Reference Files
|
||||
|
||||
See examples in:
|
||||
- `[Examples](examples.md)`
|
||||
- `[Reference](reference.md)`
|
||||
194
.claude/skills/server-action-builder/examples.md
Normal file
194
.claude/skills/server-action-builder/examples.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# Server Action Examples
|
||||
|
||||
Real examples from the Makerkit codebase.
|
||||
|
||||
## Team Billing Action
|
||||
|
||||
Location: `apps/web/app/home/[account]/billing/_lib/server/server-actions.ts`
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { UpdateBillingSchema } from '../schemas/billing.schema';
|
||||
import { createBillingService } from './billing.service';
|
||||
|
||||
export const updateBillingAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
const logger = await getLogger();
|
||||
const ctx = { name: 'update-billing', userId: user.id, accountId: data.accountId };
|
||||
|
||||
logger.info(ctx, 'Updating billing settings');
|
||||
|
||||
const service = createBillingService();
|
||||
await service.updateBilling(data);
|
||||
|
||||
logger.info(ctx, 'Billing settings updated');
|
||||
|
||||
revalidatePath(`/home/${data.accountSlug}/billing`);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: UpdateBillingSchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Personal Settings Action
|
||||
|
||||
Location: `apps/web/app/home/(user)/settings/_lib/server/server-actions.ts`
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { UpdateProfileSchema } from '../schemas/profile.schema';
|
||||
|
||||
export const updateProfileAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
const logger = await getLogger();
|
||||
const ctx = { name: 'update-profile', userId: user.id };
|
||||
|
||||
logger.info(ctx, 'Updating user profile');
|
||||
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { error } = await client
|
||||
.from('accounts')
|
||||
.update({ name: data.name })
|
||||
.eq('id', user.id);
|
||||
|
||||
if (error) {
|
||||
logger.error({ ...ctx, error }, 'Failed to update profile');
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info(ctx, 'Profile updated successfully');
|
||||
|
||||
revalidatePath('/home/settings');
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: UpdateProfileSchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Action with Redirect
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { CreateProjectSchema } from '../schemas/project.schema';
|
||||
import { createProjectService } from './project.service';
|
||||
|
||||
export const createProjectAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
const service = createProjectService();
|
||||
const project = await service.create(data);
|
||||
|
||||
// Redirect after creation
|
||||
redirect(`/home/${data.accountSlug}/projects/${project.id}`);
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: CreateProjectSchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Delete Action with Confirmation
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { DeleteItemSchema } from '../schemas/item.schema';
|
||||
|
||||
export const deleteItemAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
const logger = await getLogger();
|
||||
const ctx = { name: 'delete-item', userId: user.id, itemId: data.itemId };
|
||||
|
||||
logger.info(ctx, 'Deleting item');
|
||||
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
const { error } = await client
|
||||
.from('items')
|
||||
.delete()
|
||||
.eq('id', data.itemId)
|
||||
.eq('account_id', data.accountId); // RLS will also validate
|
||||
|
||||
if (error) {
|
||||
logger.error({ ...ctx, error }, 'Failed to delete item');
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.info(ctx, 'Item deleted successfully');
|
||||
|
||||
revalidatePath(`/home/${data.accountSlug}/items`);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: DeleteItemSchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Error Handling with isRedirectError
|
||||
|
||||
```typescript
|
||||
'use server';
|
||||
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export const submitFormAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
const logger = await getLogger();
|
||||
const ctx = { name: 'submit-form', userId: user.id };
|
||||
|
||||
try {
|
||||
logger.info(ctx, 'Submitting form');
|
||||
|
||||
// Process form
|
||||
await processForm(data);
|
||||
|
||||
logger.info(ctx, 'Form submitted, redirecting');
|
||||
|
||||
redirect('/success');
|
||||
} catch (error) {
|
||||
// Don't treat redirects as errors
|
||||
if (!isRedirectError(error)) {
|
||||
logger.error({ ...ctx, error }, 'Form submission failed');
|
||||
throw error;
|
||||
}
|
||||
throw error; // Re-throw redirect
|
||||
}
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: FormSchema,
|
||||
},
|
||||
);
|
||||
```
|
||||
179
.claude/skills/server-action-builder/reference.md
Normal file
179
.claude/skills/server-action-builder/reference.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Server Action Reference
|
||||
|
||||
## enhanceAction API
|
||||
|
||||
```typescript
|
||||
import { enhanceAction } from '@kit/next/actions';
|
||||
|
||||
export const myAction = enhanceAction(
|
||||
async function (data, user) {
|
||||
// data: validated input (typed from schema)
|
||||
// user: authenticated user object (if auth: true)
|
||||
|
||||
return { success: true, data: result };
|
||||
},
|
||||
{
|
||||
auth: true, // Require authentication (default: false)
|
||||
schema: ZodSchema, // Zod schema for validation (optional)
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `auth` | `boolean` | `false` | Require authenticated user |
|
||||
| `schema` | `ZodSchema` | - | Zod schema for input validation |
|
||||
|
||||
### Handler Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `data` | `z.infer<Schema>` | Validated input data |
|
||||
| `user` | `User` | Authenticated user (if auth: true) |
|
||||
|
||||
## enhanceRouteHandler API
|
||||
|
||||
```typescript
|
||||
import { enhanceRouteHandler } from '@kit/next/routes';
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export const POST = enhanceRouteHandler(
|
||||
async function ({ body, user, request }) {
|
||||
// body: validated request body
|
||||
// user: authenticated user (if auth: true)
|
||||
// request: original NextRequest
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
schema: ZodSchema,
|
||||
},
|
||||
);
|
||||
|
||||
export const GET = enhanceRouteHandler(
|
||||
async function ({ user, request }) {
|
||||
const url = new URL(request.url);
|
||||
const param = url.searchParams.get('param');
|
||||
|
||||
return NextResponse.json({ data: result });
|
||||
},
|
||||
{
|
||||
auth: true,
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Common Zod Patterns
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod';
|
||||
|
||||
// Basic schema
|
||||
export const CreateItemSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
description: z.string().optional(),
|
||||
accountId: z.string().uuid('Invalid account ID'),
|
||||
});
|
||||
|
||||
// With transforms
|
||||
export const SearchSchema = z.object({
|
||||
query: z.string().trim().min(1),
|
||||
page: z.coerce.number().int().positive().default(1),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(10),
|
||||
});
|
||||
|
||||
// With refinements
|
||||
export const DateRangeSchema = z.object({
|
||||
startDate: z.coerce.date(),
|
||||
endDate: z.coerce.date(),
|
||||
}).refine(
|
||||
(data) => data.endDate > data.startDate,
|
||||
{ message: 'End date must be after start date' }
|
||||
);
|
||||
|
||||
// Enum values
|
||||
export const StatusSchema = z.object({
|
||||
status: z.enum(['active', 'inactive', 'pending']),
|
||||
});
|
||||
```
|
||||
|
||||
## Revalidation
|
||||
|
||||
```typescript
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
|
||||
// Revalidate specific path
|
||||
revalidatePath('/home/[account]/items');
|
||||
|
||||
// Revalidate with dynamic segment
|
||||
revalidatePath(`/home/${accountSlug}/items`);
|
||||
|
||||
// Revalidate by tag
|
||||
revalidateTag('items');
|
||||
```
|
||||
|
||||
## Redirect
|
||||
|
||||
```typescript
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
// Redirect after action
|
||||
redirect('/success');
|
||||
|
||||
// Redirect with dynamic path
|
||||
redirect(`/home/${accountSlug}/items/${itemId}`);
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
```typescript
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
const logger = await getLogger();
|
||||
|
||||
// Context object for all logs
|
||||
const ctx = {
|
||||
name: 'action-name',
|
||||
userId: user.id,
|
||||
accountId: data.accountId,
|
||||
};
|
||||
|
||||
// Log levels
|
||||
logger.info(ctx, 'Starting operation');
|
||||
logger.warn({ ...ctx, warning: 'details' }, 'Warning message');
|
||||
logger.error({ ...ctx, error }, 'Operation failed');
|
||||
```
|
||||
|
||||
## Supabase Clients
|
||||
|
||||
```typescript
|
||||
// Standard client (RLS enforced)
|
||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||
const client = getSupabaseServerClient();
|
||||
|
||||
// Admin client (bypasses RLS - use sparingly)
|
||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||
const adminClient = getSupabaseServerAdminClient();
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
import { isRedirectError } from 'next/dist/client/components/redirect-error';
|
||||
|
||||
try {
|
||||
await operation();
|
||||
redirect('/success');
|
||||
} catch (error) {
|
||||
if (!isRedirectError(error)) {
|
||||
// Handle actual error
|
||||
logger.error({ error }, 'Operation failed');
|
||||
throw error;
|
||||
}
|
||||
throw error; // Re-throw redirect
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user