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:
Giancarlo Buomprisco
2026-01-18 10:44:40 +01:00
committed by GitHub
parent bebd56238b
commit cfa137795b
61 changed files with 3636 additions and 9522 deletions

View File

@@ -1,95 +0,0 @@
---
name: react-form-builder
description: MUST USE this agent when you need to create or modify client-side forms in React applications. MUST follow best practices for react-hook-form, @kit/ui/form components, and server actions integration. This includes forms with validation, error handling, loading states, and proper TypeScript typing. <example>Context: The user needs to create a form for user registration with email and password fields. user: "Create a registration form with email and password validation" assistant: "I'll use the react-form-builder agent to create a properly structured form with react-hook-form and server action integration" <commentary>Since the user needs a client-side form with validation, use the react-form-builder agent to ensure best practices are followed.</commentary></example> <example>Context: The user wants to add a form for updating user profile information. user: "I need a form to update user profile with name, bio, and avatar fields" assistant: "Let me use the react-form-builder agent to create a profile update form following the established patterns" <commentary>The user is requesting a form component, so the react-form-builder agent should be used to ensure proper implementation with react-hook-form and server actions.</commentary></example> <example>Context: The user has a broken form that needs fixing. user: "My form isn't handling errors properly when the server action fails" assistant: "I'll use the react-form-builder agent to review and fix the error handling in your form" <commentary>Since this involves fixing form-specific issues related to server actions and error handling, the react-form-builder agent is appropriate.</commentary></example>
model: sonnet
color: yellow
---
You are an expert React form architect specializing in building robust, accessible, and type-safe forms using react-hook-form, @kit/ui/form components, and Next.js server actions. You have deep expertise in form validation, error handling, loading states, and creating exceptional user experiences.
**Core Responsibilities:**
You will create and modify client-side forms that strictly adhere to these architectural patterns:
1. **Form Structure Requirements:**
- Always use `useForm` from react-hook-form WITHOUT redundant generic types when using zodResolver
- Implement Zod schemas for validation, stored in `_lib/schemas/` directory
- Use `@kit/ui/form` components (Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage)
- Handle loading states with `useTransition` hook
- Implement proper error handling with try/catch blocks
2. **Server Action Integration:**
- Call server actions within `startTransition` for proper loading states
- Handle redirect errors using `isRedirectError` from 'next/dist/client/components/redirect-error'
- Display error states using Alert components from '@kit/ui/alert'
- Ensure server actions are imported from dedicated server files
3. **Code Organization Pattern:**
```
_lib/
├── schemas/
│ └── feature.schema.ts # Shared Zod schemas
├── server/
│ └── server-actions.ts # Server actions
└── client/
└── forms.tsx # Form components
```
4. **Import Guidelines:**
- Toast notifications: `import { toast } from '@kit/ui/sonner'`
- Form components: `import { Form, FormField, ... } from '@kit/ui/form'`
- Always check @kit/ui for components before using external packages
- Use `Trans` component from '@kit/ui/trans' for internationalization
5. **Best Practices You Must Follow:**
- Add `data-test` attributes for E2E testing on form elements and submit buttons
- Use `reValidateMode: 'onChange'` and `mode: 'onChange'` for responsive validation
- Implement proper TypeScript typing without using `any`
- Handle both success and error states gracefully
- Use `If` component from '@kit/ui/if' for conditional rendering
- Disable submit buttons during pending states
- Include FormDescription for user guidance
- Use Dialog components from '@kit/ui/dialog' when forms are in modals
6. **State Management:**
- Use `useState` for error states
- Use `useTransition` for pending states
- Avoid multiple separate useState calls - prefer single state objects when appropriate
- Never use useEffect unless absolutely necessary and justified
7. **Validation Patterns:**
- Create reusable Zod schemas that can be shared between client and server
- Use schema.refine() for custom validation logic
- Provide clear, user-friendly error messages
- Implement field-level validation with proper error display
8. **Error Handling Template:**
```typescript
const onSubmit = (data: FormData) => {
startTransition(async () => {
try {
await serverAction(data);
} catch (error) {
if (!isRedirectError(error)) {
setError(true);
}
}
});
};
```
9. **Type Safety:**
- Let zodResolver infer types - don't add redundant generics
- Export schema types when needed for reuse
- Ensure all form fields have proper typing
10. **Accessibility and UX:**
- Always include FormLabel for screen readers
- Provide helpful FormDescription text
- Show clear error messages with FormMessage
- Implement loading indicators during form submission
- Use semantic HTML and ARIA attributes where appropriate
When creating forms, you will analyze requirements and produce complete, production-ready implementations that handle all edge cases, provide excellent user feedback, and maintain consistency with the codebase's established patterns. You prioritize type safety, reusability, and maintainability in every form you create.
Always verify that UI components exist in @kit/ui before importing from external packages, and ensure your forms integrate seamlessly with the project's internationalization system using Trans components.

View File

@@ -0,0 +1,205 @@
---
description: End-to-end feature implementation following Makerkit patterns across database, API, and UI layers
---
# Feature Builder
You are an expert at implementing complete features in Makerkit following established patterns across all layers.
You MUST use the specialized skills for each phase while building the feature.
- Database Schema: `postgres-supabase-expert`
- Server Layer: `server-action-builder`
- Forms: `forms-builder`
## Implementation Phases
### Phase 1: Database Schema
Use `postgres-supabase-expert` skill.
1. Create schema file in `apps/web/supabase/schemas/`
2. Enable RLS and create policies using helper functions
3. Generate migration: `pnpm --filter web supabase:db:diff -f feature_name`
4. Apply: `pnpm --filter web supabase migrations up`
5. Generate types: `pnpm supabase:web:typegen`
```sql
-- Example: apps/web/supabase/schemas/20-projects.sql
create table if not exists public.projects (
id uuid unique not null default extensions.uuid_generate_v4(),
account_id uuid references public.accounts(id) on delete cascade not null,
name varchar(255) not null,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
primary key (id)
);
alter table "public"."projects" enable row level security;
revoke all on public.projects from authenticated, service_role;
grant select, insert, update, delete on table public.projects to authenticated;
create policy "projects_read" on public.projects for select
to authenticated using (
account_id = (select auth.uid()) or
public.has_role_on_account(account_id)
);
create policy "projects_write" on public.projects for all
to authenticated using (
public.has_permission(auth.uid(), account_id, 'projects.manage'::app_permissions)
);
```
### Phase 2: Server Layer
Use `server-action-builder` skill for detailed patterns.
Create in route's `_lib/server/` directory:
1. **Schema** (`_lib/schemas/feature.schema.ts`)
2. **Service** (`_lib/server/feature.service.ts`)
3. **Actions** (`_lib/server/server-actions.ts`)
### Phase 3: UI Components
Use `form-builder` skill for form patterns.
Create in route's `_components/` directory:
1. **List component** - Display items with loading states
2. **Form component** - Create/edit with validation
3. **Detail component** - Single item view
### Phase 4: Page Integration
Create page in appropriate route group:
- Personal: `apps/web/app/home/(user)/feature/`
- Team: `apps/web/app/home/[account]/feature/`
```typescript
// apps/web/app/home/[account]/projects/page.tsx
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { PageBody, PageHeader } from '@kit/ui/page';
import { ProjectsList } from './_components/projects-list';
interface Props {
params: Promise<{ account: string }>;
}
export default async function ProjectsPage({ params }: Props) {
const { account } = await params;
const client = getSupabaseServerClient();
const { data: projects } = await client
.from('projects')
.select('*')
.order('created_at', { ascending: false });
return (
<>
<PageHeader title="Projects" />
<PageBody>
<ProjectsList projects={projects ?? []} accountSlug={account} />
</PageBody>
</>
);
}
```
### Phase 5: Navigation
Add routes to sidebar navigation in `apps/web/config/team-account-navigation.config.tsx` or `apps/web/config/personal-account-navigation.config.tsx`.
## File Structure
```
apps/web/app/home/[account]/projects/
├── page.tsx # List page
├── [projectId]/
│ └── page.tsx # Detail page
├── _components/
│ ├── projects-list.tsx
│ ├── project-form.tsx
│ └── project-card.tsx
└── _lib/
├── schemas/
│ └── project.schema.ts
└── server/
├── project.service.ts
└── server-actions.ts
```
## Verification Checklist
### Database Layer
- [ ] Schema file created in `apps/web/supabase/schemas/`
- [ ] RLS enabled on table
- [ ] Default permissions revoked
- [ ] Specific permissions granted to `authenticated`
- [ ] RLS policies use helper functions (`has_role_on_account`, `has_permission`)
- [ ] Indexes added for foreign keys and common queries
- [ ] Timestamps triggers added if applicable
- [ ] Migration generated and applied
- [ ] TypeScript types regenerated
### Server Layer
- [ ] Zod schema in `_lib/schemas/`
- [ ] Service class in `_lib/server/`
- [ ] Server actions use `enhanceAction`
- [ ] Actions have `auth: true` and `schema` options
- [ ] Logging added for operations
- [ ] `revalidatePath` called after mutations
- [ ] Error handling with `isRedirectError` check if applicable (i.e. when using redirect() in a server action)
### UI Layer
- [ ] Components in `_components/` directory
- [ ] Forms use `react-hook-form` with `zodResolver`
- [ ] Loading states with `useTransition`
- [ ] Error display with `Alert` component
- [ ] `data-test` attributes for E2E testing
- [ ] `Trans` component for all user-facing strings
- [ ] Toast notifications for success/error if applicable
### Page Layer
- [ ] Page in correct route group (user vs team)
- [ ] Async params handled with `await params`
- [ ] Server-side data fetching
- [ ] `PageHeader` and `PageBody` components used
- [ ] Proper error boundaries
### Navigation
- [ ] Path added to `config/paths.config.ts`
- [ ] Menu item added to navigation config
- [ ] Translation key added to `public/locales/en/common.json`
### Testing
- [ ] Page Object created for E2E tests
- [ ] Basic CRUD operations tested
- [ ] Error states tested
- [ ] `data-test` selectors used in tests
### Final Verification
```bash
# Type check
pnpm typecheck
# Lint
pnpm lint:fix
# Format
pnpm format:fix
# Test (if tests exist)
pnpm --filter web-e2e exec playwright test feature-name --workers=1
```
When you are done, run the code quality reviewer agent to verify the code quality.

View File

@@ -0,0 +1,311 @@
# Agent Evaluation: Full Feature Implementation
This eval tests whether the agent correctly follows Makerkit patterns when implementing a complete feature spanning database, API, and UI layers.
## Eval Metadata
- **Type**: Capability eval (target: improvement over time)
- **Complexity**: High (multi-step, multi-file)
- **Expected Duration**: 15-30 minutes
- **Skills Tested**: `/feature-builder`, `/server-action-builder`, `/react-form-builder`, `/postgres-expert`, `/navigation-config`
---
## Task: Implement "Projects" Feature
### Prompt
```
Implement a "Projects" feature for team accounts with the following requirements:
1. Database: Projects table with name, description, status (enum: draft/active/archived), and account_id
2. Server: CRUD actions for projects (create, update, delete, list)
3. UI: Projects list page with create/edit forms
4. Navigation: Add to team sidebar
Use the available skills for guidance. The feature should be accessible at /home/[account]/projects.
```
### Reference Solution Exists
A correct implementation requires:
- 1 schema file
- 1 migration
- 1 Zod schema file
- 1 service file
- 1 server actions file
- 2-3 component files
- 1 page file
- Config updates (paths, navigation, translations)
---
## Success Criteria (Grading Rubric)
### 1. Database Layer (25 points)
| Criterion | Points | Grader Type | Pass Condition |
|-----------|--------|-------------|----------------|
| Schema file created in `apps/web/supabase/schemas/` | 3 | Code | File exists with `.sql` extension |
| Table has correct columns | 5 | Code | Contains: id, account_id, name, description, status, created_at |
| RLS enabled | 5 | Code | Contains `enable row level security` |
| Uses helper functions in policies | 5 | Code | Contains `has_role_on_account` OR `has_permission` |
| Permissions revoked/granted correctly | 4 | Code | Contains `revoke all` AND `grant select, insert, update, delete` |
| Status enum created | 3 | Code | Contains `create type` with draft/active/archived |
**Anti-patterns to penalize (-3 each):**
- SECURITY DEFINER without access checks
- Missing `on delete cascade` for account_id FK
- No index on account_id
### 2. Server Layer (25 points)
| Criterion | Points | Grader Type | Pass Condition |
|-----------|--------|-------------|----------------|
| Zod schema in `_lib/schemas/` | 3 | Code | File exists, exports schema with `z.object` |
| Service class pattern used | 5 | Code | Contains `class` with methods, uses `getSupabaseServerClient` |
| Actions use `enhanceAction` | 5 | Code | Import from `@kit/next/actions`, wraps handler |
| Actions have `auth: true` | 3 | Code | Options object contains `auth: true` |
| Actions have `schema` validation | 3 | Code | Options object contains `schema:` |
| Uses `revalidatePath` after mutations | 3 | Code | Import and call `revalidatePath` |
| Logging with `getLogger` | 3 | Model | Appropriate logging before/after operations |
**Anti-patterns to penalize (-3 each):**
- Manual auth checks instead of trusting RLS
- `await logger.info()` (logger methods are not promises)
- Business logic in action instead of service
### 3. UI Layer (25 points)
| Criterion | Points | Grader Type | Pass Condition |
|-----------|--------|-------------|----------------|
| Components in `_components/` directory | 2 | Code | Path contains `_components/` |
| Form uses `react-hook-form` with `zodResolver` | 5 | Code | Imports both, uses `useForm({ resolver: zodResolver() })` |
| No generics on `useForm` | 3 | Code | NOT contains `useForm<` |
| Uses `@kit/ui/form` components | 4 | Code | Imports `Form, FormField, FormItem, FormLabel, FormControl, FormMessage` |
| Uses `Trans` for strings | 3 | Code | Import from `@kit/ui/trans`, uses `<Trans i18nKey=` |
| Uses `useTransition` for loading | 3 | Code | `const [pending, startTransition] = useTransition()` |
| Has `data-test` attributes | 3 | Code | Contains `data-test=` on form/buttons |
| Error handling with `isRedirectError` | 2 | Code | Import and check in catch block |
**Anti-patterns to penalize (-3 each):**
- `useForm<SomeType>` with explicit generic
- Using `watch()` instead of `useWatch`
- Hardcoded strings without `Trans`
- Missing `FormMessage` for error display
### 4. Integration & Navigation (15 points)
| Criterion | Points | Grader Type | Pass Condition |
|-----------|--------|-------------|----------------|
| Page in correct route group | 3 | Code | Path is `app/home/[account]/projects/page.tsx` |
| Uses `await params` pattern | 3 | Code | Contains `const { account } = await params` |
| Path added to `paths.config.ts` | 3 | Code | Contains `projects` path |
| Nav item added to team config | 3 | Code | Entry in `team-account-navigation.config.tsx` |
| Translation key added | 3 | Code | Entry in `public/locales/en/common.json` |
### 5. Code Quality (10 points)
| Criterion | Points | Grader Type | Pass Condition |
|-----------|--------|-------------|----------------|
| TypeScript compiles | 5 | Code | `pnpm typecheck` exits 0 |
| Lint passes | 3 | Code | `pnpm lint:fix` exits 0 |
| Format passes | 2 | Code | `pnpm format:fix` exits 0 |
---
## Grader Implementation
### Code-Based Grader (Automated)
```typescript
interface EvalResult {
score: number;
maxScore: number;
passed: boolean;
details: {
criterion: string;
points: number;
maxPoints: number;
evidence: string;
}[];
antiPatterns: string[];
}
async function gradeFeatureImplementation(): Promise<EvalResult> {
const details = [];
const antiPatterns = [];
// 1. Check schema file
const schemaFiles = glob('apps/web/supabase/schemas/*project*.sql');
const schemaContent = schemaFiles.length > 0 ? read(schemaFiles[0]) : '';
details.push({
criterion: 'Schema file exists',
points: schemaFiles.length > 0 ? 3 : 0,
maxPoints: 3,
evidence: schemaFiles[0] || 'No schema file found'
});
details.push({
criterion: 'RLS enabled',
points: schemaContent.includes('enable row level security') ? 5 : 0,
maxPoints: 5,
evidence: 'Checked for RLS statement'
});
// Check anti-patterns
if (schemaContent.includes('security definer') &&
!schemaContent.includes('has_permission') &&
!schemaContent.includes('is_account_owner')) {
antiPatterns.push('SECURITY DEFINER without access validation');
}
// 2. Check server files
const actionFiles = glob('apps/web/app/home/[account]/projects/**/*actions*.ts');
const actionContent = actionFiles.length > 0 ? read(actionFiles[0]) : '';
details.push({
criterion: 'Uses enhanceAction',
points: actionContent.includes('enhanceAction') ? 5 : 0,
maxPoints: 5,
evidence: 'Checked for enhanceAction import/usage'
});
if (actionContent.includes('await logger.info')) {
antiPatterns.push('await on logger.info (not a promise)');
}
// 3. Check UI files
const componentFiles = glob('apps/web/app/home/[account]/projects/_components/*.tsx');
const formContent = componentFiles.map(f => read(f)).join('\n');
details.push({
criterion: 'No generics on useForm',
points: !formContent.includes('useForm<') ? 3 : 0,
maxPoints: 3,
evidence: 'Checked for useForm<Type> pattern'
});
if (formContent.includes('useForm<')) {
antiPatterns.push('Explicit generic on useForm (should use zodResolver inference)');
}
// 4. Check integration
const pathsConfig = read('apps/web/config/paths.config.ts');
details.push({
criterion: 'Path configured',
points: pathsConfig.includes('projects') ? 3 : 0,
maxPoints: 3,
evidence: 'Checked paths.config.ts'
});
// 5. Run verification
const typecheckResult = await exec('pnpm typecheck');
details.push({
criterion: 'TypeScript compiles',
points: typecheckResult.exitCode === 0 ? 5 : 0,
maxPoints: 5,
evidence: `Exit code: ${typecheckResult.exitCode}`
});
// Calculate totals
const score = details.reduce((sum, d) => sum + d.points, 0);
const maxScore = details.reduce((sum, d) => sum + d.maxPoints, 0);
const penaltyPoints = antiPatterns.length * 3;
return {
score: Math.max(0, score - penaltyPoints),
maxScore,
passed: (score - penaltyPoints) >= maxScore * 0.8, // 80% threshold
details,
antiPatterns
};
}
```
### Model-Based Grader (For Nuanced Criteria)
```
You are evaluating an AI agent's implementation of a "Projects" feature in a Makerkit SaaS application.
Review the following files and assess:
1. **Logging Quality** (0-3 points):
- Are log messages descriptive and include relevant context (userId, projectId)?
- Is logging done before AND after important operations?
- Are error cases logged with appropriate severity?
2. **Code Organization** (0-3 points):
- Is business logic in services, not actions?
- Are files in the correct directories per Makerkit conventions?
- Is there appropriate separation of concerns?
3. **Error Handling** (0-3 points):
- Are errors handled gracefully?
- Does the UI show appropriate error states?
- Are redirect errors handled correctly?
Provide a score for each criterion with brief justification.
```
---
## Trial Configuration
```yaml
trials: 3 # Run 3 times to account for non-determinism
pass_threshold: 0.8 # 80% of max score
metrics:
- pass@1: "Passes on first attempt"
- pass@3: "Passes at least once in 3 attempts"
- pass^3: "Passes all 3 attempts (reliability)"
```
---
## Environment Setup
Before each trial:
1. Reset to clean git state: `git checkout -- .`
2. Ensure Supabase types are current: `pnpm supabase:web:typegen`
3. Verify clean typecheck: `pnpm typecheck`
After each trial:
1. Capture transcript (full conversation)
2. Capture outcome (files created/modified)
3. Run graders
4. Reset environment
---
## Expected Failure Modes
Document these to distinguish agent errors from eval problems:
| Failure | Likely Cause | Is Eval Problem? |
|---------|--------------|------------------|
| Missing RLS | Agent didn't follow postgres-expert skill | No |
| `useForm<Type>` | Agent ignored react-form-builder guidance | No |
| Wrong file path | Ambiguous task description | Maybe - clarify paths |
| Typecheck fails on unrelated code | Existing codebase issue | Yes - fix baseline |
| Agent uses different but valid approach | Eval too prescriptive | Yes - grade outcome not path |
---
## Iteration Log
Track eval refinements here:
| Date | Change | Reason |
|------|--------|--------|
| Initial | Created eval | - |
| | | |
---
## Notes
- **Grade outcomes, not paths**: If agent creates a working feature with slightly different file organization, that's acceptable
- **Partial credit**: A feature missing navigation but with working CRUD is still valuable
- **Read transcripts**: When scores are low, check if agent attempted to use skills or ignored them entirely

View File

@@ -1,13 +1,13 @@
--- ---
name: playwright-e2e-expert name: playwright-e2e
description: Use this agent when you need to write, review, or debug end-to-end tests using Playwright. This includes creating new test suites, fixing flaky tests, implementing complex UI interaction sequences, or ensuring test reliability and isolation. The agent excels at handling asynchronous operations, managing test concurrency, and applying Playwright best practices.\n\nExamples:\n<example>\nContext: The user needs to write e2e tests for a new feature.\nuser: "Write tests for the user registration flow"\nassistant: "I'll use the playwright-e2e-expert agent to create comprehensive end-to-end tests for the registration flow."\n<commentary>\nSince the user needs e2e tests written, use the Task tool to launch the playwright-e2e-expert agent to create robust Playwright tests.\n</commentary>\n</example>\n<example>\nContext: The user has flaky tests that need fixing.\nuser: "These login tests keep failing intermittently"\nassistant: "Let me use the playwright-e2e-expert agent to diagnose and fix the flaky test issues."\n<commentary>\nThe user has problematic e2e tests, so use the playwright-e2e-expert agent to apply best practices for test stability.\n</commentary>\n</example>\n<example>\nContext: After implementing a complex UI feature, e2e tests should be written.\nuser: "I've just finished the multi-step checkout process"\nassistant: "Now I'll use the playwright-e2e-expert agent to create thorough e2e tests for the checkout flow."\n<commentary>\nProactively use the playwright-e2e-expert agent after complex UI features are implemented to ensure proper test coverage.\n</commentary>\n</example> description: Write, review, or debug end-to-end tests using Playwright. Use when creating test suites, fixing flaky tests, implementing UI interaction sequences, or ensuring test reliability. Invoke with /playwright-e2e or when user mentions e2e tests, Playwright, or test automation.
model: sonnet
color: green
--- ---
# Playwright E2E Testing Expert
You are an elite QA automation engineer with deep expertise in Playwright and end-to-end testing. Your mastery encompasses the intricacies of browser automation, asynchronous JavaScript execution, and the unique challenges of UI testing. You are an elite QA automation engineer with deep expertise in Playwright and end-to-end testing. Your mastery encompasses the intricacies of browser automation, asynchronous JavaScript execution, and the unique challenges of UI testing.
**Core Expertise:** ## Core Expertise
You understand that e2e testing requires a fundamentally different approach from unit testing. You know that UI interactions are inherently asynchronous and that timing issues are the root of most test failures. You excel at: You understand that e2e testing requires a fundamentally different approach from unit testing. You know that UI interactions are inherently asynchronous and that timing issues are the root of most test failures. You excel at:
@@ -17,7 +17,7 @@ You understand that e2e testing requires a fundamentally different approach from
- Managing test isolation through proper setup and teardown procedures - Managing test isolation through proper setup and teardown procedures
- Handling dynamic content, animations, and network requests gracefully - Handling dynamic content, animations, and network requests gracefully
**Testing Philosophy:** ## Testing Philosophy
You write tests that verify actual user workflows and business logic, not trivial UI presence checks. Each test you create: You write tests that verify actual user workflows and business logic, not trivial UI presence checks. Each test you create:
- Has a clear purpose and tests meaningful functionality - Has a clear purpose and tests meaningful functionality
@@ -26,7 +26,7 @@ You write tests that verify actual user workflows and business logic, not trivia
- Avoids conditional logic that makes tests unpredictable - Avoids conditional logic that makes tests unpredictable
- Includes descriptive test names that explain what is being tested and why - Includes descriptive test names that explain what is being tested and why
**Technical Approach:** ## Technical Approach
When writing tests, you: When writing tests, you:
1. Always use `await` for every Playwright action and assertion 1. Always use `await` for every Playwright action and assertion
@@ -34,9 +34,9 @@ When writing tests, you:
3. Use `expect()` with Playwright's web-first assertions for automatic retries 3. Use `expect()` with Playwright's web-first assertions for automatic retries
4. Implement Page Object Model when tests become complex 4. Implement Page Object Model when tests become complex
5. Never use `page.waitForTimeout()` except as an absolute last resort 5. Never use `page.waitForTimeout()` except as an absolute last resort
6. Chain actions logically: interact wait for response assert proceed 6. Chain actions logically: interact -> wait for response -> assert -> proceed
**Common Pitfalls You Avoid:** ## Common Pitfalls You Avoid
- Race conditions from not waiting for network requests or state changes - Race conditions from not waiting for network requests or state changes
- Brittle selectors that break with minor UI changes - Brittle selectors that break with minor UI changes
@@ -45,7 +45,7 @@ When writing tests, you:
- Missing error boundaries that cause cascading failures - Missing error boundaries that cause cascading failures
- Ignoring viewport sizes and responsive behavior - Ignoring viewport sizes and responsive behavior
**Best Practices You Follow:** ## Best Practices
```typescript ```typescript
// You write tests like this: // You write tests like this:
@@ -74,6 +74,8 @@ test('user can complete checkout', async ({ page }) => {
You understand that e2e tests are expensive to run and maintain, so each test you write provides maximum value. You balance thoroughness with practicality, ensuring tests are comprehensive enough to catch real issues but simple enough to debug when they fail. You understand that e2e tests are expensive to run and maintain, so each test you write provides maximum value. You balance thoroughness with practicality, ensuring tests are comprehensive enough to catch real issues but simple enough to debug when they fail.
## Debugging Failed Tests
When debugging failed tests, you systematically analyze: When debugging failed tests, you systematically analyze:
1. Screenshots and trace files to understand the actual state 1. Screenshots and trace files to understand the actual state
2. Network activity to identify failed or slow requests 2. Network activity to identify failed or slow requests

View File

@@ -0,0 +1,156 @@
# Makerkit E2E Testing Patterns
## Page Objects Location
`apps/e2e/tests/*.po.ts`
## Auth Page Object
```typescript
export class AuthPageObject {
constructor(private readonly page: Page) {}
static MFA_KEY = 'test-mfa-key';
async signIn(params: { email: string; password: string }) {
await this.page.fill('input[name="email"]', params.email);
await this.page.fill('input[name="password"]', params.password);
await this.page.click('button[type="submit"]');
}
async signOut() {
await this.page.click('[data-test="account-dropdown-trigger"]');
await this.page.click('[data-test="account-dropdown-sign-out"]');
}
async bootstrapUser(params: { email: string; password: string; name: string }) {
// Creates user via API
await fetch('/api/test/create-user', {
method: 'POST',
body: JSON.stringify(params),
});
}
async loginAsUser(params: { email: string; password: string }) {
await this.page.goto('/auth/sign-in');
await this.signIn(params);
await this.page.waitForURL('**/home/**');
}
createRandomEmail() {
const value = Math.random() * 10000000000000;
return `${value.toFixed(0)}@makerkit.dev`;
}
}
```
## Common Selectors
```typescript
// Account dropdown
'[data-test="account-dropdown-trigger"]'
'[data-test="account-dropdown-sign-out"]'
// Navigation
'[data-test="sidebar-menu"]'
'[data-test="mobile-menu-trigger"]'
// Forms
'[data-test="submit-button"]'
'[data-test="cancel-button"]'
// Modals
'[data-test="dialog-confirm"]'
'[data-test="dialog-cancel"]'
```
## Test Setup Pattern
```typescript
// tests/auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
const auth = new AuthPageObject(page);
await auth.bootstrapUser({
email: 'test@example.com',
password: 'password123',
name: 'Test User',
});
await auth.loginAsUser({
email: 'test@example.com',
password: 'password123',
});
// Save authentication state
await page.context().storageState({ path: '.auth/user.json' });
});
```
## Reliability Patterns
### OTP/Email Operations
```typescript
await expect(async () => {
const otpCode = await this.getOtpCodeFromEmail(email);
expect(otpCode).not.toBeNull();
await this.enterOtpCode(otpCode);
}).toPass();
```
### MFA Verification
```typescript
await expect(async () => {
await auth.submitMFAVerification(AuthPageObject.MFA_KEY);
}).toPass({
intervals: [500, 2500, 5000, 7500, 10_000, 15_000, 20_000]
});
```
### Network Requests
```typescript
await expect(async () => {
const response = await this.page.waitForResponse(
resp => resp.url().includes('auth/v1/user')
);
expect(response.status()).toBe(200);
}).toPass();
```
## Test Organization
```
apps/e2e/
├── playwright.config.ts
├── tests/
│ ├── auth.setup.ts
│ ├── authentication/
│ │ ├── sign-in.spec.ts
│ │ └── sign-up.spec.ts
│ ├── billing/
│ │ └── subscription.spec.ts
│ ├── teams/
│ │ └── invitations.spec.ts
│ └── utils/
│ └── auth.po.ts
└── .auth/
└── user.json
```
## Running Tests
```bash
# Single file
pnpm --filter web-e2e exec playwright test authentication --workers=1
# With UI
pnpm --filter web-e2e exec playwright test --ui
# Debug mode
pnpm --filter web-e2e exec playwright test --debug
```

View File

@@ -1,10 +1,10 @@
--- ---
name: postgres-expert name: postgres-supabase-expert
description: MUST USE this agent when you need to create, review, optimize, or test SQL, PostgreSQL, Supabase database code including schemas, migrations, functions, triggers, RLS policies, and PgTAP tests. This includes tasks like designing new database schemas, reviewing existing SQL for safety and performance, writing migrations that preserve data integrity, implementing row-level security, optimizing queries, or creating comprehensive database tests.\n\nExamples:\n- <example>\n Context: The user needs to create a new database schema for a feature.\n user: "I need to add a comments system to my app with proper permissions"\n assistant: "I'll use the postgres-expert agent to design a robust comments schema with RLS policies"\n <commentary>\n Since this involves creating database schemas and security policies, the postgres-expert should handle this.\n </commentary>\n</example>\n- <example>\n Context: The user has written a migration and wants it reviewed.\n user: "I've created a migration to add user profiles, can you check if it's safe?"\n assistant: "Let me use the postgres-expert agent to review your migration for safety and best practices"\n <commentary>\n Database migration review requires expertise in non-destructive changes and data integrity.\n </commentary>\n</example>\n- <example>\n Context: The user needs help with database performance.\n user: "My query is running slowly, it's fetching posts with their comments"\n assistant: "I'll engage the postgres-expert agent to analyze and optimize your query performance"\n <commentary>\n Query optimization requires deep PostgreSQL knowledge that this specialist agent provides.\n </commentary>\n</example> description: Create, review, optimize, or test PostgreSQL and Supabase database code including SQL code, schemas, migrations, functions, triggers, RLS policies, and PgTAP tests. Use when writing and designing schemas, reviewing SQL for safety, writing migrations, implementing row-level security, or optimizing queries. Invoke with /postgres-supabase-expert or when user mentions database, SQL, migrations, RLS, or schema design.
model: sonnet
color: green
--- ---
# PostgreSQL & Supabase Database Expert
You are an elite PostgreSQL and Supabase database architect with deep expertise in designing, implementing, and testing production-grade database systems. Your mastery spans schema design, performance optimization, data integrity, security, and testing methodologies. You are an elite PostgreSQL and Supabase database architect with deep expertise in designing, implementing, and testing production-grade database systems. Your mastery spans schema design, performance optimization, data integrity, security, and testing methodologies.
## Core Expertise ## Core Expertise
@@ -110,3 +110,11 @@ You will anticipate and handle:
When reviewing existing code, you will identify issues related to security vulnerabilities, performance bottlenecks, data integrity risks, missing indexes, improper transaction boundaries, and suggest specific, actionable improvements with example code. When reviewing existing code, you will identify issues related to security vulnerabilities, performance bottlenecks, data integrity risks, missing indexes, improper transaction boundaries, and suggest specific, actionable improvements with example code.
You communicate technical concepts clearly, providing rationale for all recommendations and trade-offs for different approaches. You stay current with PostgreSQL and Supabase latest features and best practices. You communicate technical concepts clearly, providing rationale for all recommendations and trade-offs for different approaches. You stay current with PostgreSQL and Supabase latest features and best practices.
## Examples
See `[Examples](examples.md)` for examples of database code.
## Patterns and Functions
See `[Patterns and Functions](makerkit.md)` for patterns and functions.

View File

@@ -0,0 +1,144 @@
# Makerkit Database Examples
Real examples from the codebase.
## Accounts Schema
Location: `apps/web/supabase/schemas/03-accounts.sql`
```sql
create table if not exists public.accounts (
id uuid unique not null default extensions.uuid_generate_v4(),
primary_owner_user_id uuid references auth.users on delete cascade not null,
name varchar(255) not null,
slug varchar(255) unique,
is_personal_account boolean not null default false,
picture_url varchar(1000),
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
primary key (id)
);
alter table "public"."accounts" enable row level security;
```
## Account Memberships
Location: `apps/web/supabase/schemas/04-accounts-memberships.sql`
```sql
create table if not exists public.accounts_memberships (
account_id uuid references public.accounts(id) on delete cascade not null,
user_id uuid references auth.users(id) on delete cascade not null,
account_role varchar(50) not null,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
created_by uuid references auth.users(id),
updated_by uuid references auth.users(id),
primary key (account_id, user_id)
);
-- RLS policies using helper functions
create policy accounts_memberships_select on public.accounts_memberships
for select to authenticated using (
user_id = (select auth.uid())
or public.has_role_on_account(account_id)
);
```
## Subscriptions
Location: `apps/web/supabase/schemas/11-subscriptions.sql`
```sql
create table if not exists public.subscriptions (
id varchar(255) not null,
account_id uuid not null references public.accounts(id) on delete cascade,
billing_customer_id varchar(255) not null,
status public.subscription_status not null,
currency varchar(10) not null,
cancel_at_period_end boolean not null default false,
period_starts_at timestamp with time zone,
period_ends_at timestamp with time zone,
trial_starts_at timestamp with time zone,
trial_ends_at timestamp with time zone,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
primary key (id)
);
```
## Notifications
Location: `apps/web/supabase/schemas/12-notifications.sql`
```sql
create table if not exists public.notifications (
id uuid primary key default extensions.uuid_generate_v4(),
account_id uuid references public.accounts(id) on delete cascade not null,
type public.notification_type not null,
body jsonb not null default '{}',
dismissed boolean not null default false,
link text,
created_at timestamptz default now() not null
);
-- Only account members can see notifications
create policy read_notifications on public.notifications
for select to authenticated using (
public.has_role_on_account(account_id)
);
```
## Storage Bucket
Location: `apps/web/supabase/schemas/16-storage.sql`
```sql
insert into storage.buckets (id, name, public)
values ('account_image', 'account_image', true)
on conflict (id) do nothing;
create policy account_image on storage.objects for all using (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
)
with check (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_permission(
auth.uid(),
kit.get_storage_filename_as_uuid(name),
'settings.manage'
)
)
);
```
## Enum Types
```sql
-- Subscription status
create type public.subscription_status as enum (
'active',
'trialing',
'past_due',
'canceled',
'unpaid',
'incomplete',
'incomplete_expired',
'paused'
);
-- App permissions
create type public.app_permissions as enum (
'settings.manage',
'billing.manage',
'members.manage',
'invitations.manage'
);
```

View File

@@ -0,0 +1,138 @@
# Makerkit Database Patterns
## Schema Location
All schemas are in `apps/web/supabase/schemas/` with numbered prefixes for dependency ordering.
## Existing Helper Functions - DO NOT Recreate
```sql
-- Account Access Control
public.has_role_on_account(account_id uuid, role_name? text)
public.has_permission(user_id uuid, account_id uuid, permission app_permissions)
public.is_account_owner(account_id uuid)
public.has_active_subscription(account_id uuid)
public.is_team_member(account_id uuid, user_id uuid)
public.can_action_account_member(target_account_id uuid, target_user_id uuid)
-- Administrative
public.is_super_admin()
public.is_aal2()
public.is_mfa_compliant()
-- Configuration
public.is_set(field_name text)
```
## RLS Policy Patterns
### Personal + Team Access
```sql
create policy "table_read" on public.table for select
to authenticated using (
account_id = (select auth.uid()) or
public.has_role_on_account(account_id)
);
```
### Permission-Based Access
```sql
create policy "table_manage" on public.table for all
to authenticated using (
public.has_permission(auth.uid(), account_id, 'feature.manage'::app_permissions)
);
```
### Storage Bucket Policy
```sql
create policy bucket_policy on storage.objects for all using (
bucket_id = 'bucket_name'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
);
```
## Adding New Permissions
```sql
-- Add to app_permissions enum
ALTER TYPE public.app_permissions ADD VALUE 'feature.manage';
COMMIT;
```
## Standard Table Template
```sql
create table if not exists public.feature (
id uuid unique not null default extensions.uuid_generate_v4(),
account_id uuid references public.accounts(id) on delete cascade not null,
name varchar(255) not null,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
created_by uuid references auth.users(id),
updated_by uuid references auth.users(id),
primary key (id)
);
-- Enable RLS
alter table "public"."feature" enable row level security;
-- Revoke defaults, grant specific
revoke all on public.feature from authenticated, service_role;
grant select, insert, update, delete on table public.feature to authenticated;
-- Add triggers
create trigger set_timestamps
before insert or update on public.feature
for each row execute function public.trigger_set_timestamps();
create trigger set_user_tracking
before insert or update on public.feature
for each row execute function public.trigger_set_user_tracking();
-- Add indexes
create index ix_feature_account_id on public.feature(account_id);
```
## Migration Workflow
```bash
# New entity: copy schema to migration
pnpm --filter web run supabase migrations new feature_name
# Modify existing: generate diff
pnpm --filter web run supabase:db:diff -f update_feature
# Apply
pnpm --filter web supabase migrations up
# Generate types
pnpm supabase:web:typegen
```
## Security Definer Function Pattern
```sql
create or replace function public.admin_function(target_id uuid)
returns void
language plpgsql
security definer
set search_path = ''
as $$
begin
-- ALWAYS validate permissions first
if not public.is_account_owner(target_id) then
raise exception 'Access denied';
end if;
-- Safe to proceed
end;
$$;
grant execute on function public.admin_function(uuid) to authenticated;
```

View File

@@ -0,0 +1,197 @@
---
name: forms-builder
description: Create or modify client-side forms in React applications following best practices for react-hook-form, @kit/ui/form components, and server actions integration. Use when building forms with validation, error handling, loading states, and TypeScript typing. Invoke with /react-form-builder or when user mentions creating forms, form validation, or react-hook-form.
---
# React Form Builder Expert
You are an expert React form architect specializing in building robust, accessible, and type-safe forms using react-hook-form, @kit/ui/form components, and Next.js server actions. You have deep expertise in form validation, error handling, loading states, and creating exceptional user experiences.
## Core Responsibilities
You will create and modify client-side forms that strictly adhere to these architectural patterns:
### 1. Form Structure Requirements
- Always use `useForm` from react-hook-form WITHOUT redundant generic types when using zodResolver
- Implement Zod schemas for validation, stored in `_lib/schemas/` directory
- Use `@kit/ui/form` components (Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage)
- Handle loading states with `useTransition` hook
- Implement proper error handling with try/catch blocks
### 2. Server Action Integration
- Call server actions within `startTransition` for proper loading states
- Handle redirect errors using `isRedirectError` from 'next/dist/client/components/redirect-error'
- Display error states using Alert components from '@kit/ui/alert'
- Ensure server actions are imported from dedicated server files
### 3. Code Organization Pattern
```
_lib/
├── schemas/
│ └── feature.schema.ts # Shared Zod schemas
├── server/
│ └── server-actions.ts # Server actions
└── client/
└── forms.tsx # Form components
```
### 4. Import Guidelines
- Toast notifications: `import { toast } from '@kit/ui/sonner'`
- Form components: `import { Form, FormField, ... } from '@kit/ui/form'`
- Always check @kit/ui for components before using external packages
- Use `Trans` component from '@kit/ui/trans' for internationalization
### 5. Best Practices You Must Follow
- Add `data-test` attributes for E2E testing on form elements and submit buttons
- Use `reValidateMode: 'onChange'` and `mode: 'onChange'` for responsive validation
- Implement proper TypeScript typing without using `any`
- Handle both success and error states gracefully
- Use `If` component from '@kit/ui/if' for conditional rendering
- Disable submit buttons during pending states
- Include FormDescription for user guidance
- Use Dialog components from '@kit/ui/dialog' when forms are in modals
### 6. State Management
- Use `useState` for error states
- Use `useTransition` for pending states
- Avoid multiple separate useState calls - prefer single state objects when appropriate
- Never use useEffect unless absolutely necessary and justified
### 7. Validation Patterns
- Create reusable Zod schemas that can be shared between client and server
- Use schema.refine() for custom validation logic
- Provide clear, user-friendly error messages
- Implement field-level validation with proper error display
### 8. Error Handling Template
```typescript
const onSubmit = (data: FormData) => {
startTransition(async () => {
try {
await serverAction(data);
} catch (error) {
if (!isRedirectError(error)) {
setError(true);
}
}
});
};
```
### 9. Type Safety
- Let zodResolver infer types - don't add redundant generics to useForm
- Export schema types when needed for reuse
- Ensure all form fields have proper typing
### 10. Accessibility and UX
- Always include FormLabel for screen readers
- Provide helpful FormDescription text
- Show clear error messages with FormMessage
- Implement loading indicators during form submission
- Use semantic HTML and ARIA attributes where appropriate
## Complete Form Example
```tsx
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTransition, useState } from 'react';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Button } from '@kit/ui/button';
import { Alert, AlertDescription } from '@kit/ui/alert';
import { If } from '@kit/ui/if';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';
import { CreateEntitySchema } from '../_lib/schemas/entity.schema';
import { createEntityAction } from '../_lib/server/server-actions';
export function CreateEntityForm() {
const [pending, startTransition] = useTransition();
const [error, setError] = useState(false);
const form = useForm({
resolver: zodResolver(CreateEntitySchema),
defaultValues: {
name: '',
description: '',
},
mode: 'onChange',
reValidateMode: 'onChange',
});
const onSubmit = (data: z.infer<typeof CreateEntitySchema>) => {
setError(false);
startTransition(async () => {
try {
await createEntityAction(data);
toast.success('Entity created successfully');
} catch (e) {
if (!isRedirectError(e)) {
setError(true);
}
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Form {...form}>
<If condition={error}>
<Alert variant="destructive">
<AlertDescription>
<Trans i18nKey="common:errors.generic" />
</AlertDescription>
</Alert>
</If>
<FormField
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="entity:name" />
</FormLabel>
<FormControl>
<Input
data-test="entity-name-input"
placeholder="Enter name"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
disabled={pending}
data-test="submit-entity-button"
>
{pending ? (
<Trans i18nKey="common:creating" />
) : (
<Trans i18nKey="common:create" />
)}
</Button>
</Form>
</form>
);
}
```
When creating forms, you will analyze requirements and produce complete, production-ready implementations that handle all edge cases, provide excellent user feedback, and maintain consistency with the codebase's established patterns. You prioritize type safety, reusability, and maintainability in every form you create.
Always verify that UI components exist in @kit/ui before importing from external packages, and ensure your forms integrate seamlessly with the project's internationalization system using Trans components.
## Components
See `[Components](components.md)` for examples of form components.

View File

@@ -0,0 +1,249 @@
# Makerkit Form Components Reference
## Import Pattern
```typescript
import { Form, FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Button } from '@kit/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kit/ui/select';
import { Textarea } from '@kit/ui/textarea';
import { Checkbox } from '@kit/ui/checkbox';
import { Switch } from '@kit/ui/switch';
import { Alert, AlertDescription } from '@kit/ui/alert';
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { toast } from '@kit/ui/sonner';
```
## Form Field Pattern
```tsx
<FormField
name="fieldName"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey="namespace:fieldLabel" />
</FormLabel>
<FormControl>
<Input
data-test="field-name-input"
placeholder="Enter value"
{...field}
/>
</FormControl>
<FormDescription>
<Trans i18nKey="namespace:fieldDescription" />
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
```
## Select Field
```tsx
<FormField
name="category"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger data-test="category-select">
<SelectValue placeholder="Select category" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
```
## Checkbox Field
```tsx
<FormField
name="acceptTerms"
control={form.control}
render={({ field }) => (
<FormItem className="flex items-center space-x-2">
<FormControl>
<Checkbox
data-test="accept-terms-checkbox"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel className="!mt-0">
<Trans i18nKey="namespace:acceptTerms" />
</FormLabel>
</FormItem>
)}
/>
```
## Switch Field
```tsx
<FormField
name="notifications"
control={form.control}
render={({ field }) => (
<FormItem className="flex items-center justify-between">
<div>
<FormLabel>Enable Notifications</FormLabel>
<FormDescription>Receive email notifications</FormDescription>
</div>
<FormControl>
<Switch
data-test="notifications-switch"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
```
## Textarea Field
```tsx
<FormField
name="description"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
data-test="description-textarea"
placeholder="Enter description..."
rows={4}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```
## Error Alert
```tsx
<If condition={error}>
<Alert variant="destructive">
<AlertDescription>
<Trans i18nKey="common:errors.generic" />
</AlertDescription>
</Alert>
</If>
```
## Submit Button
```tsx
<Button
type="submit"
disabled={pending}
data-test="submit-button"
>
{pending ? (
<Trans i18nKey="common:submitting" />
) : (
<Trans i18nKey="common:submit" />
)}
</Button>
```
## Complete Form Template
```tsx
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTransition, useState } from 'react';
import { isRedirectError } from 'next/dist/client/components/redirect-error';
import type { z } from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Button } from '@kit/ui/button';
import { Alert, AlertDescription } from '@kit/ui/alert';
import { If } from '@kit/ui/if';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';
import { MySchema } from '../_lib/schemas/my.schema';
import { myAction } from '../_lib/server/server-actions';
export function MyForm() {
const [pending, startTransition] = useTransition();
const [error, setError] = useState(false);
const form = useForm({
resolver: zodResolver(MySchema),
defaultValues: { name: '' },
mode: 'onChange',
});
const onSubmit = (data: z.infer<typeof MySchema>) => {
setError(false);
startTransition(async () => {
try {
await myAction(data);
toast.success('Success!');
} catch (e) {
if (!isRedirectError(e)) {
setError(true);
}
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Form {...form}>
<If condition={error}>
<Alert variant="destructive">
<AlertDescription>
<Trans i18nKey="common:errors.generic" />
</AlertDescription>
</Alert>
</If>
<FormField
name="name"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input data-test="name-input" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={pending} data-test="submit-button">
{pending ? 'Saving...' : 'Save'}
</Button>
</Form>
</form>
);
}
```

View 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)`

View 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,
},
);
```

View 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
}
```

View File

@@ -1,76 +0,0 @@
---
description: Personal Accounts context and functionality
globs:
alwaysApply: false
---
# Personal Account Context
This rule provides guidance for working with personal account related components in the application.
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. [require-user-in-server-component.ts](mdc:apps/web/lib/server/require-user-in-server-component.ts)
### 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';
```
This utility only works in paths under `apps/*/app/home/(user)`.
## Guidelines
### Components and Structure
- Personal account components are used in the `/home/(user)` route
- Reusable components should be in `packages/features/accounts/src/components`
- Settings-related components should be in `packages/features/accounts/src/components/personal-account-settings`
### State Management
- Use the `UserWorkspaceContext` to access user workspace data
- Personal account data can be fetched using `usePersonalAccountData` hook
- Mutations should use React Query's `useMutation` hooks
### Authentication Flow
- User authentication status is available via `useUser` hook
- Account deletion requires OTP verification
- Password updates may require reauthentication
### Feature Flags
- Personal account features are controlled via `featureFlagsConfig` [feature-flags.config.ts](mdc:apps/web/config/feature-flags.config.ts)
- Key flags:
- `enableAccountDeletion`
- `enablePasswordUpdate`
- `enablePersonalAccountBilling`
- `enableNotifications`
## Personal Account API
The API for the personal account is [api.ts](mdc:packages/features/accounts/src/server/api.ts)
A class that provides methods for interacting with account-related data in the database. Initializes a new instance of the `AccountsApi` class with a Supabase client.
### AccountsApi
```typescript
constructor(client: SupabaseClient<Database>)
```
### Methods
- `getAccount(id: string)` - Get account by ID
- `getAccountWorkspace()` - Get current user's account workspace
- `loadUserAccounts()` - Get all accounts for current user
- `getSubscription(accountId: string)` - Get account subscription
- `getOrder(accountId: string)` - Get account order
- `getCustomerId(accountId: string)` - Get account customer ID
## Database
When applying Database rules [database.mdc](mdc:.cursor/rules/database.mdc) must ensure the authenticated user matches the account ID of the entity
```sql
account_id = (select auth.uid())
```

View File

@@ -1,90 +0,0 @@
---
description: Fetch data from the Database using the Supabase Clients
globs: apps/**,packages/**
alwaysApply: false
---
# Data Fetching
## General Data Flow
- 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 [server-client.ts](mdc:packages/supabase/src/clients/server-client.ts)
- 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` [server-admin-client.ts](mdc:packages/supabase/src/clients/server-admin-client.ts)
## 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. For example a good usage is [roles-data-provider.tsx](mdc:packages/features/team-accounts/src/components/members/roles-data-provider.tsx) as shown in [update-member-role-dialog.tsx](mdc:packages/features/team-accounts/src/components/members/update-member-role-dialog.tsx)
## Error Handling
- Logging using the `@kit/shared/logger` package [logger.ts](mdc:packages/shared/src/logger/logger.ts)
- 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
```tsx
'use server';
import { getLogger } from '@kit/shared/logger';
export async function myServerAction() {
const logger = await getLogger();
logger.info('Request started...');
try {
// your code here
await someAsyncFunction();
logger.info('Request succeeded...');
} catch (error) {
logger.error('Request failed...');
// handle error
}
return {
success: true,
};
}
```

View File

@@ -1,294 +0,0 @@
---
description: Detailed Database Schema and Architecture
globs:
alwaysApply: true
---
# Database Rules
## Database Architecture
- 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.
## Schemas
- The DB schemas are available at `apps/web/supabase/schemas`
- To edit the DB schema, we can either change the schema files, or created new ones
- To create a new schema, create a file at `apps/web/supabase/schemas/<number>-<name>.sql`
## Migrations
- After creating a schema, we can create a migration
- Use the command `pnpm --filter web supabase:db:diff` for creating migrations from schemas
- After generating a migration, reset the database for applying the changes using the command `pnpm run supabase:web:reset`
## Security & RLS
- 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`)
- Consider the required compromises between simplicity, functionality and developer experience. However, never compromise on security, which is paramount and fundamental.
- Use existing helper functions for access control instead of making your own queries, unless unavailable
## Schema Overview
Makerkit uses a Supabase Postgres database with a well-defined schema focused on multi-tenancy through the concepts of accounts (both personal and team) and robust permission systems.
### Database Schema
1. Enums [01-enums.sql](mdc:apps/web/supabase/schemas/01-enums.sql)
2. Config [02-config.sql](mdc:apps/web/supabase/schemas/02-config.sql)
3. Accounts [03-accounts.sql](mdc:apps/web/supabase/schemas/03-accounts.sql)
4. Roles [04-roles.sql](mdc:apps/web/supabase/schemas/04-roles.sql)
5. Memberships [05-memberships.sql](mdc:apps/web/supabase/schemas/05-memberships.sql)
6. Roles Permissions [06-roles-permissions.sql](mdc:apps/web/supabase/schemas/06-roles-permissions.sql)
7. Invitations [07-invitations.sql](mdc:apps/web/supabase/schemas/07-invitations.sql)
8. Billing Customers [08-billing-customers.sql](mdc:apps/web/supabase/schemas/08-billing-customers.sql)
9. Subscriptions [09-subscriptions.sql](mdc:apps/web/supabase/schemas/09-subscriptions.sql)
10. Orders [10-orders.sql](mdc:apps/web/supabase/schemas/10-orders.sql)
11. Notifications [11-notifications.sql](mdc:apps/web/supabase/schemas/11-notifications.sql)
12. One Time Tokens [12-one-time-tokens.sql](mdc:apps/web/supabase/schemas/12-one-time-tokens.sql)
13. Multi Factor Auth [13-mfa.sql](mdc:apps/web/supabase/schemas/13-mfa.sql)
14. Super Admin [14-super-admin.sql](mdc:apps/web/supabase/schemas/14-super-admin.sql)
15. Account Views [15-account-views.sql](mdc:apps/web/supabase/schemas/15-account-views.sql)
16. Storage [16-storage.sql](mdc:apps/web/supabase/schemas/16-storage.sql)
## Database Best Practices
### Inferring Database types
Fetch auto-generated data types using the `@kit/supabase/database` import. Do not write types manually if the shape is the same as the one from the database row.
```tsx
import { Tables } from '@kit/supabase/database';
// public.accounts
type Account = Tables<'accounts'>;
// public.subscriptions
type Subscription = Tables<'subscriptions'>;
// public.notifications
type Notification = Tables<'notifications'>;
// ...
```
### Security
- **Always enable RLS** on new tables unless explicitly instructed otherwise
- **Create proper RLS policies** for all CRUD operations following existing patterns
- **Always associate data with accounts** using a foreign key to ensure proper access control
- **Use explicit schema references** (`public.table_name` not just `table_name`)
- **Private schema**: Place internal functions in the `kit` schema
- **Search Path**: Always set search path to '' when defining functions
- **Security Definer**: Do not use `security definer` functions unless stricly required
### Data Access Patterns
- Use `public.has_role_on_account(account_id, role?)` to check membership
- Use `public.has_permission(user_id, account_id, permission)` for permission checks
- Use `public.is_account_owner(account_id)` to identify account ownership
### SQL Coding Style
- Use explicit transactions for multi-step operations
- Follow existing naming conventions:
- Tables: snake_case, plural nouns (`accounts`, `subscriptions`)
- Functions: snake_case, verb phrases (`create_team_account`, `verify_nonce`)
- Triggers: descriptive action names (`set_slug_from_account_name`)
- Document functions and complex SQL with comments
- Use parameterized queries to prevent SQL injection
### Common Patterns
- **Account Lookup**: Typically by `id` (UUID) or `slug` (for team accounts)
- **Permission Check**: Always verify proper permissions before mutations
- **Timestamp Automation**: Use the `trigger_set_timestamps()` function
- **User Tracking**: Use the `trigger_set_user_tracking()` function
- **Configuration**: Use `is_set(field_name)` to check enabled features
## Best Practices for Database Code
### 1. RLS Policy Management
#### Always Enable RLS
Always enable RLS for your tables unless you have a specific reason not to.
```sql
ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;
```
#### Use Helper Functions to validate permissions and access control
Use the existing structure for policies:
```sql
-- SELECT policy
CREATE POLICY "my_table_read" ON public.my_table FOR SELECT
TO authenticated USING (
account_id = (select auth.uid()) OR
public.has_role_on_account(account_id)
);
-- INSERT/UPDATE/DELETE policies follow similar patterns
```
When using RLS at team-account level, use `public.has_role_on_account(account_id)` for a generic check to understand if a user is part of a team.
When using RLS at user-account level, use `account_id = (select auth.uid())`.
When an entity can belong to both, use both.
When requiring a specific role, use the role parameter `public.has_role_on_account(account_id, 'owner')`
### 2. Account Association
- **Associate Data with Accounts**: Always link data to accounts using a foreign key:
```sql
CREATE TABLE if not exists public.my_data (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID REFERENCES public.accounts(id) ON DELETE CASCADE NOT NULL,
/* other fields */
);
```
### 3. Permission System
- **Use the Permission System**: Leverage the built-in permission system for access control:
```sql
-- Check if a user has a specific permission
SELECT public.has_permission(
auth.uid(),
account_id,
'my.permission'::public.app_permissions
);
```
### 4. Schema Organization
- **Use Schemas Explicitly**: Always use schema prefixes explicitly:
```sql
-- Good
SELECT * FROM public.accounts;
-- Avoid
SELECT * FROM accounts;
```
- **Put Internal Functions in 'kit' Schema**: Use the 'kit' schema for internal helper functions
```sql
CREATE OR REPLACE FUNCTION kit.my_helper_function()
RETURNS void AS $$
-- function body
$$ LANGUAGE plpgsql;
```
### 5. Types and Constraints
- **Use Enums for Constrained Values**: Create and use enum types for values with a fixed set:
```sql
CREATE TYPE public.my_status AS ENUM('active', 'inactive', 'pending');
CREATE TABLE if not exists public.my_table (
status public.my_status NOT NULL DEFAULT 'pending'
);
```
- **Apply Appropriate Constraints**: Use constraints to ensure data integrity:
```sql
CREATE TABLE if not exists public.my_table (
email VARCHAR(255) NOT NULL CHECK (email ~* '^.+@.+\..+$'),
count INTEGER NOT NULL CHECK (count >= 0),
/* other fields */
);
```
### 6. Authentication and User Management
- **Use Supabase Auth**: Leverage auth.users for identity management
- **Handle User Creation**: Use triggers like `kit.setup_new_user` to set up user data after registration
### 7. Function Security
- **Apply Security Definer Carefully**: For functions that need elevated privileges:
```sql
CREATE OR REPLACE FUNCTION public.my_function()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = '' AS $$
-- function body
$$;
```
- **Set Proper Function Permissions**:
```sql
GRANT EXECUTE ON FUNCTION public.my_function() TO authenticated, service_role;
```
### 8. Error Handling and Validation
- **Use Custom Error Messages**: Return meaningful errors:
```sql
IF NOT validation_passed THEN
RAISE EXCEPTION 'Validation failed: %', error_message;
END IF;
```
### 9. Triggers for Automation
- **Use Triggers for Derived Data**: Automate updates to derived fields:
```sql
CREATE TRIGGER update_timestamp
BEFORE UPDATE ON public.my_table
FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamps();
```
### 10. View Structure for Commonly Used Queries
- **Create Views for Complex Joins**: As done with `user_account_workspace`
```sql
CREATE OR REPLACE VIEW public.my_view
WITH (security_invoker = true) AS
SELECT ...
```
You always must use `(security_invoker = true)` for views.
## Key Functions to Know
1. **Account Access**
- `public.has_role_on_account(account_id, account_role)`
- `public.is_account_owner(account_id)`
- `public.is_team_member(account_id, user_id)`
2. **Permissions**
- `public.has_permission(user_id, account_id, permission_name)`
- `public.has_more_elevated_role(target_user_id, target_account_id, role_name)`
3. **Team Management**
- `public.create_team_account(account_name)`
4. **Billing & Subscriptions**
- `public.has_active_subscription(target_account_id)`
5. **One-Time Tokens**
- `public.create_nonce(...)`
- `public.verify_nonce(...)`
- `public.revoke_nonce(...)`
6. **Super Admins**
- `public.is_super_admin()`
7. **MFA**:
- `public.is_aal2()`
- `public.is_mfa_compliant()`
## Configuration Control
- **Use the `config` Table**: The application has a central configuration table
- **Check Features with `public.is_set(field_name)`**:
```sql
-- Check if team accounts are enabled
SELECT public.is_set('enable_team_accounts');
```

View File

@@ -1,151 +0,0 @@
---
description: Writing Forms with Shadcn UI, Server Actions, Zod
globs: apps/**/*.tsx,packages/**/*.tsx
alwaysApply: false
---
# 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.
- Use Server Actions [server-actions.mdc](mdc:.cursor/rules/server-actions.mdc) for server-side code handling
- Use Sonner for writing toasters for UI feedback
- Never add generics to `useForm`, use Zod resolver to infer types instead
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
Server Actions [server-actions.mdc](mdc:.cursor/rules/server-actions.mdc) can help us create endpoints for our forms.
```tsx
'use server';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
import { CreateNoteSchema } from '../schema/create-note.schema';
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,
},
);
```
## Create the Form Component
Then create a client component to handle the form submission:
```tsx
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Textarea } from '@kit/ui/textarea';
import { Input } from '@kit/ui/input';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
import { toast } from '@kit/ui/sonner';
import { useTranslation } from 'react-i18next';
import { CreateNoteSchema } from '../_lib/schema/create-note.schema';
export function CreateNoteForm() {
const [pending, startTransition] = useTransition();
const { t } = useTranslation();
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
defaultValues: {
title: '',
content: '',
},
});
const onSubmit = (data) => {
startTransition(async () => {
await toast.promise(createNoteAction(data), {
loading: t('notes:creatingNote`),
success: t('notes:createNoteSuccess`),
error: t('notes:createNoteError`)
})
});
};
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.

View File

@@ -1,206 +0,0 @@
---
description:
globs: *.tsx
alwaysApply: false
---
# JSX Best Practices
This guide outlines our conventions for writing clean, maintainable JSX in React applications.
## Utility Functions
### Class Name Management
When merging complex classes, always use the `cn` utility from `clsx`/`tailwind-merge`:
```tsx
import { cn } from '@kit/ui/utils';
// Simple usage
<button className={cn('btn', className)}>Submit</button>
// Conditional classes
<div className={cn('base-class', {
'text-lg': isLarge,
'bg-primary': isPrimary,
'opacity-50': isDisabled
})}>
Content
</div>
// Array syntax for dynamic classes
<span className={cn([
'badge',
variant === 'success' && 'badge-success',
variant === 'error' && 'badge-error'
])}>
{label}
</span>
```
Why use `cn`:
- Handles merging tailwind classes correctly
- Automatically removes duplicate classes
- Resolves conflicting classes by keeping the last one
- Provides type-safety with TypeScript
## Common Patterns
### Conditional Rendering with `If`
Prefer the `If` component to complex ternary operators in JSX:
```tsx
import { If } from '@kit/ui/if';
// Basic usage
<If condition={isLoading}>
<Spinner />
</If>
// With fallback
<If condition={isLoading} fallback={<Content />}>
<Spinner />
</If>
// With callback function for condition match
<If condition={user}>
{(userData) => <UserProfile data={userData} />}
</If>
```
Benefits:
- Improves readability compared to ternary operators
- Type-safe with TypeScript
- Reduces nesting and complexity in JSX
### List Rendering
Consistently use these patterns for list rendering:
```tsx
// Empty state handling, avoid ternaries
{items.length > 0 ? (
<ul className="list">
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
) : (
<EmptyState message="No items found" />
)}
// Even better with If component
<If condition={items.length > 0} fallback={
<EmptyState message="No items found" />
}>
<ul className="list">
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</If>
```
### Using Translations
All user-facing text should use the `Trans` component unless specified otherwise:
```tsx
import { Trans } from '@kit/ui/trans';
// Basic usage
<Trans i18nKey="common:welcomeMessage" defaults="Welcome!" />
// With variables
<Trans
i18nKey="user:lastLogin"
values={{ date: formatDate(lastLogin) }}
defaults="Last login: {date}"
/>
// With HTML elements
<Trans
i18nKey="terms:agreement"
components={{
TermsLink: <a href="/terms" className="underline" />,
PrivacyLink: <a href="/privacy" className="underline" />
}}
defaults="I agree to the <TermsLink>Terms</TermsLink> and <PrivacyLink>Privacy Policy</PrivacyLink>."
/>
// Pluralization
<Trans
i18nKey="notifications:count"
count={notifications.length}
defaults="{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
/>
```
Important rules:
- Always provide a `defaults` prop with the English text as fallback
- Ensure the key exists in the appropriate translation file
- Keep HTML elements minimal in translations
## Error and Loading States
Use consistent patterns for handling loading and error states:
```tsx
// Loading state
<If condition={isLoading}>
<div className="flex justify-center p-8">
<Spinner />
</div>
</If>
// Error state that infer the type of the condition. The type of the variable "err" is now inferred
// Always use this pattern when the value of the condition is used within the body
<If condition={error}>
{(err) => (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>
<Trans i18nKey="common:errorTitle" />
</AlertTitle>
<AlertDescription>
{err.message}
</AlertDescription>
</Alert>
)}
</If>
// Empty state
<If condition={items.length === 0}>
<div className="flex flex-col items-center justify-center p-8 text-center">
<EmptyIcon className="h-12 w-12 text-muted-foreground" />
<h3 className="mt-4 text-lg font-medium">
<Trans i18nKey="common:noData" />
</h3>
<p className="text-sm text-muted-foreground">
<Trans i18nKey="common:noDataDescription" />
</p>
</div>
</If>
```
## Testing Attributes
Add consistent data attributes for testing:
```tsx
<button data-test="submit-button">
Submit
</button>
<div data-test="user-profile" data-user-id={user.id}>
{/* User profile content */}
</div>
<form data-test="signup-form">
{/* Form fields */}
</form>
```

View File

@@ -1,57 +0,0 @@
---
description: Server side functions logging
globs:
alwaysApply: false
---
## Logging
Consider logging asynchronous requests using the `@kit/shared/logger` [logger.ts](mdc:packages/shared/src/logger/logger.ts) package in a structured way to provide context to the logs in both server actions and route handlers.
The logger uses the following interface:
```tsx
type LogFn = {
<T extends object>(obj: T, msg?: string, ...args: unknown[]): void;
(obj: unknown, msg?: string, ...args: unknown[]): void;
(msg: string, ...args: unknown[]): void;
};
/**
* @name Logger
* @description Logger interface for logging messages
*/
export interface Logger {
info: LogFn;
error: LogFn;
warn: LogFn;
debug: LogFn;
fatal: LogFn;
}
```
Using the logger:
```tsx
import { getLogger } from '@kit/shared/logger';
async function fetchNotes() {
const logger = await getLogger();
const ctx = {
name: 'notes', // 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, error }, 'Request failed...');
// handle error
} else {
logger.info(ctx, 'Request succeeded...');
// use data
}
}
```

View File

@@ -1,59 +0,0 @@
---
description: The OTP API provides the ability to perform additional checks before executing sensitive operations
globs:
alwaysApply: false
---
The OTP API allows the user to:
1. protect sensitive operations behind an additional layer of verification
2. other security operations such as oAuth2 (storing the "state" parameter with additional metadata)
- API: The OTP API [index.ts](mdc:packages/otp/src/api/index.ts) abstract operations with the Database RPCs.
- The Database schema can be found at [12-one-time-tokens.sql](mdc:apps/web/supabase/schemas/12-one-time-tokens.sql)
## Creating an OTP Token
We can se the [verify-otp-form.tsx](mdc:packages/otp/src/components/verify-otp-form.tsx) for creating a quick form to create tokens server side.
```tsx
import { VerifyOtpForm } from '@kit/otp/components';
function MyVerificationPage(props: {
userEmail: string;
}) {
return (
<VerifyOtpForm
purpose="password-reset"
email={props.userEmail}
onSuccess={(otp) => {
// Handle successful verification
// Use the OTP for verification on the server
}}
CancelButton={
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
}
/>
);
}
```
## Verifying a Token
And here is the server action that verifies the OTP:
```tsx
// Verify the token
const result = await api.verifyToken({
token: submittedToken,
purpose: 'email-verification'
});
if (result.valid) {
// Token is valid, proceed with the operation
const { userId, metadata } = result;
// Handle successful verification
} else {
// Token is invalid or expired
// Handle verification failure
}
```

View File

@@ -1,321 +0,0 @@
---
description: Creating new Pages in the app
globs: apps/**
alwaysApply: false
---
# Creating Pages
# Makerkit Page & Layout Guidelines
## Page Structure Overview
Makerkit uses Next.js App Router architecture with a clear separation of concerns for layouts and pages. The application's structure reflects the multi-tenant approach with specific routing patterns:
```
- app
- home # protected routes
- (user) # user workspace (personal account context)
- [account] # team workspace (team account context)
- (marketing) # marketing pages
- auth # auth pages
```
## Key Components
### Layouts
Layouts in Makerkit provide the structure for various parts of the application:
1. **Root Layout**: The base structure for the entire application
2. **Workspace Layouts**:
- User Workspace Layout (`app/home/(user)/layout.tsx`): For personal account context
- Team Workspace Layout (`app/home/[account]/layout.tsx`): For team account context
Layouts handle:
- Workspace context providers
- Navigation components
- Authentication requirements
- UI structure (sidebar vs header style)
### Pages
Pages represent the actual content for each route and follow a consistent pattern:
1. **Metadata Generation**: Using `generateMetadata()` for SEO and page titles
2. **Content Structure**:
- Page headers with titles and descriptions
- Page body containing the main content
3. **i18n Implementation**: Wrapped with `withI18n` HOC
## Creating a New Page
### 1. Define the Page Structure
Create a new file within the appropriate route folder:
```tsx
// app/home/(user)/my-feature/page.tsx
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
// Import components from the _components folder if needed
import { MyFeatureHeader } from './_components/my-feature-header';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('account:myFeaturePage');
return {
title,
};
};
function MyFeaturePage() {
return (
<>
<MyFeatureHeader
title={<Trans i18nKey={'common:routes.myFeature'} />}
description={<Trans i18nKey={'common:myFeatureDescription'} />}
/>
<PageBody>
{/* Main page content */}
</PageBody>
</>
);
}
export default withI18n(MyFeaturePage);
```
- Authentication is enforced already in the middleware
- Authorization is normally enforced by RLS at the database level
- In the rare case you use the Supabase Admin client, you must enforce both authentication and authorization manually
### 2. Create a Loading State
```tsx
// app/home/(user)/my-feature/loading.tsx
import { GlobalLoader } from '@kit/ui/global-loader';
export default GlobalLoader;
```
### 3. Create a Layout (if needed)
If the feature requires a specific layout, create a layout file:
```tsx
// app/home/(user)/my-feature/layout.tsx
import { use } from 'react';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { Page, PageNavigation } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
// Import components from the _components folder
import { MyFeatureNavigation } from './_components/my-feature-navigation';
function MyFeatureLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
return (
<UserWorkspaceContextProvider value={workspace}>
<Page>
<PageNavigation>
<MyFeatureNavigation workspace={workspace} />
</PageNavigation>
{children}
</Page>
</UserWorkspaceContextProvider>
);
}
export default withI18n(MyFeatureLayout);
```
## Layout Patterns
### 1. User Workspace Layout
For pages in the personal account context, use the user workspace layout pattern:
```tsx
import { use } from 'react';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { Page } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { loadUserWorkspace } from './_lib/server/load-user-workspace';
function MyLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
return (
<UserWorkspaceContextProvider value={workspace}>
<Page>
{/* Navigation components */}
{children}
</Page>
</UserWorkspaceContextProvider>
);
}
export default withI18n(MyLayout);
```
### 2. Team Workspace Layout
For pages in the team account context, use the team workspace layout pattern:
```tsx
import { use } from 'react';
import { TeamAccountWorkspaceContextProvider } from '@kit/team-accounts/components';
import { Page } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { loadTeamWorkspace } from './_lib/server/load-team-workspace';
function TeamLayout({ children, params }: LayoutParams) {
const workspace = use(loadTeamWorkspace(params.account));
return (
<TeamAccountWorkspaceContextProvider value={workspace}>
<Page>
{/* Navigation components */}
{children}
</Page>
</TeamAccountWorkspaceContextProvider>
);
}
export default withI18n(TeamLayout);
```
## UI Components Structure
### Page Components
Break down pages into reusable components:
1. **Page Headers**: Create header components for consistent titling:
```tsx
// _components/my-feature-header.tsx
import { PageHeader } from '@kit/ui/page-header';
export function MyFeatureHeader({
title,
description
}: {
title: React.ReactNode,
description: React.ReactNode
}) {
return (
<PageHeader
title={title}
description={description}
/>
);
}
```
2. **Feature Components**: Create components for feature-specific functionality:
```tsx
// _components/my-feature-component.tsx
'use client';
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
export function MyFeatureComponent() {
const { user } = useUserWorkspace();
return (
<div>
{/* Component content */}
</div>
);
}
```
### Navigation Components
Create navigation components to handle sidebar or header navigation:
```tsx
// _components/my-feature-navigation.tsx
'use client';
import { NavigationMenu } from '@kit/ui/navigation-menu';
export function MyFeatureNavigation({
workspace
}: {
workspace: UserWorkspace
}) {
return (
<NavigationMenu>
{/* Navigation items */}
</NavigationMenu>
);
}
```
## Layout Styles
Makerkit supports different layout styles that can be toggled by the user:
1. **Sidebar Layout**: A vertical sidebar navigation
2. **Header Layout**: A horizontal header navigation
The layout style is stored in cookies and can be accessed server-side:
```tsx
async function getLayoutState() {
const cookieStore = await cookies();
const layoutStyleCookie = cookieStore.get('layout-style');
return {
style: layoutStyleCookie?.value ?? defaultStyle,
// Other layout state properties
};
}
```
## Best Practices
1. **Server vs. Client Components**:
- Use Server Components for data fetching and initial rendering
- Use Client Components ('use client') for interactive elements
2. **Data Loading**:
- Load workspace data in layouts using server functions
- Pass data down to components that need it
- Use React Query for client-side data fetching
3. **Component Organization**:
- Place feature-specific components in a `_components` folder
- Place feature-specific server utilities in a `_lib/server` folder
- Place feature-specific client utilities in a `_lib/client` folder
4. **i18n Support**:
- Always use `withI18n` HOC for pages and layouts
- Use `<Trans>` component for translated text
- Define translation keys in the appropriate namespace in `apps/web/public/locales/<locale>/<namespace>.json`
5. **Metadata**:
- Always include `generateMetadata` for SEO and UX
- Use translations for page titles and descriptions
6. **Loading States**:
- Always provide a loading state for each route
- Use the `GlobalLoader` or custom loading components
7. **Error Handling**:
- Implement error.tsx files for route error boundaries
- Handle data fetching errors gracefully

View File

@@ -1,68 +0,0 @@
---
description: Permissions
globs: apps/**,packages/**
alwaysApply: false
---
# Access Control & Permissions Guidelines
This rule provides guidance for implementing access control, permissions, and subscription-related functionality in the application.
## Role-Based Access Control
### Account Roles
- Roles are defined in the `roles` table with hierarchy levels (lower number = higher privilege)
- Default roles include `owner` (hierarchy_level=1) and `member` with specific permissions
- Primary account owner has special privileges that cannot be revoked
- Role hierarchy controls what actions users can perform on other members
### Role Permissions
- Permissions are stored in `role_permissions` table mapping roles to specific permissions
- Core permissions:
- `roles.manage`: Manage roles of users with lower hierarchy
- `billing.manage`: Access/update billing information
- `settings.manage`: Update account settings
- `members.manage`: Add/remove members
- `invites.manage`: Create/update/delete invitations
### Permission Checking
- Use `has_permission(user_id, account_id, permission_name)` to check specific permissions
- Use `can_action_account_member(target_team_account_id, target_user_id)` to verify if a user can act on another
- Use `is_account_owner(account_id)` to check if user is primary owner
- Primary owners can perform any action regardless of explicit permissions
## Team Account Access
### Team Membership
- Use `has_role_on_account(account_id, account_role)` to check if user is a member with specific role
- Use `is_team_member(account_id, user_id)` to check if a specific user is a member
- Use the authenticated user's `TeamAccountWorkspaceContext` to access current permissions array
### Invitations
- Only users with `invites.manage` permission can create/manage invitations
- Users can only invite others with the same or lower role hierarchy than they have
- Invitations have expiry dates (default: 7 days)
- Accept invitations using `accept_invitation` function with token
## Subscription Access
### Subscription Status Checking
- Check active subscriptions with `has_active_subscription(account_id)`
- Active status includes both `active` and `trialing` subscriptions
- Guard premium features with subscription checks in both frontend and backend
### Billing Access
- Only users with `billing.manage` permission can access billing functions
- All billing operations should be guarded with permission checks
- Per-seat billing automatically updates when members are added/removed
## Row Level Security
### Table RLS
- Most tables have RLS policies restricting access based on team membership
- Personal account data is only accessible by the account owner
- Team account data is accessible by all team members based on their roles
### Actions on Members
- Higher roles can update/remove lower roles but not equal or higher roles
- Primary owner cannot be removed from their account
- Ownership transfer requires OTP verification and is limited to primary owners

View File

@@ -1,236 +0,0 @@
---
description: Detailed Project Structure of the app
globs: apps/**
alwaysApply: false
---
# Project Structure
```
apps/web/app/ # Root directory (apps/web/app)
├── (marketing)/ # Marketing pages group
│ ├── _components/ # Shared components for marketing routes
│ │ ├── site-footer.tsx
│ │ ├── site-header.tsx
│ │ ├── site-navigation.tsx
│ │ └── site-page-header.tsx
│ │
│ ├── (legal)/ # Legal pages subgroup
│ │ ├── cookie-policy/
│ │ │ └── page.tsx
│ │ ├── privacy-policy/
│ │ │ └── page.tsx
│ │ └── terms-of-service/
│ │ └── page.tsx
│ │
│ ├── blog/ # Blog section
│ │ ├── _components/ # Blog-specific components
│ │ │ ├── blog-pagination.tsx
│ │ │ ├── post-header.tsx
│ │ │ └── post-preview.tsx
│ │ ├── [slug]/ # Dynamic route for blog posts
│ │ │ └── page.tsx
│ │ └── page.tsx # Blog listing page
│ │
│ ├── contact/ # Contact page
│ │ ├── _components/
│ │ │ └── contact-form.tsx
│ │ ├── _lib/ # Contact page utilities
│ │ │ ├── contact-email.schema.ts
│ │ │ └── server/
│ │ │ └── server-actions.ts
│ │ └── page.tsx
│ │
│ ├── docs/ # Documentation pages
│ │ ├── _components/
│ │ ├── _lib/
│ │ │ ├── server/
│ │ │ │ └── docs.loader.ts
│ │ │ └── utils.ts
│ │ ├── [slug]/
│ │ │ └── page.tsx
│ │ ├── layout.tsx # Layout specific to docs section
│ │ └── page.tsx
│ │
│ ├── faq/
│ │ └── page.tsx
│ │
│ ├── pricing/
│ │ └── page.tsx
│ │
│ ├── layout.tsx # Layout for all marketing pages
│ ├── loading.tsx # Loading state for marketing pages
│ └── page.tsx # Home/landing page
├── (auth)/ # Authentication pages group
│ ├── callback/ # Auth callback routes
│ │ ├── error/
│ │ │ └── page.tsx
│ │ └── route.ts # API route handler for auth callback
│ │
│ ├── confirm/
│ │ └── route.ts
│ │
│ ├── password-reset/
│ │ └── page.tsx
│ │
│ ├── sign-in/
│ │ └── page.tsx
│ │
│ ├── sign-up/
│ │ └── page.tsx
│ │
│ ├── verify/
│ │ └── page.tsx
│ │
│ ├── layout.tsx # Layout for auth pages
│ └── loading.tsx # Loading state for auth pages
├── admin/ # Admin section
│ ├── _components/
│ │ ├── admin-sidebar.tsx
│ │ └── mobile-navigation.tsx
│ │
│ ├── accounts/
│ │ ├── [id]/
│ │ │ └── page.tsx
│ │ └── page.tsx
│ │
│ ├── layout.tsx
│ ├── loading.tsx
│ └── page.tsx
├── api/ # API routes
│ ├── billing/
│ │ └── webhook/
│ │ └── route.ts
│ │
│ └── db/
│ └── webhook/
│ └── route.ts
├── home/ # User dashboard area
│ ├── (user)/ # Personal user routes
│ │ ├── _components/ # User dashboard components
│ │ │ ├── home-account-selector.tsx
│ │ │ └── home-sidebar.tsx
│ │ │
│ │ ├── _lib/ # User dashboard utilities
│ │ │ └── server/
│ │ │ └── load-user-workspace.ts
│ │ │
│ │ ├── billing/ # Personal account billing
│ │ │ ├── _components/
│ │ │ ├── _lib/
│ │ │ │ ├── schema/
│ │ │ │ │ └── personal-account-checkout.schema.ts
│ │ │ │ └── server/
│ │ │ │ ├── personal-account-billing-page.loader.ts
│ │ │ │ ├── server-actions.ts
│ │ │ │ └── user-billing.service.ts
│ │ │ │
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── return/
│ │ │ └── page.tsx
│ │ │
│ │ ├── settings/
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ │
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │
│ ├── [account]/ # Team account routes (dynamic)
│ │ ├── _components/ # Team account components
│ │ │ ├── dashboard-demo.tsx
│ │ │ ├── team-account-accounts-selector.tsx
│ │ │ └── team-account-layout-sidebar.tsx
│ │ │
│ │ ├── _lib/ # Team account utilities
│ │ │ └── server/
│ │ │ ├── team-account-billing-page.loader.ts
│ │ │ └── team-account-workspace.loader.ts
│ │ │
│ │ ├── billing/ # Team billing section
│ │ │ ├── _components/
│ │ │ ├── _lib/
│ │ │ │ ├── schema/
│ │ │ │ │ └── team-billing.schema.ts
│ │ │ │ └── server/
│ │ │ │ ├── server-actions.ts
│ │ │ │ └── team-billing.service.ts
│ │ │ │
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── return/
│ │ │ └── page.tsx
│ │ │
│ │ ├── members/ # Team members management
│ │ │ ├── _lib/
│ │ │ │ └── server/
│ │ │ │ └── members-page.loader.ts
│ │ │ └── page.tsx
│ │ │
│ │ ├── settings/
│ │ │ └── page.tsx
│ │ │
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │
│ └── loading.tsx
├── join/ # Team join page
│ └── page.tsx
├── update-password/
│ └── page.tsx
├── error.tsx # Global error page
├── global-error.tsx # Global error component
├── layout.tsx # Root layout
├── not-found.tsx # 404 page
├── robots.ts # Robots.txt config
├── sitemap.xml/ # Sitemap generation
│ └── route.ts
└── version/ # Version info endpoint
└── route.ts
```
## Key Organization Patterns
1. **Route Groups**
- `(marketing)` - Groups all marketing/public pages
- `(auth)` - Groups all authentication related pages
- `(user)` - Groups all personal user dashboard pages
2. **Component Organization**
- `_components/` - Route-specific components
- Global components are in the root `/components` directory (not shown)
3. **Utilities & Data**
- `_lib/` - Route-specific utilities, types, and helpers
- `_lib/server/` - Server-side utilities including data loaders
- `/lib/` - Global utilities (not shown)
4. **Data Fetching**
- Use of React's `cache()` function for request deduplication
5. **Server Actions**
- `server-actions.ts` - Server-side actions for mutating data
- Follows 'use server' directive pattern
6. **Special Files**
- `layout.tsx` - Define layouts for routes
- `loading.tsx` - Loading UI for routes
- `error.tsx` - Error handling for routes
- `page.tsx` - Page component for routes
- `route.ts` - API route handlers
7. **Dynamic Routes**
- `[account]` - Dynamic route for team accounts. The [account] property is the account slug in the table `public.accounts`.
- `[slug]` - Dynamic route for blog posts and documentation

View File

@@ -1,224 +0,0 @@
---
description:
globs: *.tsx
alwaysApply: false
---
# React
## Core Principles
- **Component-Driven Development**: Build applications as a composition of isolated, reusable components
- **One-Way Data Flow**: Follow React's unidirectional data flow pattern
- **Single Responsibility**: Each component should have a clear, singular purpose
- **TypeScript First**: Use TypeScript for type safety and better developer experience
- **Internationalization (i18n) By Default**: All user-facing text should be translatable
## React Components
### Component Structure
- Always use functional components with TypeScript
- Name components using PascalCase (e.g., `UserProfile`)
- Use named exports for components, not default exports
- Split components by responsibility and avoid "god components"
- Name files to match their component name (e.g., `user-profile.tsx`)
### Props
- Always type props using TypeScript interfaces or type aliases
- Use discriminated unions for complex prop types with conditional rendering
- Destructure props at the start of component functions
- Use prop spreading cautiously and only when appropriate
- Provide default props for optional parameters when it makes sense
```typescript
type ButtonProps = {
variant: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
};
function Button({
variant,
size = 'md',
children,
disabled = false,
onClick
}: ButtonProps) {
// Component implementation
}
```
### State Management
- Keep state as local as possible
- Lift state up when multiple components need access
- Use Context sparingly and only for truly global state
- Prefer the "Container/Presenter" pattern when separating data and UI
```typescript
// Container component (manages data)
function UserProfileContainer() {
const userData = useUserData();
if (userData.isLoading) {
return <LoadingSpinner />;
}
if (userData.error) {
return <ErrorMessage error={userData.error} />;
}
return <UserProfilePresenter data={userData.data} />;
}
// Presenter component (renders UI)
function UserProfilePresenter({ data }: { data: UserData }) {
return (
<div>
<h1>{data.name}</h1>
{/* Rest of the UI */}
</div>
);
}
```
### Hooks
- Follow the Rules of Hooks (only call hooks at the top level, only call them from React functions)
- Create custom hooks for reusable logic
- Keep custom hooks focused on a single concern
- Name custom hooks with a 'use' prefix (e.g., `useUserProfile`)
- Extract complex effect logic into separate functions
- Always provide a complete dependencies array to `useEffect`
### Performance Optimization
- Apply `useMemo` for expensive calculations
- Use `useCallback` for functions passed as props to child components
- Split code using dynamic imports and `React.lazy()`
```typescript
const MemoizedComponent = React.memo(function Component(props: Props) {
// Component implementation
});
// For expensive calculations
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// For callback functions passed as props
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
```
### Internationalization (i18n)
- Always use the `Trans` component for text rendering (no hardcoded strings)
- Ensure all i18n keys are available in locale files
- Use namespaces to organize translations logically
- Include interpolation variables in translation keys
- Test UI with different languages, especially those with longer text
```typescript
// Correct
<Trans i18nKey="user:profile.welcomeMessage" values={{ name: user.name }} />
// Incorrect
<p>Welcome, {user.name}!</p>
```
## Server Components
### Fundamentals
- Server Components render React server-side and never run on the client
- Use Server Components as the default choice, especially for data fetching
- No use of hooks, browser APIs, or event handlers in Server Components
- No use of `useState`, `useEffect`, or any other React hooks
- Server Components can render Client Components but not vice versa
### Data Fetching
- Fetch data directly using async/await in Server Components
- Use Suspense boundaries around data-fetching components
- Apply security checks before fetching sensitive data
- Never pass sensitive data (API keys, tokens) to Client Components
- Use React's `cache()` function for caching data requests
### Error Handling
- Implement error boundaries at appropriate levels
- Use the Next.js `error.tsx` file for route-level error handling
- Create fallback UI for when data fetching fails
- Log server errors appropriately without exposing details to clients
### Streaming and Suspense
- Use React Suspense for progressive loading experiences if specified
- Implement streaming rendering for large or complex pages
- Structure components to enable meaningful loading states
- Prioritize above-the-fold content when using streaming
## Client Components
### Fundamentals
- Add the `'use client'` directive at the top of files for Client Components
- Keep Client Components focused on interactivity and browser APIs
- Use hooks appropriately following the Rules of Hooks
- Implement controlled components for form elements
- Handle all browser events in Client Components
### Data Fetching
- Use React Query (TanStack Query) for data fetching in Client Components
- Create custom hooks for data fetching logic (e.g., `useUserData`)
- Always handle loading, success, and error states
### Form Handling
- Use libraries like React Hook Form for complex forms
- Implement proper validation with libraries like Zod
- Create reusable form components
- Handle form submissions with loading and error states
- Use controlled components for form inputs
### Error Handling
- Implement error boundaries to catch and handle component errors if using client components
- Always handle network request errors
- Provide user-friendly error messages
- Log errors appropriately
- Implement retry mechanisms where applicable
```typescript
'use client';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export function UserProfileWithErrorHandling() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset application state here if needed
}}
>
<UserProfile userId="123" />
</ErrorBoundary>
);
}
```

View File

@@ -1,50 +0,0 @@
---
description: Next.js API Endpoints/Route Handlers
globs: apps/**/route.{ts,tsx}
alwaysApply: false
---
# 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. [index.ts](mdc:packages/next/src/routes/index.ts)
```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,
},
);
```

View File

@@ -1,152 +0,0 @@
---
description: Security guidelines for writing secure code
globs:
alwaysApply: false
---
# Next.js-Specific Security
### Client Component Data Passing
- **Never pass sensitive data** to Client Components
- **Never pass unsanitized data** to Client Components (raw cookies, client-provided data)
### Server Components Security
- **Always sanitize user input** before using in Server Components
- **Validate cookies and headers** in Server Components
### Environment Variables
- **Use `import 'server-only'`** for code that should only be run on the server side
- **Never expose server-only env vars** to the client
- **Never pass environment variables as props to client components** unless they're suffixed with `NEXT_PUBLIC_`
- **Never use `NEXT_PUBLIC_` prefix** for sensitive data (ex. API keys, secrets)
- **Use `NEXT_PUBLIC_` prefix** only for client-safe variables
### Client Hydration Protection
- **Never expose sensitive data** in initial HTML
## Authentication & Authorization
### Row Level Security (RLS)
- **Always enable RLS** on all tables unless explicitly specified otherwise [database.mdc](mdc:.cursor/rules/database.mdc)
### Super Admin Protected Routes
Always perform extra checks when writing Super Admin code [super-admin.mdc](mdc:.cursor/rules/super-admin.mdc)
## Server Actions & API Routes
### Server Actions
- Always use `enhanceAction` wrapper for consistent security [server-actions.mdc](mdc:.cursor/rules/server-actions.mdc)
- Always use 'use server' directive at the top of the file to safely bundle server-side code
- Validate input with Zod schemas
- Implement authentication checks:
```typescript
'use server';
import { enhanceAction } from '@kit/next/actions';
import { MyActionSchema } from '../schema';
export const secureAction = enhanceAction(
async function(data, user) {
// Additional permission checks
const hasPermission = await checkUserPermission(user.id, data.accountId, 'action.perform');
if (!hasPermission) throw new Error('Insufficient permissions');
// Validated data available
return processAction(data);
},
{
auth: true,
schema: MyActionSchema
}
);
```
### API Routes
- Use `enhanceRouteHandler` for consistent security [route-handlers.mdc](mdc:.cursor/rules/route-handlers.mdc)
- Implement authentication and authorization checks:
```typescript
import { enhanceRouteHandler } from '@kit/next/routes';
import { RouteSchema } from '../schema';
export const POST = enhanceRouteHandler(
async function({ body, user, request }) {
// Additional authorization checks
const canAccess = await canAccessResource(user.id, body.resourceId);
if (!canAccess) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
// Safe to process with validated body
return NextResponse.json({ success: true });
},
{
auth: true,
schema: RouteSchema
}
);
```
## Client Components Security
### Context Awareness
- Use appropriate workspace contexts for access control:
- `useUserWorkspace()` in personal account pages
- `useTeamAccountWorkspace()` in team account pages
- Check permissions before rendering sensitive UI elements:
```typescript
function SecureComponent() {
const { account, user } = useTeamAccountWorkspace();
const canEdit = account.permissions.includes('entity.update');
if (!canEdit) return null;
return <EditComponent />;
}
```
### Data Fetching
- Use React Query with proper error handling
- Never trust client-side permission checks alone
## One-Time Tokens
Consider using OTP tokens when implementing highly destructive operations like deleting an entity that would otherwise require a full backup: [otp.mdc](mdc:.cursor/rules/otp.mdc)
## Critical Data Protection
### Personal Information
- Never log or expose sensitive user data (api keys, passwords, secrets, etc.)
- Use proper session management
## Error Handling
- Never expose internal errors to clients
- Log errors securely with appropriate context
- Return generic error messages to users
```typescript
try {
await sensitiveOperation();
} catch (error) {
logger.error({ error, context }, 'Operation failed');
return { error: 'Unable to complete operation' };
}
```
## Database Security
- Avoid dynamic SQL generation
- Use SECURITY DEFINER functions sparingly and carefully, warn user if you do so
- Implement proper foreign key constraints
- Use appropriate data types with constraints

View File

@@ -1,37 +0,0 @@
---
description: Writing Server Actions for mutating data
globs: apps/**","packages/**
alwaysApply: false
---
# 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 [index.ts](mdc:packages/next/src/actions/index.ts)
- Always use the 'use server' directive at the top of the file
- Place the Zod schema in a separate file so it can be reused with `react-hook-form`
```tsx
'use server';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
import { EntitySchema } from '../entity.schema.ts`;
export const myServerAction = enhanceAction(
async function (data, user) {
// 1. "data" is already a valid EntitySchema and it's safe to use
// 2. "user" is the authenticated user
// ... your code here
return {
success: true,
};
},
{
auth: true,
schema: EntitySchema,
},
);
```

View File

@@ -1,210 +0,0 @@
---
description: Super Admin functionalities
globs: apps/*/app/admin/**,packages/features/admin/**
alwaysApply: false
---
## Super Admin
1. Page Authentication:
- All pages in the admin section must be wrapped with the `AdminGuard` HOC
- This ensures only users with the 'super-admin' role and MFA enabled can access these pages
- Example: `export default AdminGuard(AdminPageComponent);`
2. Server Actions:
- Use the `adminAction` wrapper for all server actions in the admin section
- This checks if the current user is a super admin before executing the action
- Example:
```typescript
export const yourAdminAction = adminAction(
enhanceAction(
async (data) => {
// Action implementation
},
{
schema: YourActionSchema,
}
)
);
```
3. Authorization Functions:
- Import and use `isSuperAdmin` from '@kit/admin' to check if the current user is a super admin [is-super-admin.ts](mdc:packages/features/admin/src/lib/server/utils/is-super-admin.ts)
- This function returns a boolean indicating whether the user has the super-admin role and MFA enabled
- Example:
```typescript
const isAdmin = await isSuperAdmin(getSupabaseServerClient());
if (!isAdmin) {
notFound(); // or redirect/throw error
}
```
4. Schema Validation:
- Define Zod schemas for all admin actions in the 'schema' directory
- Follow the pattern in [admin-actions.schema.ts](mdc:packages/features/admin/src/lib/server/schema/admin-actions.schema.ts)
- Include appropriate validation for all fields
5. Data Fetching
- Do not use `ServerDataLoader` unless the query is very simple
- Use the authed Supabase Server Client such as [admin-dashboard.loader.ts](mdc:packages/features/admin/src/lib/server/loaders/admin-dashboard.loader.ts)
The Super Admin section requires strict access control as it provides elevated privileges. Always ensure the current user cannot perform destructive actions on their own account and properly validate input data."
## Writing Pages in the Admin Sections
1. Basic Page Structure:
```typescript
import { AdminGuard } from '@kit/admin/components/admin-guard';
import { PageBody, PageHeader } from '@kit/ui/page';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
// Optional metadata export
export const metadata = {
title: `Page Title`,
};
async function YourAdminPage() {
// Load data using cached loaders
const data = await loadYourAdminData();
return (
<>
<PageHeader description={<AppBreadcrumbs />} />
<PageBody>
{/* Page content */}
</PageBody>
</>
);
}
// IMPORTANT: Always wrap with AdminGuard
export default AdminGuard(YourAdminPage);
```
2. Data Loading:
- Create a cached loader function in a server directory
- Use the Supabase client for database operations
- Example:
```typescript
// in _lib/server/loaders/your-loader.ts
import { cache } from 'react';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export const loadYourAdminData = cache(async () => {
const client = getSupabaseServerClient();
const { data, error } = await client.from('your_table').select('*');
if (error) throw error;
return data;
});
```
3. Dynamic Routes:
- For pages that need parameters (like `[id]`), handle them appropriately
- For example:
```typescript
interface Params {
params: Promise<{
id: string;
}>;
}
async function AdminDetailPage({ params }: Params) {
const { id } = await params;
const item = await loadItemById(id);
// ...
}
```
4. Updating Sidebar navigation at [admin-sidebar.tsx](mdc:apps/web/app/admin/_components/admin-sidebar.tsx) to include new pages
### Security Considerations:
- Validate that the target is not the current super admin
- Implement confirmation steps for destructive actions
- Never expose sensitive error details to the client
### Services
1. Basic Service Structure:
```typescript
import 'server-only';
import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
import { getLogger } from '@kit/shared/logger';
export function createYourAdminService(client: SupabaseClient<Database>) {
return new YourAdminService(client);
}
class YourAdminService {
constructor(private readonly client: SupabaseClient<Database>) {}
async performAction(params: YourActionParams) {
const logger = await getLogger();
const ctx = { name: 'admin.yourService', ...params };
logger.info(ctx, 'Starting admin action');
// Perform the action
const { data, error } = await this.client
.from('your_table')
.update({ some_field: params.value })
.eq('id', params.id);
if (error) {
logger.error({ ...ctx, error }, 'Admin action failed');
throw error;
}
logger.info(ctx, 'Admin action completed successfully');
return data;
}
}
```
2. Important Patterns:
- Mark files with 'server-only' directive
- Use factory functions to create service instances
- Use class-based services with typed parameters
- Properly type the Supabase client with the Database type
- Use structured logging with context
- Handle errors consistently
3. Security Checks:
- Implement methods to verify the current user is not taking action on their own account
- Example:
```typescript
private async assertUserIsNotCurrentSuperAdmin(targetId: string) {
const { data } = await this.client.auth.getUser();
const currentUserId = data.user?.id;
if (!currentUserId) {
throw new Error(`Error fetching user`);
}
if (currentUserId === targetId) {
throw new Error(
`You cannot perform a destructive action on your own account as a Super Admin`
);
}
}
```
4. Data Access:
- Use the appropriate Supabase client (admin or regular)
- For admin-only operations, use the admin client
- For regular operations, use the standard client
- Example:
```typescript
constructor(
private readonly client: SupabaseClient<Database>,
private readonly adminClient?: SupabaseClient<Database>
) {}
```
5. Error Handling:
- Use structured error handling
- Include appropriate context in error logs
- Return typed error responses
Services should be focused on specific domains and follow the principle of single responsibility.

View File

@@ -1,79 +0,0 @@
---
description: Team Accounts context and functionality
globs: apps/*/app/home/[account],packages/features/team-accounts/**
alwaysApply: false
---
## Team Accounts
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 `useTeamAccountWorkspace` hook [use-team-account-workspace.ts](mdc:packages/features/team-accounts/src/hooks/use-team-account-workspace.ts)
```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.
## Guidelines
### State Management
- Use the `TeamAccountWorkspaceContext` to access account workspace data
- Team account data can be accessed using `useTeamAccountWorkspace` hook
- Server-side loading done with `loadTeamWorkspace` in `team-account-workspace.loader.ts` [team-account-workspace.loader.ts](mdc:apps/web/app/home/[account]/_lib/server/team-account-workspace.loader.ts)
### Account Management Features
- Role-based permissions control what users can do within a team
- Team members can be invited, roles can be updated, and members can be removed
- Primary account owner has special privileges (transfer ownership, delete team)
- Account deletion requires OTP verification
### Billing Integration
- Team account billing uses [team-billing.service.ts](mdc:apps/web/app/home/[account]/billing/_lib/server/team-billing.service.ts)
- Per-seat billing handled by [account-per-seat-billing.service.ts](mdc:packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts)
## API
The API for the personal account is [api.ts](mdc:packages/features/team-accounts/src/server/api.ts)
### Factory
```typescript
createAccountsApi(client: SupabaseClient<Database>): AccountsApi
```
### TeamAccountsApi
```typescript
constructor(client: SupabaseClient<Database>)
```
### Methods
- `getTeamAccount(slug: string)` - Get team by slug
- `getTeamAccountById(accountId: string)` - Get team by ID
- `getSubscription(accountId: string)` - Get team subscription
- `getOrder(accountId: string)` - Get team order
- `getAccountWorkspace(slug: string)` - Get team workspace
- `hasPermission({accountId, userId, permission})` - Check user permission
- `getMembersCount(accountId: string)` - Get team member count
- `getCustomerId(accountId: string)` - Get team customer ID
- `getInvitation(adminClient, token)` - Get invitation by token
## Feature Flags
- Key flags at [feature-flags.config.ts](mdc:apps/web/config/feature-flags.config.ts)
- `enableTeamAccountBilling`
- `enableTeamDeletion`
- `enableTeamCreation`
- `enableNotifications`

View File

@@ -1,146 +0,0 @@
---
description: I18n and Translations
globs:
alwaysApply: false
---
# i18n System Guide
This document provides a comprehensive overview of the internationalization (i18n) system in our Next.js application.
## Architecture Overview
The i18n system consists of:
1. **Core i18n Package**: Located in `packages/i18n`, providing the foundation for i18n functionality
2. **Application-specific Implementation**: Located in `apps/web/lib/i18n`, customizing the core functionality
3. **Translation Files**: Located in `apps/web/public/locales/[language]/[namespace].json`
## Usage Guide
### 1. Setting Up a Page or Layout with i18n
Wrap your page or layout component with the `withI18n` HOC:
```typescript
import { withI18n } from '~/lib/i18n/with-i18n';
function HomePage() {
// Your component code
}
export default withI18n(HomePage);
```
### 2. Using Translations in Client Components
Use the `useTranslation` hook from react-i18next:
```tsx
'use client';
import { useTranslation } from 'react-i18next';
export function MyComponent() {
const { t } = useTranslation('common');
return <h1>{t('homeTabLabel')}</h1>;
}
```
### 3. Using Translations with the Trans Component
For complex translations that include HTML or variables:
```tsx
import { Trans } from '@kit/ui/trans';
export function MyComponent() {
return (
<div>
<Trans
i18nKey="teams:inviteAlertBody"
values={{ accountName: 'My Team' }}
/>
</div>
);
}
```
### 4. Adding Language Selection to Your UI
Use the `LanguageSelector` component:
```tsx
import { LanguageSelector } from '@kit/ui/language-selector';
export function SettingsPage() {
return (
<div>
<h2>Language Settings</h2>
<LanguageSelector />
</div>
);
}
```
### 5. Adding New Translations
1. Create or update JSON files in `apps/web/public/locales/[language]/[namespace].json`
2. Follow the existing structure, adding your new keys
For example, in `apps/web/public/locales/en/common.json`:
```json
{
"existingKey": "Existing translation",
"newKey": "New translation text"
}
```
### 6. Adding a New Language
1. Add the language code to the `languages` array in `i18n.settings.ts`
2. Create corresponding translation files in `apps/web/public/locales/[new-language]/`
3. Copy the structure from the English files as a template
### 7. Adding a New Namespace
1. Add the namespace to `defaultI18nNamespaces` in `i18n.settings.ts`
2. Create corresponding translation files for all supported languages
## Advanced Usage
### Dynamic Namespace Loading
When you need translations from namespaces not included in the default set:
```typescript
import { getI18nSettings } from '~/lib/i18n/i18n.settings';
// Load specific namespaces
const settings = getI18nSettings(language, ['specific-namespace']);
```
### Language Priority
The system uses the following priority to determine the language:
1. User-selected language (from cookie)
2. Browser language (if priority is set to 'user')
3. Default language from environment variable
### Common Issues
- **Translation not showing**: Check that you're using the correct namespace
- **Dynamic content not interpolated**: Make sure to use the `values` prop with `Trans` component
## Available Namespaces and Keys
Here's a brief overview of the available namespaces:
- **common**: General UI elements, navigation, errors [common.json](mdc:apps/web/public/locales/en/common.json)
- **auth**: Authentication-related text [auth.json](mdc:apps/web/public/locales/en/auth.json)
- **account**: Account settings and profile [account.json](mdc:apps/web/public/locales/en/account.json)
- **teams**: Team management [teams.json](mdc:apps/web/public/locales/en/teams.json)
- **billing**: Subscription and payment [billing.json](mdc:apps/web/public/locales/en/billing.json)
- **marketing**: Landing pages, blog, etc. [marketing.json](mdc:apps/web/public/locales/en/marketing.json)
When creating a new functionality, it can be useful to add a new namespace.

View File

@@ -1,36 +0,0 @@
---
description:
globs: *.ts,*.tsx
alwaysApply: false
---
# Typescript
- Write clean, clear, well-designed, explicit Typescript
- Make sure types are validated strictly
- Use implicit type inference, unless impossible
- Consider using classes for server-side services, but export a function instead of the class
```tsx
// service.ts
class UserService {
getUser(id: number) {
// ... implementation ...
return { id, name: 'Example User' };
}
}
export function createUserService() {
return new UserService();
}
```
- Follow the Single Responsibility Principle (SRP). Each module/function/class should have one reason to change.
- Favor composition over inheritance.
- Handle errors gracefully using try/catch and appropriate error types.
- Keep functions short and focused.
- Use descriptive names for variables, functions, and classes.
- Avoid unnecessary complexity.
- Avoid using `any` type as much as possible. If necessary, use `unknown`
- Use enums only when appropriate. Consider union types of string literals as an alternative.
- Be aware of performance implications of your code.

View File

@@ -1,111 +0,0 @@
---
description: UI Components API reference and guidelines
globs: **/*.tsx
alwaysApply: false
---
# 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.
## 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.
Makerkit leverages two sets of UI components:
1. **Shadcn UI Components**: Base components from the Shadcn UI library
2. **Makerkit-specific Components**: Custom components built on top of Shadcn UI
## Importing Components
```tsx
// Import Shadcn UI components
import { Button } from '@kit/ui/button';
import { Card } from '@kit/ui/card';
// Import Makerkit-specific components
import { If } from '@kit/ui/if';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';
```
## Core Shadcn UI Components
| Component | Description | Import Path |
| ---------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `Accordion` | Expandable/collapsible content sections | `@kit/ui/accordion` [accordion.tsx](mdc:packages/ui/src/shadcn/accordion.tsx) |
| `AlertDialog` | Modal dialog for important actions | `@kit/ui/alert-dialog` [alert-dialog.tsx](mdc:packages/ui/src/shadcn/alert-dialog.tsx) |
| `Alert` | Status/notification messages | `@kit/ui/alert` [alert.tsx](mdc:packages/ui/src/shadcn/alert.tsx) |
| `Avatar` | User profile images with fallback | `@kit/ui/avatar` [avatar.tsx](mdc:packages/ui/src/shadcn/avatar.tsx) |
| `Badge` | Small status indicators | `@kit/ui/badge` [badge.tsx](mdc:packages/ui/src/shadcn/badge.tsx) |
| `Breadcrumb` | Navigation path indicators | `@kit/ui/breadcrumb` [breadcrumb.tsx](mdc:packages/ui/src/shadcn/breadcrumb.tsx) |
| `Button` | Clickable action elements | `@kit/ui/button` [button.tsx](mdc:packages/ui/src/shadcn/button.tsx) |
| `Calendar` | Date picker and date display | `@kit/ui/calendar` [calendar.tsx](mdc:packages/ui/src/shadcn/calendar.tsx) |
| `Card` | Container for grouped content | `@kit/ui/card` [card.tsx](mdc:packages/ui/src/shadcn/card.tsx) |
| `Checkbox` | Selection input | `@kit/ui/checkbox` [checkbox.tsx](mdc:packages/ui/src/shadcn/checkbox.tsx) |
| `Command` | Command palette interface | `@kit/ui/command` [command.tsx](mdc:packages/ui/src/shadcn/command.tsx) |
| `DataTable` | Table | `@kit/ui/data-table` [data-table.tsx](mdc:packages/ui/src/shadcn/data-table.tsx) |
| `Dialog` | Modal window for focused interactions | `@kit/ui/dialog` [dialog.tsx](mdc:packages/ui/src/shadcn/dialog.tsx) |
| `DropdownMenu` | Menu triggered by a button | `@kit/ui/dropdown-menu` [dropdown-menu.tsx](mdc:packages/ui/src/shadcn/dropdown-menu.tsx) |
| `Form` | Form components with validation | `@kit/ui/form` [form.tsx](mdc:packages/ui/src/shadcn/form.tsx) |
| `Input` | Text input field | `@kit/ui/input` [input.tsx](mdc:packages/ui/src/shadcn/input.tsx) |
| `Input OTP` | OTP Text input field | `@kit/ui/input-otp` [input-otp.tsx](mdc:packages/ui/src/shadcn/input-otp.tsx) |
| `Label` | Text label for form elements | `@kit/ui/label` [label.tsx](mdc:packages/ui/src/shadcn/label.tsx) |
| `NavigationMenu` | Hierarchical navigation component | `@kit/ui/navigation-menu` [navigation-menu.tsx](mdc:packages/ui/src/shadcn/navigation-menu.tsx) |
| `Popover` | Floating content triggered by interaction | `@kit/ui/popover` [popover.tsx](mdc:packages/ui/src/shadcn/popover.tsx) |
| `RadioGroup` | Radio button selection group | `@kit/ui/radio-group` [radio-group.tsx](mdc:packages/ui/src/shadcn/radio-group.tsx) |
| `ScrollArea` | Customizable scrollable area | `@kit/ui/scroll-area` [scroll-area.tsx](mdc:packages/ui/src/shadcn/scroll-area.tsx) |
| `Select` | Dropdown selection menu | `@kit/ui/select` [select.tsx](mdc:packages/ui/src/shadcn/select.tsx) |
| `Separator` | Visual divider between content | `@kit/ui/separator` [separator.tsx](mdc:packages/ui/src/shadcn/separator.tsx) |
| `Sheet` | Sliding panel from screen edge | `@kit/ui/sheet` [sheet.tsx](mdc:packages/ui/src/shadcn/sheet.tsx) |
| `Sidebar` | Advanced sidebar navigation | `@kit/ui/shadcn-sidebar` [sidebar.tsx](mdc:packages/ui/src/shadcn/sidebar.tsx) |
| `Skeleton` | Loading placeholder | `@kit/ui/skeleton` [skeleton.tsx](mdc:packages/ui/src/shadcn/skeleton.tsx) |
| `Switch` | Toggle control | `@kit/ui/switch` [switch.tsx](mdc:packages/ui/src/shadcn/switch.tsx) |
| `Toast` | Toaster | `@kit/ui/sonner` [sonner.tsx](mdc:packages/ui/src/shadcn/sonner.tsx) |
| `Tabs` | Tab-based navigation | `@kit/ui/tabs` [tabs.tsx](mdc:packages/ui/src/shadcn/tabs.tsx) |
| `Textarea` | Multi-line text input | `@kit/ui/textarea` [textarea.tsx](mdc:packages/ui/src/shadcn/textarea.tsx) |
| `Tooltip` | Contextual information on hover | `@kit/ui/tooltip` [tooltip.tsx](mdc:packages/ui/src/shadcn/tooltip.tsx) |
## Makerkit-specific Components
| Component | Description | Import Path |
| ---------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------- |
| `If` | Conditional rendering component | `@kit/ui/if` [if.tsx](mdc:packages/ui/src/makerkit/if.tsx) |
| `Trans` | Internationalization text component | `@kit/ui/trans` [trans.tsx](mdc:packages/ui/src/makerkit/trans.tsx) |
| `Page` | Page layout with navigation | `@kit/ui/page` [page.tsx](mdc:packages/ui/src/makerkit/page.tsx) |
| `GlobalLoader` | Full-page loading indicator | `@kit/ui/global-loader` [global-loader.tsx](mdc:packages/ui/src/makerkit/global-loader.tsx) |
| `ImageUploader` | Image upload component | `@kit/ui/image-uploader` [image-uploader.tsx](mdc:packages/ui/src/makerkit/image-uploader.tsx) |
| `ProfileAvatar` | User avatar with fallback | `@kit/ui/profile-avatar` [profile-avatar.tsx](mdc:packages/ui/src/makerkit/profile-avatar.tsx) |
| `DataTable` (Enhanced) | Extended data table with pagination | `@kit/ui/enhanced-data-table` [data-table.tsx](mdc:packages/ui/src/makerkit/data-table.tsx) |
| `Stepper` | Multi-step process indicator | `@kit/ui/stepper` [stepper.tsx](mdc:packages/ui/src/makerkit/stepper.tsx) |
| `CookieBanner` | GDPR-compliant cookie notice | `@kit/ui/cookie-banner` [cookie-banner.tsx](mdc:packages/ui/src/makerkit/cookie-banner.tsx) |
| `CardButton` | Card-styled button | `@kit/ui/card-button` [card-button.tsx](mdc:packages/ui/src/makerkit/card-button.tsx) |
| `MultiStepForm` | Form with multiple steps | `@kit/ui/multi-step-form` [multi-step-form.tsx](mdc:packages/ui/src/makerkit/multi-step-form.tsx) |
| `EmptyState` | Empty data placeholder | `@kit/ui/empty-state` [empty-state.tsx](mdc:packages/ui/src/makerkit/empty-state.tsx) |
| `AppBreadcrumbs` | Application path breadcrumbs | `@kit/ui/app-breadcrumbs` [app-breadcrumbs.tsx](mdc:packages/ui/src/makerkit/app-breadcrumbs.tsx) |
## Marketing Components
Import all marketing components with:
```tsx
import {
GradientText,
// etc.
Hero,
HeroTitle,
} from '@kit/ui/marketing';
```
Key marketing components:
- `Hero` - Hero sections [hero.tsx](mdc:packages/ui/src/makerkit/marketing/hero.tsx)
- `SecondaryHero` [secondary-hero.tsx](mdc:packages/ui/src/makerkit/marketing/secondary-hero.tsx)
- `FeatureCard`, `FeatureGrid` - Feature showcases [feature-card.tsx](mdc:packages/ui/src/makerkit/marketing/feature-card.tsx)
- `Footer` - Page Footer [footer.tsx](mdc:packages/ui/src/makerkit/marketing/footer.tsx)
- `Header` - Page Header [header.tsx](mdc:packages/ui/src/makerkit/marketing/header.tsx)
- `NewsletterSignup` - Email collection [newsletter-signup-container.tsx](mdc:packages/ui/src/makerkit/marketing/newsletter-signup-container.tsx)
- `ComingSoon` - Coming soon page template [coming-soon.tsx](mdc:packages/ui/src/makerkit/marketing/coming-soon.tsx)

3
.gemini/GEMINI.md Normal file
View File

@@ -0,0 +1,3 @@
- Check for the presence of AGENTS.md files in the project workspace
- There may be additional AGENTS.md in sub-folders with additional specific instructions that are related to only that part of the codebase.

9
.mcp.json Normal file
View File

@@ -0,0 +1,9 @@
{
"mcpServers": {
"makerkit": {
"type": "stdio",
"command": "node",
"args": ["packages/mcp-server/build/index.js"]
}
}
}

View File

@@ -1,239 +0,0 @@
# Makerkit Guidelines
## Project Stack
- Framework: Next.js 16 App Router, TypeScript, React, Node.js
- Backend: Supabase with Postgres
- UI: Shadcn UI, Tailwind CSS
- Key libraries: React Hook Form, React Query, Zod, Lucide React
- Focus: Code clarity, Readability, Best practices, Maintainability
## Project 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
/supabase # supabase root
```
## Core Principles
### Data Flow
1. Server Components
- Use Supabase Client directly via `getSupabaseServerClient`
- Handle errors with proper boundaries
- Example:
```tsx
async function ServerComponent() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
if (error) return <ErrorComponent error={error} />;
return <ClientComponent data={data} />;
}
```
2. Client Components
- Use React Query for data fetching
- Implement proper loading states
- Example:
```tsx
function useNotes() {
const { data, isLoading } = useQuery({
queryKey: ['notes'],
queryFn: async () => {
const { data } = await fetch('/api/notes');
return data;
},
});
return { data, isLoading };
}
```
### Server Actions
- Name files as "server-actions.ts" in `_lib/server` folder
- Export with "Action" suffix
- Use `enhanceAction` with proper typing
- Example:
```tsx
export const createNoteAction = enhanceAction(
async function (data, user) {
const client = getSupabaseServerClient();
const { error } = await client
.from('notes')
.insert({ ...data, user_id: user.id });
if (error) throw error;
return { success: true };
},
{
auth: true,
schema: NoteSchema,
},
);
```
### Route Handlers
- Use `enhanceRouteHandler` to wrap route handlers
- Use Route Handlers when data fetching from Client Components
## Database & Security
### RLS Policies
- Strive to create a safe, robust, secure and consistent database schema
- 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.
- Enable RLS by default and propose the required RLS policies
- `public.accounts` are the root tables for the application
- Implement cascading deletes when appropriate
- Ensure strong consistency considering triggers and constraints
- Always use Postgres schemas explicitly (e.g., `public.accounts`)
## Forms Pattern
### 1. Schema Definition
```tsx
// schema/note.schema.ts
import { z } from 'zod';
export const NoteSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
category: z.enum(['work', 'personal']),
});
```
### 2. Form Component
```tsx
'use client';
export function NoteForm() {
const [pending, startTransition] = useTransition();
const form = useForm({
resolver: zodResolver(NoteSchema),
defaultValues: { title: '', content: '', category: 'personal' },
});
const onSubmit = (data: z.infer<typeof NoteSchema>) => {
startTransition(async () => {
try {
await createNoteAction(data);
form.reset();
} catch (error) {
// Handle error
}
});
};
return (
<Form {...form}>
<FormField
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Other fields */}
</Form>
);
}
```
## Error Handling
- Consider logging asynchronous requests in server code using the `@kit/shared/logger`
- Handle promises and async/await gracefully
- Consider the unhappy path and handle errors appropriately
### Structured Logging
```tsx
const ctx = {
name: 'create-note',
userId: user.id,
noteId: note.id,
};
logger.info(ctx, 'Creating new note...');
try {
await createNote();
logger.info(ctx, 'Note created successfully');
} catch (error) {
logger.error(ctx, 'Failed to create note', { error });
throw error;
}
```
## Context Management
In client components, we can use the `useUserWorkspace` hook to access the user's workspace data.
### Personal Account
```tsx
'use client';
function PersonalDashboard() {
const { workspace, user } = useUserWorkspace();
if (!workspace) return null;
return (
<div>
<h1>Welcome, {user.email}</h1>
<SubscriptionStatus status={workspace.subscription_status} />
</div>
);
}
```
### Team Account
In client components, we can use the `useTeamAccountWorkspace` hook to access the team account's workspace data. It only works under the `/home/[account]` route.
```tsx
'use client';
function TeamDashboard() {
const { account, user } = useTeamAccountWorkspace();
return (
<div>
<h1>{account.name}</h1>
<RoleDisplay role={account.role} />
<PermissionsList permissions={account.permissions} />
</div>
);
}
```
## 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.
## Creating Pages
When creating new pages ensure:
- The page is exported using `withI18n(Page)` to enable i18n.
- The page has the required and correct metadata using the `metadata` or `generateMetadata` function.
- Don't worry about authentication, it's handled in the middleware.

230
AGENTS.md
View File

@@ -1,207 +1,61 @@
This file provides guidance to Claude Code when working with code in this repository. # Makerkit SaaS Starter
## Core Technologies ## Tech Stack
- **Next.js 16** with App Router - **Next.js 16** (App Router) + **React 19** + **TypeScript**
- **Supabase** for database, auth, and storage - **Supabase** (Postgres, Auth, Storage)
- **React 19** - **Tailwind CSS 4** + Shadcn UI
- **TypeScript** - **Turborepo** monorepo
- **Tailwind CSS 4**, Shadcn UI, Lucide React
- **Turborepo**
## Monorepo Structure ## Monorepo Structure
- `apps/web` - Main Next.js SaaS application | Directory | Purpose | Details |
- `apps/web/supabase` - Supabase folder (migrations, schemas, tests) |-----------|---------|---------|
- `apps/e2e` - Playwright end-to-end tests | `apps/web` | Main Next.js app | See `apps/web/AGENTS.md` |
- `packages/features/*` - Feature packages | `apps/web/supabase` | Database schemas & migrations | See `apps/web/supabase/AGENTS.md` |
- `packages/` - Shared packages and utilities | `apps/e2e` | Playwright E2E tests | See `apps/e2e/AGENTS.md` |
| `packages/ui` | UI components (@kit/ui) | See `packages/ui/AGENTS.md` |
| `packages/supabase` | Supabase clients | See `packages/supabase/AGENTS.md` |
| `packages/next` | Next.js utilities | See `packages/next/AGENTS.md` |
| `packages/features` | Feature packages | See `packages/features/AGENTS.md` |
## Multi-Tenant Architecture ## Multi-Tenant Architecture
**Personal Accounts**: Individual user accounts (auth.users.id = accounts.id) - **Personal Accounts**: `auth.users.id = accounts.id`
**Team Accounts**: Shared workspaces with members, roles, and permissions - **Team Accounts**: Shared workspaces with members, roles, permissions
- Data links to accounts via `account_id` foreign key
Data associates with accounts via foreign keys for proper access control.
## Essential Commands ## Essential Commands
### Development Workflow
```bash ```bash
pnpm dev # Start all apps pnpm dev # Start development
pnpm supabase:web:start # Start local Supabase
pnpm supabase:web:reset # Reset database
pnpm supabase:web:typegen # Generate TypeScript types
pnpm typecheck # Type check
pnpm lint:fix # Fix linting
pnpm format:fix # Format code
``` ```
### Database Operations ## Key Patterns (Quick Reference)
```bash | Pattern | Import | Details |
pnpm supabase:web:start # Start Supabase locally |---------|--------|---------|
pnpm --filter web supabase migrations up # Apply new migrations | Server Actions | `enhanceAction` from `@kit/next/actions` | `packages/next/AGENTS.md` |
pnpm supabase:web:reset # Reset with latest schema (clean rebuild) | Route Handlers | `enhanceRouteHandler` from `@kit/next/routes` | `packages/next/AGENTS.md` |
pnpm supabase:web:typegen # Generate TypeScript types | Server Client | `getSupabaseServerClient` from `@kit/supabase/server-client` | `packages/supabase/AGENTS.md` |
pnpm --filter web supabase:db:diff # Create migration | UI Components | `@kit/ui/{component}` | `packages/ui/AGENTS.md` |
``` | Translations | `Trans` from `@kit/ui/trans` | `packages/ui/AGENTS.md` |
The typegen command must be run after applying migrations or resetting the database. ## Authorization
## Typescript - **RLS enforces access control** - no manual auth checks needed with standard client
- **Admin client** (`getSupabaseServerAdminClient`) bypasses RLS - use sparingly with manual validation
- Write clean, clear, well-designed, explicit TypeScript ## Verification
- Avoid obvious comments
- Avoid unnecessary complexity or overly abstract code
- Always use implicit type inference, unless impossible
- You must avoid using `any`
- Handle errors gracefully using try/catch and appropriate error types
- Use service pattern for server-side APIs
- Add `import 'server-only';` to code that is exclusively server-side
- Never mix client and server imports from a file or a package
- Extract self-contained classes/utilities (ex. algortihmic code) from classes that cross the network boundary
## React After implementation, always run:
1. `pnpm typecheck`
- Encapsulate repeated blocks of code into reusable local components 2. `pnpm lint:fix`
- Write small, composable, explicit, well-named components 3. `pnpm format:fix`
- Always use `react-hook-form` and `@kit/ui/form` for writing forms 4. Run code quality reviewer agent
- Always use 'use client' directive for client components
- Add `data-test` for E2E tests where appropriate
- `useEffect` is a code smell and must be justified - avoid if possible
- Do not write many (such as 4-5) separate `useState`, prefer single state object (unless required)
- Prefer server-side data fetching using RSC
- Display loading indicators (ex. with LoadingSpinner) component where appropriate
## Next.js
- Use `enhanceAction` for Server Actions
- Use `use server` in server actions files
- Use `enhanceRouteHandler` for API Routes
- Export page components using the `withI18n` utility
- Add well-written page metadata to pages
- Redirect using `redirect` following a server action instead of using client-side router
- Since `redirect` throws an error, handle `catch` block using `isRedirectError` from `next/dist/client/components/redirect-error` in client-side forms when calling the server action
## Data Fetching Architecture
Makerkit uses a clear separation between data fetching and mutations:
### Server Components with Loaders (Reading Data)
**Pattern**: Use async server components that call loader functions for initial data fetching.
```typescript
// Page component (apps/web/app/home/[account]/page.tsx)
async function TeamAccountPage({ params }: Props) {
const client = getSupabaseServerClient();
const slug = (await params).account;
const [projects, workspace] = await loadProjectsPageData(client, slug);
return <ProjectsList projects={projects} />;
}
// Loader function (_lib/server/projects-page.loader.ts)
import 'server-only';
export async function loadProjectsPageData(
client: SupabaseClient<Database>,
slug: string,
) {
return Promise.all([
loadProjects(client, slug),
loadTeamWorkspace(slug),
]);
}
async function loadProjects(client: SupabaseClient<Database>, slug: string) {
const { data, error } = await client.rpc('get_team_projects', {
account_slug: slug,
});
if (error) throw error;
return data ?? [];
}
```
### Server Actions (Mutations Only)
**Pattern**: Use `enhanceAction` for create/update/delete operations with schema validation.
```typescript
// server-actions.ts
'use server';
import { enhanceAction } from '@kit/next/actions';
export const createProject = enhanceAction(
async (data) => {
const client = getSupabaseServerClient();
const service = createProjectsService();
const response = await service.createProject(data);
if (response.error) {
throw response.error;
}
return {
success: true,
data: response.data,
};
},
{
schema: CreateProjectSchema,
},
);
```
### Authorization & RLS
- **Server Components**: RLS automatically enforces access control
- **Server Actions**: RLS validates permissions on mutations
- **No manual auth checks needed** when using standard Supabase client
- **Admin client**: Only for bypassing RLS (rare cases, requires careful manual validation)
## File Organization Patterns
### Route Structure
```
apps/web/app/home/[account]/
├── page.tsx # Team dashboard
├── members/
│ ├── page.tsx # Members listing
│ └── _lib/server/ # Server-side utilities
│ └── members-page.loader.ts
├── projects/ # New feature example
│ ├── page.tsx # Projects listing
│ ├── [id]/ # Individual project
│ │ └── page.tsx # Project detail page
│ ├── _components/ # Feature-specific components
│ │ ├── project-list.tsx
│ │ └── create-project-form.tsx
│ └── _lib/
│ ├── server/ # Server-side logic
│ │ ├── projects-page.loader.ts
│ │ └── projects-server-actions.ts
│ └── schemas/ # Zod validation schemas
│ └── project.schema.ts
└── _components/ # Shared team account components
└── team-account-layout-page-header.tsx
```
### Naming Conventions
- **Pages**: `page.tsx` (Next.js convention)
- **Loaders**: `{feature}-page.loader.ts`
- **Actions**: `{feature}-server-actions.ts`
- **Schemas**: `{feature}.schema.ts`
- **Components**: `kebab-case.tsx`
## UI Components
UI Components are placed at `packages/ui`. Call MCP tool to list components to verify they exist.
## Verification Steps
After implementation:
1. **Run `pnpm typecheck`** - Must pass without errors
2. **Run `pnpm lint:fix`** - Auto-fix issues
3. **Run `pnpm format:fix`** - Format code

212
CLAUDE.md
View File

@@ -1,211 +1 @@
This file provides guidance to Claude Code when working with code in this repository. @AGENTS.md
## Core Technologies
- **Next.js 16** with App Router
- **Supabase** for database, auth, and storage
- **React 19**
- **TypeScript**
- **Tailwind CSS 4**, Shadcn UI, Lucide React
- **Turborepo**
## Monorepo Structure
- `apps/web` - Main Next.js SaaS application
- `apps/web/supabase` - Supabase folder (migrations, schemas, tests)
- `apps/e2e` - Playwright end-to-end tests
- `packages/features/*` - Feature packages
- `packages/` - Shared packages and utilities
## Multi-Tenant Architecture
**Personal Accounts**: Individual user accounts (auth.users.id = accounts.id)
**Team Accounts**: Shared workspaces with members, roles, and permissions
Data associates with accounts via foreign keys for proper access control.
## Essential Commands
### Development Workflow
```bash
pnpm dev # Start all apps
```
### Database Operations
```bash
pnpm supabase:web:start # Start Supabase locally
pnpm --filter web supabase migrations up # Apply new migrations
pnpm supabase:web:reset # Reset with latest schema (clean rebuild)
pnpm supabase:web:typegen # Generate TypeScript types
pnpm --filter web supabase:db:diff # Create migration
```
The typegen command must be run after applying migrations or resetting the database.
## Typescript
- Write clean, clear, well-designed, explicit TypeScript
- Avoid obvious comments
- Avoid unnecessary complexity or overly abstract code
- Always use implicit type inference, unless impossible
- You must avoid using `any`
- Handle errors gracefully using try/catch and appropriate error types
- Use service pattern for server-side APIs
- Add `import 'server-only';` to code that is exclusively server-side
- Never mix client and server imports from a file or a package
- Extract self-contained classes/utilities (ex. algortihmic code) from classes that cross the network boundary
## React
- Encapsulate repeated blocks of code into reusable local components
- Write small, composable, explicit, well-named components
- Always use `react-hook-form` and `@kit/ui/form` for writing forms
- Always use 'use client' directive for client components
- Add `data-test` for E2E tests where appropriate
- `useEffect` is a code smell and must be justified - avoid if possible
- Do not write many (such as 4-5) separate `useState`, prefer single state object (unless required)
- Prefer server-side data fetching using RSC
- Display loading indicators (ex. with LoadingSpinner) component where appropriate
## Next.js
- Use `enhanceAction` for Server Actions
- Use `use server` in server actions files
- Use `enhanceRouteHandler` for API Routes
- Export page components using the `withI18n` utility
- Add well-written page metadata to pages
- Redirect using `redirect` following a server action instead of using client-side router
- Since `redirect` throws an error, handle `catch` block using `isRedirectError` from `next/dist/client/components/redirect-error` in client-side forms when calling the server action
## Data Fetching Architecture
Makerkit uses a clear separation between data fetching and mutations:
### Server Components with Loaders (Reading Data)
**Pattern**: Use async server components that call loader functions for initial data fetching.
```typescript
// Page component (apps/web/app/home/[account]/page.tsx)
async function TeamAccountPage({ params }: Props) {
const client = getSupabaseServerClient();
const slug = (await params).account;
const [projects, workspace] = await loadProjectsPageData(client, slug);
return <ProjectsList projects={projects} />;
}
// Loader function (_lib/server/projects-page.loader.ts)
import 'server-only';
export async function loadProjectsPageData(
client: SupabaseClient<Database>,
slug: string,
) {
return Promise.all([
loadProjects(client, slug),
loadTeamWorkspace(slug),
]);
}
async function loadProjects(client: SupabaseClient<Database>, slug: string) {
const { data, error } = await client.rpc('get_team_projects', {
account_slug: slug,
});
if (error) throw error;
return data ?? [];
}
```
### Server Actions (Mutations Only)
**Pattern**: Use `enhanceAction` for create/update/delete operations with schema validation.
```typescript
// server-actions.ts
'use server';
import { enhanceAction } from '@kit/next/actions';
export const createProject = enhanceAction(
async (data) => {
const client = getSupabaseServerClient();
const service = createProjectsService();
const response = await service.createProject(data);
if (response.error) {
throw response.error;
}
return {
success: true,
data: response.data,
};
},
{
schema: CreateProjectSchema,
},
);
```
### Authorization & RLS
- **Server Components**: RLS automatically enforces access control
- **Server Actions**: RLS validates permissions on mutations
- **No manual auth checks needed** when using standard Supabase client
- **Admin client**: Only for bypassing RLS (rare cases, requires careful manual validation)
## File Organization Patterns
### Route Structure
```
apps/web/app/home/[account]/
├── page.tsx # Team dashboard
├── members/
│ ├── page.tsx # Members listing
│ └── _lib/server/ # Server-side utilities
│ └── members-page.loader.ts
├── projects/ # New feature example
│ ├── page.tsx # Projects listing
│ ├── [id]/ # Individual project
│ │ └── page.tsx # Project detail page
│ ├── _components/ # Feature-specific components
│ │ ├── project-list.tsx
│ │ └── create-project-form.tsx
│ └── _lib/
│ ├── server/ # Server-side logic
│ │ ├── projects-page.loader.ts
│ │ └── projects-server-actions.ts
│ └── schemas/ # Zod validation schemas
│ └── project.schema.ts
└── _components/ # Shared team account components
└── team-account-layout-page-header.tsx
```
### Naming Conventions
- **Pages**: `page.tsx` (Next.js convention)
- **Loaders**: `{feature}-page.loader.ts`
- **Actions**: `{feature}-server-actions.ts`
- **Schemas**: `{feature}.schema.ts`
- **Components**: `kebab-case.tsx`
## UI Components
UI Components are placed at `packages/ui`. Call MCP tool to list components to verify they exist.
## Delegate to Agents
Please use the Task tool to delegate suitable tasks to specialized sub-agents for best handling the task at hand.
## Verification Steps
After implementation:
1. **Run `pnpm typecheck`** - Must pass without errors
2. **Run `pnpm lint:fix`** - Auto-fix issues
3. **Run `pnpm format:fix`** - Format code

View File

@@ -1,32 +1,23 @@
# End-to-End Testing
## End-to-End Testing with Playwright ## Skills
For E2E test implementation:
- `/playwright-e2e` - Test patterns and Page Objects
## Running Tests ## Running Tests
Running the tests for testing single file:
```bash ```bash
pnpm --filter web-e2e exec playwright test <partial-name-or-folder-name> --workers=1 # Single file (preferred)
``` pnpm --filter web-e2e exec playwright test <name> --workers=1
Example: # All tests
```bash
pnpm --filter web-e2e exec playwright test <partial-name-or-folder-name> --workers=1
```
This is useful for quickly testing a single file or a specific feature and should be your default choice.
Running all tests (rarely needed, only use if asked by the user):
```bash
pnpm test pnpm test
``` ```
### Page Object Pattern (Required) ## Page Object Pattern (Required)
Always use Page Objects for test organization and reusability:
```typescript ```typescript
// Example: auth.po.ts
export class AuthPageObject { export class AuthPageObject {
constructor(private readonly page: Page) {} constructor(private readonly page: Page) {}
@@ -35,91 +26,35 @@ export class AuthPageObject {
await this.page.fill('input[name="password"]', params.password); await this.page.fill('input[name="password"]', params.password);
await this.page.click('button[type="submit"]'); await this.page.click('button[type="submit"]');
} }
async signOut() {
await this.page.click('[data-test="account-dropdown-trigger"]');
await this.page.click('[data-test="account-dropdown-sign-out"]');
}
} }
``` ```
### Reliability Patterns ## Selectors
**Use `toPass()` for flaky operations** - Always wrap unreliable operations: Always use `data-test` attributes:
```typescript ```typescript
// ✅ CORRECT - Reliable email/OTP operations await this.page.click('[data-test="submit-button"]');
await expect(async () => { await this.page.getByTestId('submit-button').click();
const otpCode = await this.getOtpCodeFromEmail(email); ```
expect(otpCode).not.toBeNull();
await this.enterOtpCode(otpCode);
}).toPass();
// ✅ CORRECT - Network requests with validation ## Reliability with `toPass()`
```typescript
await expect(async () => { await expect(async () => {
const response = await this.page.waitForResponse(resp => const response = await this.page.waitForResponse(
resp.url().includes('auth/v1/user') resp => resp.url().includes('auth/v1/user')
); );
expect(response.status()).toBe(200); expect(response.status()).toBe(200);
}).toPass(); }).toPass();
// ✅ CORRECT - Complex operations with custom intervals
await expect(async () => {
await auth.submitMFAVerification(AuthPageObject.MFA_KEY);
}).toPass({
intervals: [500, 2500, 5000, 7500, 10_000, 15_000, 20_000]
});
``` ```
### Test Data Management ## Test Organization
**Email Testing**: Use `createRandomEmail()` for unique test emails:
```typescript
createRandomEmail() {
const value = Math.random() * 10000000000000;
return `${value.toFixed(0)}@makerkit.dev`;
}
``` ```
tests/
**User Bootstrapping**: Use `bootstrapUser()` for consistent test user creation: ├── authentication/
```typescript ├── billing/
await auth.bootstrapUser({ ├── *.po.ts # Page Objects
email: 'test@example.com', └── utils/
password: 'testingpassword',
name: 'Test User'
});
``` ```
This method creates a user with an API call.
To sign in:
```tsx
await auth.loginAsUser({
email: 'test@example.com',
password: 'testingpassword',
});
```
### Test Selectors
**Always use `data-test` attributes** for reliable element selection:
```typescript
// ✅ CORRECT - Use data-test attributes
await this.page.click('[data-test="submit-button"]');
await this.page.fill('[data-test="email-input"]', email);
// ✅ OR
await this.page.getByTestId('submit-button').click();
// ❌ AVOID - Fragile selectors
await this.page.click('.btn-primary');
await this.page.click('button:nth-child(2)');
```
### Test Organization
- **Feature-based folders**: `/tests/authentication/`, `/tests/billing/`
- **Page Objects**: `*.po.ts` files for reusable page interactions
- **Setup files**: `auth.setup.ts` for global test setup
- **Utility classes**: `/tests/utils/` for shared functionality

View File

@@ -1,125 +1 @@
@AGENTS.md
## End-to-End Testing with Playwright
## Running Tests
Running the tests for testing single file:
```bash
pnpm --filter web-e2e exec playwright test <partial-name-or-folder-name> --workers=1
```
Example:
```bash
pnpm --filter web-e2e exec playwright test <partial-name-or-folder-name> --workers=1
```
This is useful for quickly testing a single file or a specific feature and should be your default choice.
Running all tests (rarely needed, only use if asked by the user):
```bash
pnpm test
```
### Page Object Pattern (Required)
Always use Page Objects for test organization and reusability:
```typescript
// Example: auth.po.ts
export class AuthPageObject {
constructor(private readonly page: Page) {}
async signIn(params: { email: string; password: string }) {
await this.page.fill('input[name="email"]', params.email);
await this.page.fill('input[name="password"]', params.password);
await this.page.click('button[type="submit"]');
}
async signOut() {
await this.page.click('[data-test="account-dropdown-trigger"]');
await this.page.click('[data-test="account-dropdown-sign-out"]');
}
}
```
### Reliability Patterns
**Use `toPass()` for flaky operations** - Always wrap unreliable operations:
```typescript
// ✅ CORRECT - Reliable email/OTP operations
await expect(async () => {
const otpCode = await this.getOtpCodeFromEmail(email);
expect(otpCode).not.toBeNull();
await this.enterOtpCode(otpCode);
}).toPass();
// ✅ CORRECT - Network requests with validation
await expect(async () => {
const response = await this.page.waitForResponse(resp =>
resp.url().includes('auth/v1/user')
);
expect(response.status()).toBe(200);
}).toPass();
// ✅ CORRECT - Complex operations with custom intervals
await expect(async () => {
await auth.submitMFAVerification(AuthPageObject.MFA_KEY);
}).toPass({
intervals: [500, 2500, 5000, 7500, 10_000, 15_000, 20_000]
});
```
### Test Data Management
**Email Testing**: Use `createRandomEmail()` for unique test emails:
```typescript
createRandomEmail() {
const value = Math.random() * 10000000000000;
return `${value.toFixed(0)}@makerkit.dev`;
}
```
**User Bootstrapping**: Use `bootstrapUser()` for consistent test user creation:
```typescript
await auth.bootstrapUser({
email: 'test@example.com',
password: 'testingpassword',
name: 'Test User'
});
```
This method creates a user with an API call.
To sign in:
```tsx
await auth.loginAsUser({
email: 'test@example.com',
password: 'testingpassword',
});
```
### Test Selectors
**Always use `data-test` attributes** for reliable element selection:
```typescript
// ✅ CORRECT - Use data-test attributes
await this.page.click('[data-test="submit-button"]');
await this.page.fill('[data-test="email-input"]', email);
// ✅ OR
await this.page.getByTestId('submit-button').click();
// ❌ AVOID - Fragile selectors
await this.page.click('.btn-primary');
await this.page.click('button:nth-child(2)');
```
### Test Organization
- **Feature-based folders**: `/tests/authentication/`, `/tests/billing/`
- **Page Objects**: `*.po.ts` files for reusable page interactions
- **Setup files**: `auth.setup.ts` for global test setup
- **Utility classes**: `/tests/utils/` for shared functionality

View File

@@ -1,329 +1,78 @@
# Web Application Instructions # Web Application
This file contains instructions specific to the main Next.js web application. ## Route Organization
## Application Structure
### Route Organization
``` ```
app/ app/
├── (marketing)/ # Public pages (landing, blog, docs) ├── (marketing)/ # Public pages
├── (auth)/ # Authentication pages ├── (auth)/ # Authentication
├── home/ ├── home/ # Authenticated routes
│ ├── (user)/ # Personal account context │ ├── (user)/ # Personal account
│ └── [account]/ # Team account context ([account] = team slug) │ └── [account]/ # Team account (slug, not ID)
├── admin/ # Super admin section ├── admin/ # Super admin
└── api/ # API routes └── api/ # API routes
``` ```
Key Examples: ## Component Organization
- Marketing layout: `app/(marketing)/layout.tsx` - Route-specific: `_components/`
- Personal dashboard: `app/home/(user)/page.tsx` - Route utilities: `_lib/` (client), `_lib/server/` (server)
- Team workspace: `app/home/[account]/page.tsx`
- Admin section: `app/admin/page.tsx`
### Component Organization ## Skills
- **Route-specific**: Use `_components/` directories For specialized implementation:
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side - `/feature-builder` - End-to-end feature implementation
- **Global components**: Root-level directories - `/server-action-builder` - Server actions
- `/forms-builder` - Forms with validation
- `/navigation-config` - Adding routes and menu items
Example: ## Next.js 16 Params Pattern
- Team components: `app/home/[account]/_components/`
- Team server utils: `app/home/[account]/_lib/server/`
- Marketing components: `app/(marketing)/_components/`
The `[account]` parameter is the `accounts.slug` property, not the ID
## React Server Components - Async Pattern
**CRITICAL**: In Next.js 16, always await params directly in async server components:
```typescript ```typescript
// ❌ WRONG - Don't use React.use() in async functions // CORRECT - await params directly
async function Page({ params }: Props) { async function Page({ params }: Props) {
const { account } = use(params); const { account } = await params;
}
// ✅ CORRECT - await params directly in Next.js 16
async function Page({ params }: Props) {
const { account } = await params; // ✅ Server component pattern
}
// ✅ CORRECT - "use" in non-async functions in Next.js 16
function Page({ params }: Props) {
const { account } = use(params); // ✅ Server component pattern
} }
``` ```
## Data Fetching Strategy ## Data Fetching
**Quick Decision Framework:** - **Server Components** (default): `getSupabaseServerClient()` - RLS enforced
- **Client Components**: `useSupabase()` hook with React Query
- **Admin Client**: Bypasses RLS - requires manual auth validation
- **Server Components**: Default choice for initial data loading ## Workspace Contexts
- **Client Components**: For interactive features requiring hooks or real-time updates
- **Admin Client**: Only for bypassing RLS (rare cases - requires manual auth/authorization)
### Server Components (Preferred) ✅
```typescript ```typescript
import { getSupabaseServerClient } from '@kit/supabase/server-client'; // Personal: app/home/(user)
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
async function NotesPage() { // Team: app/home/[account]
const client = getSupabaseServerClient(); import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
const { data, error } = await client.from('notes').select('*');
if (error) return <ErrorMessage error={error} />;
return <NotesList notes={data} />;
}
``` ```
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed! ## Key Config Files
### Client Components (Interactive) 🖱️ | Purpose | Location |
|---------|----------|
```typescript | Feature flags | `config/feature-flags.config.ts` |
'use client'; | Paths | `config/paths.config.ts` |
import { useSupabase } from '@kit/supabase/hooks/use-supabase'; | Personal nav | `config/personal-account-navigation.config.tsx` |
import { useQuery } from '@tanstack/react-query'; | Team nav | `config/team-account-navigation.config.tsx` |
| i18n | `lib/i18n/i18n.settings.ts` |
function InteractiveNotes() {
const supabase = useSupabase();
const { data, isLoading } = useQuery({
queryKey: ['notes'],
queryFn: () => supabase.from('notes').select('*')
});
if (isLoading) return <Spinner />;
return <NotesList notes={data} />;
}
```
### Performance Optimization - Parallel Data Fetching 🚀
**Sequential (Slow) Pattern ❌**
```typescript
async function SlowDashboard() {
const userData = await loadUserData();
const notifications = await loadNotifications();
const metrics = await loadMetrics();
// Total time: sum of all requests
}
```
**Parallel (Optimized) Pattern ✅**
```typescript
async function FastDashboard() {
// Execute all requests simultaneously
const [userData, notifications, metrics] = await Promise.all([
loadUserData(),
loadNotifications(),
loadMetrics()
]);
// Total time: longest single request
return <Dashboard user={userData} notifications={notifications} metrics={metrics} />;
}
```
**Performance Impact**: Parallel fetching can reduce page load time by 60-80% for multi-data pages!
## Authorization Patterns - Critical Understanding 🔐
### RLS-Protected Data Fetching (Standard) ✅
```typescript
async function getUserNotes(userId: string) {
const client = getSupabaseServerClient();
// RLS automatically ensures user can only access their own notes
// NO additional authorization checks needed!
const { data } = await client.from('notes').select('*').eq('user_id', userId); // RLS validates this automatically
return data;
}
```
### Admin Client Usage (Dangerous - Rare Cases Only) ⚠️
```typescript
async function adminGetUserNotes(userId: string) {
const adminClient = getSupabaseServerAdminClient();
// CRITICAL: Manual authorization required - bypasses RLS!
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
}
// Additional validation: ensure current admin isn't targeting themselves
if (currentUser.id === userId) {
throw new Error('Cannot perform admin action on own account');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient
.from('notes')
.select('*')
.eq('user_id', userId);
return data;
}
```
**Rule of thumb**: If using standard Supabase client, trust RLS. If using admin client, validate everything manually.
## Internationalization ## Internationalization
Always use `Trans` component from `@kit/ui/trans`: Always use `Trans` component:
```tsx ```tsx
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
<Trans i18nKey="namespace:key" values={{ name }} />
<Trans
i18nKey="user:welcomeMessage"
values={{ name: user.name }}
/>
// With HTML elements
<Trans
i18nKey="terms:agreement"
components={{
TermsLink: <a href="/terms" className="underline" />,
}}
/>
``` ```
### Adding New Languages ## Security
1. Add language code to `lib/i18n/i18n.settings.ts` - Authentication enforced by middleware
2. Create translation files in `public/locales/[new-language]/` - Authorization handled by RLS
3. Copy structure from English files - Never pass sensitive data to Client Components
- Never expose server env vars to client
### Adding new namespaces
1. Translation files: `public/locales/<locale>/<namespace>.json`
2. Add namespace to `defaultI18nNamespaces` in `apps/web/lib/i18n/i18n.settings.ts`
## Workspace Contexts 🏢
### Personal Account Context (`app/home/(user)`)
```tsx
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
function PersonalComponent() {
const { user, account } = useUserWorkspace();
// Personal account data
}
```
Context provider: `@packages/features/accounts/src/components/user-workspace-context-provider.tsx`
### Team Account Context (`app/home/[account]`)
```tsx
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
function TeamComponent() {
const { account, user, accounts } = useTeamAccountWorkspace();
// Team account data with permissions
}
```
Context provider: `@packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
## Key Configuration Files
- **Feature flags**: `config/feature-flags.config.ts`
- **i18n settings**: `lib/i18n/i18n.settings.ts`
- **Supabase config**: `supabase/config.toml`
- **Middleware**: `middleware.ts`
## Route Handlers (API Routes)
Use `enhanceRouteHandler` from `@packages/next/src/routes/index.ts`:
```typescript
import { enhanceRouteHandler } from '@kit/next/routes';
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// body is validated, user available if auth: true
return NextResponse.json({ success: true });
},
{
auth: true,
schema: ZodSchema,
},
);
```
## Navigation Menu Configuration 🗺️
### Adding Sidebar Menu Items
**Config Files:**
- Personal: `config/personal-account-navigation.config.tsx`
- Team: `config/team-account-navigation.config.tsx`
**Add to Personal Navigation:**
```typescript
{
label: 'common:routes.yourFeature',
path: pathsConfig.app.yourFeaturePath,
Icon: <YourIcon className="w-4" />,
end: true,
},
```
**Add to Team Navigation:**
```typescript
{
label: 'common:routes.yourTeamFeature',
path: createPath(pathsConfig.app.yourTeamFeaturePath, account),
Icon: <YourIcon className="w-4" />,
},
```
**Add Paths:**
```typescript
// config/paths.config.ts
app: {
yourFeaturePath: '/home/your-feature',
yourTeamFeaturePath: '/home/[account]/your-feature',
}
```
**Add Translations:**
```json
// public/locales/en/common.json
"routes": {
"yourFeature": "Your Feature"
}
```
## Security Guidelines 🛡️
### Authentication & Authorization
- Authentication already enforced by middleware
- Authorization handled by RLS at database level (in most cases)
- Avoid defensive code - use RLS instead
- When using the Supabase admin client, must enforce both authentication and authorization
### Passing data to the client
- **Never pass sensitive data** to Client Components
- **Never expose server environment variables** to client (unless prefixed with NEXT_PUBLIC)
- Always validate user input

View File

@@ -1,329 +1 @@
# Web Application Instructions @AGENTS.md
This file contains instructions specific to the main Next.js web application.
## Application Structure
### Route Organization
```
app/
├── (marketing)/ # Public pages (landing, blog, docs)
├── (auth)/ # Authentication pages
├── home/
│ ├── (user)/ # Personal account context
│ └── [account]/ # Team account context ([account] = team slug)
├── admin/ # Super admin section
└── api/ # API routes
```
Key Examples:
- Marketing layout: `app/(marketing)/layout.tsx`
- Personal dashboard: `app/home/(user)/page.tsx`
- Team workspace: `app/home/[account]/page.tsx`
- Admin section: `app/admin/page.tsx`
### Component Organization
- **Route-specific**: Use `_components/` directories
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side
- **Global components**: Root-level directories
Example:
- Team components: `app/home/[account]/_components/`
- Team server utils: `app/home/[account]/_lib/server/`
- Marketing components: `app/(marketing)/_components/`
The `[account]` parameter is the `accounts.slug` property, not the ID
## React Server Components - Async Pattern
**CRITICAL**: In Next.js 16, always await params directly in async server components:
```typescript
// ❌ WRONG - Don't use React.use() in async functions
async function Page({ params }: Props) {
const { account } = use(params);
}
// ✅ CORRECT - await params directly in Next.js 16
async function Page({ params }: Props) {
const { account } = await params; // ✅ Server component pattern
}
// ✅ CORRECT - "use" in non-async functions in Next.js 16
function Page({ params }: Props) {
const { account } = use(params); // ✅ Server component pattern
}
```
## Data Fetching Strategy
**Quick Decision Framework:**
- **Server Components**: Default choice for initial data loading
- **Client Components**: For interactive features requiring hooks or real-time updates
- **Admin Client**: Only for bypassing RLS (rare cases - requires manual auth/authorization)
### Server Components (Preferred) ✅
```typescript
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function NotesPage() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
if (error) return <ErrorMessage error={error} />;
return <NotesList notes={data} />;
}
```
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed!
### Client Components (Interactive) 🖱️
```typescript
'use client';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
import { useQuery } from '@tanstack/react-query';
function InteractiveNotes() {
const supabase = useSupabase();
const { data, isLoading } = useQuery({
queryKey: ['notes'],
queryFn: () => supabase.from('notes').select('*')
});
if (isLoading) return <Spinner />;
return <NotesList notes={data} />;
}
```
### Performance Optimization - Parallel Data Fetching 🚀
**Sequential (Slow) Pattern ❌**
```typescript
async function SlowDashboard() {
const userData = await loadUserData();
const notifications = await loadNotifications();
const metrics = await loadMetrics();
// Total time: sum of all requests
}
```
**Parallel (Optimized) Pattern ✅**
```typescript
async function FastDashboard() {
// Execute all requests simultaneously
const [userData, notifications, metrics] = await Promise.all([
loadUserData(),
loadNotifications(),
loadMetrics()
]);
// Total time: longest single request
return <Dashboard user={userData} notifications={notifications} metrics={metrics} />;
}
```
**Performance Impact**: Parallel fetching can reduce page load time by 60-80% for multi-data pages!
## Authorization Patterns - Critical Understanding 🔐
### RLS-Protected Data Fetching (Standard) ✅
```typescript
async function getUserNotes(userId: string) {
const client = getSupabaseServerClient();
// RLS automatically ensures user can only access their own notes
// NO additional authorization checks needed!
const { data } = await client.from('notes').select('*').eq('user_id', userId); // RLS validates this automatically
return data;
}
```
### Admin Client Usage (Dangerous - Rare Cases Only) ⚠️
```typescript
async function adminGetUserNotes(userId: string) {
const adminClient = getSupabaseServerAdminClient();
// CRITICAL: Manual authorization required - bypasses RLS!
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
}
// Additional validation: ensure current admin isn't targeting themselves
if (currentUser.id === userId) {
throw new Error('Cannot perform admin action on own account');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient
.from('notes')
.select('*')
.eq('user_id', userId);
return data;
}
```
**Rule of thumb**: If using standard Supabase client, trust RLS. If using admin client, validate everything manually.
## Internationalization
Always use `Trans` component from `@kit/ui/trans`:
```tsx
import { Trans } from '@kit/ui/trans';
<Trans
i18nKey="user:welcomeMessage"
values={{ name: user.name }}
/>
// With HTML elements
<Trans
i18nKey="terms:agreement"
components={{
TermsLink: <a href="/terms" className="underline" />,
}}
/>
```
### Adding New Languages
1. Add language code to `lib/i18n/i18n.settings.ts`
2. Create translation files in `public/locales/[new-language]/`
3. Copy structure from English files
### Adding new namespaces
1. Translation files: `public/locales/<locale>/<namespace>.json`
2. Add namespace to `defaultI18nNamespaces` in `apps/web/lib/i18n/i18n.settings.ts`
## Workspace Contexts 🏢
### Personal Account Context (`app/home/(user)`)
```tsx
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
function PersonalComponent() {
const { user, account } = useUserWorkspace();
// Personal account data
}
```
Context provider: `@packages/features/accounts/src/components/user-workspace-context-provider.tsx`
### Team Account Context (`app/home/[account]`)
```tsx
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
function TeamComponent() {
const { account, user, accounts } = useTeamAccountWorkspace();
// Team account data with permissions
}
```
Context provider: `@packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
## Key Configuration Files
- **Feature flags**: `config/feature-flags.config.ts`
- **i18n settings**: `lib/i18n/i18n.settings.ts`
- **Supabase config**: `supabase/config.toml`
- **Middleware**: `middleware.ts`
## Route Handlers (API Routes)
Use `enhanceRouteHandler` from `@packages/next/src/routes/index.ts`:
```typescript
import { enhanceRouteHandler } from '@kit/next/routes';
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// body is validated, user available if auth: true
return NextResponse.json({ success: true });
},
{
auth: true,
schema: ZodSchema,
},
);
```
## Navigation Menu Configuration 🗺️
### Adding Sidebar Menu Items
**Config Files:**
- Personal: `config/personal-account-navigation.config.tsx`
- Team: `config/team-account-navigation.config.tsx`
**Add to Personal Navigation:**
```typescript
{
label: 'common:routes.yourFeature',
path: pathsConfig.app.yourFeaturePath,
Icon: <YourIcon className="w-4" />,
end: true,
},
```
**Add to Team Navigation:**
```typescript
{
label: 'common:routes.yourTeamFeature',
path: createPath(pathsConfig.app.yourTeamFeaturePath, account),
Icon: <YourIcon className="w-4" />,
},
```
**Add Paths:**
```typescript
// config/paths.config.ts
app: {
yourFeaturePath: '/home/your-feature',
yourTeamFeaturePath: '/home/[account]/your-feature',
}
```
**Add Translations:**
```json
// public/locales/en/common.json
"routes": {
"yourFeature": "Your Feature"
}
```
## Security Guidelines 🛡️
### Authentication & Authorization
- Authentication already enforced by middleware
- Authorization handled by RLS at database level (in most cases)
- Avoid defensive code - use RLS instead
- When using the Supabase admin client, must enforce both authentication and authorization
### Passing data to the client
- **Never pass sensitive data** to Client Components
- **Never expose server environment variables** to client (unless prefixed with NEXT_PUBLIC)
- Always validate user input

View File

@@ -1,119 +1,55 @@
# Super Admin # Super Admin
This file provides specific guidance for AI agents working in the super admin section of the application. ## Critical Security Rules
## Core Admin Principles - **ALWAYS** use `AdminGuard` to protect pages
- **ALWAYS** validate admin status before operations
- **NEVER** bypass authentication or authorization
- **ALWAYS** audit admin operations with logging
### Security-First Development ## Page Structure
- **ALWAYS** use `AdminGuard` to protect admin pages ```typescript
- **NEVER** bypass authentication or authorization checks import { AdminGuard } from '@kit/admin/components/admin-guard';
- **CRITICAL**: Use admin Supabase client with manual authorization validation import { PageBody, PageHeader } from '@kit/ui/page';
- Validate permissions for every admin operation
### Admin Client Usage Pattern async function AdminPage() {
return (
<>
<PageHeader title="Admin" />
<PageBody>{/* Content */}</PageBody>
</>
);
}
export default AdminGuard(AdminPage);
```
## Admin Client Usage
```typescript ```typescript
import { isSuperAdmin } from '@kit/admin'; import { isSuperAdmin } from '@kit/admin';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client'; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
async function adminOperation() { async function adminOperation() {
const adminClient = getSupabaseServerAdminClient(); // CRITICAL: Validate first - admin client bypasses RLS
// CRITICAL: Always validate admin status first
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) { if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required'); throw new Error('Unauthorized');
} }
// Now safe to proceed with admin privileges const adminClient = getSupabaseServerAdminClient();
const { data } = await adminClient.from('accounts').select('*'); // Safe to proceed
return data;
} }
``` ```
## Page Structure Patterns ## Audit Logging
### Standard Admin Page Template
```typescript ```typescript
import { AdminGuard } from '@kit/admin/components/admin-guard'; const logger = await getLogger();
import { PageBody, PageHeader } from '@kit/ui/page'; logger.info({
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; name: 'admin-audit',
action: 'delete-user',
async function AdminPageComponent() { adminId: currentUser.id,
return ( targetId: userId,
<> }, 'Admin action performed');
<PageHeader description={<AppBreadcrumbs />}>
{/* Page actions go here */}
</PageHeader>
<PageBody>
{/* Main content */}
</PageBody>
</>
);
}
// ALWAYS wrap with AdminGuard
export default AdminGuard(AdminPageComponent);
``` ```
### Async Server Component Pattern
```typescript
// ✅ CORRECT - Next.js 16 pattern
async function AdminPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params; // ✅ await params directly
// Fetch admin data
const data = await loadAdminData(id);
return <AdminContent data={data} />;
}
```
## Security Guidelines
### Critical Security Rules
1. **NEVER** expose admin functionality to non-admin users
2. **ALWAYS** validate admin status before operations
3. **NEVER** trust client-side admin checks alone
4. **ALWAYS** use server-side validation for admin actions
5. **NEVER** log sensitive admin data
6. **ALWAYS** audit admin operations
### Admin Action Auditing
```typescript
async function auditedAdminAction(action: string, data: unknown) {
const logger = await getLogger();
await logger.info(
{
name: 'admin-audit',
action,
adminId: currentUser.id,
timestamp: new Date().toISOString(),
data: {
// Log only non-sensitive fields
operation: action,
targetId: data.id,
},
},
'Admin action performed',
);
}
```
## Common Patterns to Follow
1. **Always wrap admin pages with `AdminGuard`**
2. **Use admin client only when RLS bypass is required**
3. **Implement proper error boundaries for admin components**
4. **Add comprehensive logging for admin operations**
5. **Use TypeScript strictly for admin interfaces**
6. **Follow the established admin component naming conventions**
7. **Implement proper loading states for admin operations**
8. **Add proper metadata to admin pages**

View File

@@ -1,119 +1 @@
# Super Admin @AGENTS.md
This file provides specific guidance for AI agents working in the super admin section of the application.
## Core Admin Principles
### Security-First Development
- **ALWAYS** use `AdminGuard` to protect admin pages
- **NEVER** bypass authentication or authorization checks
- **CRITICAL**: Use admin Supabase client with manual authorization validation
- Validate permissions for every admin operation
### Admin Client Usage Pattern
```typescript
import { isSuperAdmin } from '@kit/admin';
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
async function adminOperation() {
const adminClient = getSupabaseServerAdminClient();
// CRITICAL: Always validate admin status first
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient.from('accounts').select('*');
return data;
}
```
## Page Structure Patterns
### Standard Admin Page Template
```typescript
import { AdminGuard } from '@kit/admin/components/admin-guard';
import { PageBody, PageHeader } from '@kit/ui/page';
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
async function AdminPageComponent() {
return (
<>
<PageHeader description={<AppBreadcrumbs />}>
{/* Page actions go here */}
</PageHeader>
<PageBody>
{/* Main content */}
</PageBody>
</>
);
}
// ALWAYS wrap with AdminGuard
export default AdminGuard(AdminPageComponent);
```
### Async Server Component Pattern
```typescript
// ✅ CORRECT - Next.js 16 pattern
async function AdminPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params; // ✅ await params directly
// Fetch admin data
const data = await loadAdminData(id);
return <AdminContent data={data} />;
}
```
## Security Guidelines
### Critical Security Rules
1. **NEVER** expose admin functionality to non-admin users
2. **ALWAYS** validate admin status before operations
3. **NEVER** trust client-side admin checks alone
4. **ALWAYS** use server-side validation for admin actions
5. **NEVER** log sensitive admin data
6. **ALWAYS** audit admin operations
### Admin Action Auditing
```typescript
async function auditedAdminAction(action: string, data: unknown) {
const logger = await getLogger();
await logger.info(
{
name: 'admin-audit',
action,
adminId: currentUser.id,
timestamp: new Date().toISOString(),
data: {
// Log only non-sensitive fields
operation: action,
targetId: data.id,
},
},
'Admin action performed',
);
}
```
## Common Patterns to Follow
1. **Always wrap admin pages with `AdminGuard`**
2. **Use admin client only when RLS bypass is required**
3. **Implement proper error boundaries for admin components**
4. **Add comprehensive logging for admin operations**
5. **Use TypeScript strictly for admin interfaces**
6. **Follow the established admin component naming conventions**
7. **Implement proper loading states for admin operations**
8. **Add proper metadata to admin pages**

View File

@@ -1,265 +1,73 @@
# Supabase Database Schema Management # Supabase Database
This file contains guidance for working with database schemas, migrations, and Supabase development workflows.
## Schema Organization ## Schema Organization
Schemas are organized in numbered files in the `schemas/` directory. Numbers are used to sort dependencies. Schemas in `schemas/` directory with numbered prefixes for dependency ordering.
Migrations are generated from schemas. If creating a new schema, the migration can be created using the exact same content. ## Skills
If modifying an existing migration, use the `diff` command: For database implementation:
- `/postgres-expert` - Schema design, RLS, migrations, testing
### 1. Creating new entities ## Migration Workflow
When creating new entities (such as creating a new tabble), we can create a migration as is, just copying its content. ### New Entities
```bash ```bash
# Create new schema file # Create schema file
touch apps/web/supabase/schemas/15-my-new-feature.sql touch schemas/20-feature.sql
# Create Migration # Create migration
pnpm --filter web run supabase migrations new my-new-feature pnpm --filter web run supabase migrations new feature_name
# Copy content to migration # Copy content, apply, generate types
cp apps/web/supabase/schemas/15-my-new-feature.sql apps/web/supabase/migrations/$(ls -t apps/web/supabase/migrations/ | head -n1) pnpm --filter web supabase migrations up
# Apply migration
pnpm --filter web supabase migrations up # alternatively reset db with pnpm supabase:web:reset
# Generate TypeScript types
pnpm supabase:web:typegen pnpm supabase:web:typegen
``` ```
### 2. Modifying existing entities ### Modify Existing
When modifying existing entities (such ass adding a field to an existing table), we can use the `diff` command to generate a migration following the changes:
```bash ```bash
# Edit schema file (e.g., schemas/03-accounts.sql) # Edit schema, generate diff
# Make your changes... pnpm --filter web run supabase:db:diff -f update_feature
# Create migration for changes # Apply and regenerate
pnpm --filter web run supabase:db:diff -f update-accounts pnpm --filter web supabase migrations up
# Apply and test
pnpm --filter web supabase migrations up # alternatively reset db with pnpm supabase:web:reset
# After resetting
pnpm supabase:web:typegen pnpm supabase:web:typegen
``` ```
## Security First Patterns ## Security Rules
## Add permissions (if any) - **ALWAYS enable RLS** on new tables
- **NEVER use SECURITY DEFINER** without explicit access controls
- Use existing helper functions (see `/postgres-expert` skill)
## Table Template
```sql ```sql
ALTER TYPE public.app_permissions ADD VALUE 'notes.manage'; create table if not exists public.feature (
COMMIT;
```
### Table Creation with RLS
```sql
-- Create table
create table if not exists public.notes (
id uuid unique not null default extensions.uuid_generate_v4(), id uuid unique not null default extensions.uuid_generate_v4(),
account_id uuid references public.accounts(id) on delete cascade not null, account_id uuid references public.accounts(id) on delete cascade not null,
-- ... created_at timestamp with time zone default now(),
primary key (id) primary key (id)
); );
-- CRITICAL: Always enable RLS alter table "public"."feature" enable row level security;
alter table "public"."notes" enable row level security; revoke all on public.feature from authenticated, service_role;
grant select, insert, update, delete on table public.feature to authenticated;
-- Revoke default permissions -- Use helper functions for policies
revoke all on public.notes from authenticated, service_role; create policy "feature_read" on public.feature for select
-- Grant specific permissions
grant select, insert, update, delete on table public.notes to authenticated;
-- Add RLS policies
create policy "notes_read" on public.notes for select
to authenticated using ( to authenticated using (
account_id = (select auth.uid()) or account_id = (select auth.uid()) or
public.has_role_on_account(account_id) public.has_role_on_account(account_id)
); );
create policy "notes_write" on public.notes for insert
to authenticated with check (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
create policy "notes_update" on public.notes for update
to authenticated using (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
)
with check (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
create policy "notes_delete" on public.notes for delete
to authenticated using (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
``` ```
### Storage Bucket Policies ## Commands
```sql
-- Create storage bucket
insert into storage.buckets (id, name, public)
values ('documents', 'documents', false);
-- RLS policy for storage
create policy documents_policy on storage.objects for all using (
bucket_id = 'documents'
and (
-- File belongs to user's account
kit.get_storage_filename_as_uuid(name) = auth.uid()
or
-- User has access to the account
public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
)
with check (
bucket_id = 'documents'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or
public.has_permission(
auth.uid(),
kit.get_storage_filename_as_uuid(name),
'files.upload'::app_permissions
)
)
);
```
## Function Creation Patterns
### Safe Security Definer Functions
```sql
-- NEVER create security definer functions without explicit access controls
create or replace function public.create_team_account(account_name text)
returns public.accounts
language plpgsql
security definer -- Elevated privileges
set search_path = '' -- Prevent SQL injection
as $$
declare
new_account public.accounts;
begin
-- CRITICAL: Validate permissions first
if not public.is_set('enable_team_accounts') then
raise exception 'Team accounts are not enabled';
end if;
-- Additional validation can go here
if length(account_name) < 3 then
raise exception 'Account name must be at least 3 characters';
end if;
-- Now safe to proceed with elevated privileges
insert into public.accounts (name, is_personal_account)
values (account_name, false)
returning * into new_account;
return new_account;
end;
$$;
-- Grant to authenticated users only
grant execute on function public.create_team_account(text) to authenticated;
```
### Security Invoker Functions (Safer)
```sql
-- Preferred: Functions that inherit RLS policies
create or replace function public.get_account_notes(target_account_id uuid)
returns setof public.notes
language plpgsql
security invoker -- Inherits caller's permissions (RLS applies)
set search_path = ''
as $$
begin
-- RLS policies will automatically restrict results
return query
select * from public.notes
where account_id = target_account_id
order by created_at desc;
end;
$$;
grant execute on function public.get_account_notes(uuid) to authenticated;
```
### Safe Column Additions
```sql
-- Safe: Add nullable columns
alter table public.accounts
add column if not exists description text;
-- Safe: Add columns with defaults
alter table public.accounts
add column if not exists is_verified boolean default false not null;
-- Unsafe: Adding non-null columns without defaults
-- alter table public.accounts add column required_field text not null; -- DON'T DO THIS
```
### Index Management
```sql
-- Create indexes concurrently for large tables
create index concurrently if not exists ix_accounts_created_at
on public.accounts (created_at desc);
-- Drop unused indexes
drop index if exists ix_old_unused_index;
```
## Testing Database Changes
### Local Testing
```bash ```bash
# Test with fresh database pnpm supabase:web:reset # Reset database
pnpm supabase:web:reset pnpm supabase:web:typegen # Generate TypeScript types
pnpm --filter web supabase migrations list # View migrations
# Test your changes
pnpm run supabase:web:test
```
## Common Schema Patterns
### Audit Trail
Add triggers if the properties exist and are appropriate:
- `public.trigger_set_timestamps()` - for tables with `created_at` and `updated_at`
columns
- `public.trigger_set_user_tracking()` - for tables with `created_by` and `updated_by`
columns
### Useful Commands
```bash
# View migration status
pnpm --filter web supabase migrations list
# Reset database completely
pnpm supabase:web:reset
# Generate migration from schema diff
pnpm --filter web run supabase:db:diff -f migration-name
## Apply created migration
pnpm --filter web supabase migrations up
# Apply specific migration
pnpm --filter web supabase migrations up --include-schemas public
``` ```

View File

@@ -1,265 +1 @@
# Supabase Database Schema Management @AGENTS.md
This file contains guidance for working with database schemas, migrations, and Supabase development workflows.
## Schema Organization
Schemas are organized in numbered files in the `schemas/` directory. Numbers are used to sort dependencies.
Migrations are generated from schemas. If creating a new schema, the migration can be created using the exact same content.
If modifying an existing migration, use the `diff` command:
### 1. Creating new entities
When creating new entities (such as creating a new tabble), we can create a migration as is, just copying its content.
```bash
# Create new schema file
touch apps/web/supabase/schemas/15-my-new-feature.sql
# Create Migration
pnpm --filter web supabase migrations new my-new-feature
# Copy content to migration
cp apps/web/supabase/schemas/15-my-new-feature.sql apps/web/supabase/migrations/$(ls -t apps/web/supabase/migrations/ | head -n1)
# Apply migration
pnpm --filter web supabase migrations up # alternatively reset db with pnpm supabase:web:reset
# Generate TypeScript types
pnpm supabase:web:typegen
```
### 2. Modifying existing entities
When modifying existing entities (such ass adding a field to an existing table), we can use the `diff` command to generate a migration following the changes:
```bash
# Edit schema file (e.g., schemas/03-accounts.sql)
# Make your changes...
# Create migration for changes
pnpm --filter web run supabase:db:diff -f update-accounts
# Apply and test
pnpm --filter web supabase migrations up # alternatively reset db with pnpm supabase:web:reset
# After resetting
pnpm supabase:web:typegen
```
## Security First Patterns
## Add permissions (if any)
```sql
ALTER TYPE public.app_permissions ADD VALUE 'notes.manage';
COMMIT;
```
### Table Creation with RLS
```sql
-- Create table
create table if not exists public.notes (
id uuid unique not null default extensions.uuid_generate_v4(),
account_id uuid references public.accounts(id) on delete cascade not null,
-- ...
primary key (id)
);
-- CRITICAL: Always enable RLS
alter table "public"."notes" enable row level security;
-- Revoke default permissions
revoke all on public.notes from authenticated, service_role;
-- Grant specific permissions
grant select, insert, update, delete on table public.notes to authenticated;
-- Add RLS policies
create policy "notes_read" on public.notes for select
to authenticated using (
account_id = (select auth.uid()) or
public.has_role_on_account(account_id)
);
create policy "notes_write" on public.notes for insert
to authenticated with check (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
create policy "notes_update" on public.notes for update
to authenticated using (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
)
with check (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
create policy "notes_delete" on public.notes for delete
to authenticated using (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
```
### Storage Bucket Policies
```sql
-- Create storage bucket
insert into storage.buckets (id, name, public)
values ('documents', 'documents', false);
-- RLS policy for storage
create policy documents_policy on storage.objects for all using (
bucket_id = 'documents'
and (
-- File belongs to user's account
kit.get_storage_filename_as_uuid(name) = auth.uid()
or
-- User has access to the account
public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
)
with check (
bucket_id = 'documents'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or
public.has_permission(
auth.uid(),
kit.get_storage_filename_as_uuid(name),
'files.upload'::app_permissions
)
)
);
```
## Function Creation Patterns
### Safe Security Definer Functions
```sql
-- NEVER create security definer functions without explicit access controls
create or replace function public.create_team_account(account_name text)
returns public.accounts
language plpgsql
security definer -- Elevated privileges
set search_path = '' -- Prevent SQL injection
as $$
declare
new_account public.accounts;
begin
-- CRITICAL: Validate permissions first
if not public.is_set('enable_team_accounts') then
raise exception 'Team accounts are not enabled';
end if;
-- Additional validation can go here
if length(account_name) < 3 then
raise exception 'Account name must be at least 3 characters';
end if;
-- Now safe to proceed with elevated privileges
insert into public.accounts (name, is_personal_account)
values (account_name, false)
returning * into new_account;
return new_account;
end;
$$;
-- Grant to authenticated users only
grant execute on function public.create_team_account(text) to authenticated;
```
### Security Invoker Functions (Safer)
```sql
-- Preferred: Functions that inherit RLS policies
create or replace function public.get_account_notes(target_account_id uuid)
returns setof public.notes
language plpgsql
security invoker -- Inherits caller's permissions (RLS applies)
set search_path = ''
as $$
begin
-- RLS policies will automatically restrict results
return query
select * from public.notes
where account_id = target_account_id
order by created_at desc;
end;
$$;
grant execute on function public.get_account_notes(uuid) to authenticated;
```
### Safe Column Additions
```sql
-- Safe: Add nullable columns
alter table public.accounts
add column if not exists description text;
-- Safe: Add columns with defaults
alter table public.accounts
add column if not exists is_verified boolean default false not null;
-- Unsafe: Adding non-null columns without defaults
-- alter table public.accounts add column required_field text not null; -- DON'T DO THIS
```
### Index Management
```sql
-- Create indexes concurrently for large tables
create index concurrently if not exists ix_accounts_created_at
on public.accounts (created_at desc);
-- Drop unused indexes
drop index if exists ix_old_unused_index;
```
## Testing Database Changes
### Local Testing
```bash
# Test with fresh database
pnpm supabase:web:reset
# Test your changes
pnpm run supabase:web:test
```
## Common Schema Patterns
### Audit Trail
Add triggers if the properties exist and are appropriate:
- `public.trigger_set_timestamps()` - for tables with `created_at` and `updated_at`
columns
- `public.trigger_set_user_tracking()` - for tables with `created_by` and `updated_by`
columns
### Useful Commands
```bash
# View migration status
pnpm --filter web supabase migrations list
# Reset database completely
pnpm supabase:web:reset
# Generate migration from schema diff
pnpm --filter web run supabase:db:diff -f migration-name
## Apply created migration
pnpm --filter web supabase migrations up
# Apply specific migration
pnpm --filter web supabase migrations up --include-schemas public
```

View File

@@ -1,6 +1,6 @@
{ {
"name": "next-supabase-saas-kit-turbo", "name": "next-supabase-saas-kit-turbo",
"version": "2.23.5", "version": "2.23.7",
"private": true, "private": true,
"sideEffects": false, "sideEffects": false,
"engines": { "engines": {

View File

@@ -1,105 +1 @@
# @kit/analytics Package @AGENTS.md
Analytics package providing a unified interface for tracking events, page views, and user identification across multiple analytics providers.
## Architecture
- **AnalyticsManager**: Central manager orchestrating multiple analytics providers
- **AnalyticsService**: Interface defining analytics operations (track, identify, pageView)
- **Provider System**: Pluggable providers (currently includes NullAnalyticsService)
- **Client/Server Split**: Separate entry points for client and server-side usage
## Usage
### Basic Import
```typescript
// Client-side
import { analytics } from '@kit/analytics';
// Server-side
import { analytics } from '@kit/analytics/server';
```
### Core Methods
```typescript
// Track events
await analytics.trackEvent('button_clicked', {
button_id: 'signup',
page: 'homepage'
});
// Track page views
await analytics.trackPageView('/dashboard');
// Identify users
await analytics.identify('user123', {
email: 'user@example.com',
plan: 'premium'
});
```
Page views and user identification are handled by the plugin by default.
## Creating Custom Providers
Implement the `AnalyticsService` interface:
```typescript
import { AnalyticsService } from '@kit/analytics';
class CustomAnalyticsService implements AnalyticsService {
async initialize(): Promise<void> {
// Initialize your analytics service
}
async trackEvent(name: string, properties?: Record<string, string | string[]>): Promise<void> {
// Track event implementation
}
async trackPageView(path: string): Promise<void> {
// Track page view implementation
}
async identify(userId: string, traits?: Record<string, string>): Promise<void> {
// Identify user implementation
}
}
```
## Default Behavior
- Uses `NullAnalyticsService` when no providers are active
- All methods return Promises that resolve to arrays of provider results
- Console debug logging when no active services or using null service
- Graceful error handling with console warnings for missing providers
## Server-Side Analytics
When using PostHog, you can track events server-side for better reliability and privacy:
```typescript
import { analytics } from '@kit/analytics/server';
// Server-side event tracking (e.g., in API routes)
export async function POST(request: Request) {
// ... handle request
// Track server-side events
await analytics.trackEvent('api_call', {
endpoint: '/api/users',
method: 'POST',
user_id: userId,
});
return Response.json({ success: true });
}
// Track user registration server-side
await analytics.identify(user.id, {
email: user.email,
created_at: user.created_at,
plan: user.plan,
});
```

View File

@@ -1,289 +1 @@
# Feature Packages Instructions @AGENTS.md
This file contains instructions for working with feature packages including accounts, teams, billing, auth, and notifications.
## Feature Package Structure
- `accounts/` - Personal account management
- `admin/` - Super admin functionality
- `auth/` - Authentication features
- `notifications/` - Notification system
- `team-accounts/` - Team account management
## Account Services
### Personal Accounts API
Located at: `packages/features/accounts/src/server/api.ts`
```typescript
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
const client = getSupabaseServerClient();
const api = createAccountsApi(client);
// Get account data
const account = await api.getAccount(accountId);
// Get account workspace
const workspace = await api.getAccountWorkspace();
// Load user accounts
const accounts = await api.loadUserAccounts();
// Get subscription
const subscription = await api.getSubscription(accountId);
// Get customer ID
const customerId = await api.getCustomerId(accountId);
```
### Team Accounts API
Located at: `packages/features/team-accounts/src/server/api.ts`
```typescript
import { createTeamAccountsApi } from '@kit/team-accounts/api';
const api = createTeamAccountsApi(client);
// Get team account by slug
const account = await api.getTeamAccount(slug);
// Get account workspace
const workspace = await api.getAccountWorkspace(slug);
// Check permissions
const hasPermission = await api.hasPermission({
accountId,
userId,
permission: 'billing.manage'
});
// Get members count
const count = await api.getMembersCount(accountId);
// Get invitation
const invitation = await api.getInvitation(adminClient, token);
```
## Workspace Contexts
### Personal Account Context
Use in `apps/web/app/home/(user)` routes:
```tsx
import { useUserWorkspace } from 'kit/accounts/hooks/use-user-workspace';
function PersonalComponent() {
const { user, account } = useUserWorkspace();
// user: authenticated user data
// account: personal account data
return <div>Welcome {user.name}</div>;
}
```
Context provider: `packages/features/accounts/src/components/user-workspace-context-provider.tsx`
### Team Account Context
Use in `apps/web/app/home/[account]` routes:
```tsx
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
function TeamComponent() {
const { account, user, accounts } = useTeamAccountWorkspace();
// account: current team account data
// user: authenticated user data
// accounts: all accounts user has access to
return <div>Team: {account.name}</div>;
}
```
Context provider: `packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
## Billing Services
### Personal Billing
Located at: `apps/web/app/home/(user)/billing/_lib/server/user-billing.service.ts`
```typescript
// Personal billing operations
// - Manage individual user subscriptions
// - Handle personal account payments
// - Process individual billing changes
```
### Team Billing
Located at: `apps/web/app/home/[account]/billing/_lib/server/team-billing.service.ts`
```typescript
// Team billing operations
// - Manage team subscriptions
// - Handle team payments
// - Process team billing changes
```
### Per-Seat Billing Service
Located at: `packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts`
```typescript
import { createAccountPerSeatBillingService } from '@kit/team-accounts/billing';
const billingService = createAccountPerSeatBillingService(client);
// Increase seats when adding team members
await billingService.increaseSeats(accountId);
// Decrease seats when removing team members
await billingService.decreaseSeats(accountId);
// Get per-seat subscription item
const subscription = await billingService.getPerSeatSubscriptionItem(accountId);
```
## Authentication Features
### OTP for Sensitive Operations
Use one-time tokens from `packages/otp/src/api/index.ts`:
```tsx
import { VerifyOtpForm } from '@kit/otp/components';
<VerifyOtpForm
purpose="account-deletion"
email={user.email}
onSuccess={(otp) => {
// Proceed with verified operation
handleSensitiveOperation(otp);
}}
CancelButton={<Button variant="outline">Cancel</Button>}
/>
```
## Admin Features
### Super Admin Protection
For admin routes, use `AdminGuard`:
```tsx
import { AdminGuard } from '@kit/admin/components/admin-guard';
function AdminPage() {
return (
<div>
<h1>Admin Dashboard</h1>
{/* Admin content */}
</div>
);
}
// Wrap the page component
export default AdminGuard(AdminPage);
```
### Admin Service
Located at: `packages/features/admin/src/lib/server/services/admin.service.ts`
```typescript
// Admin service operations
// - Manage all accounts
// - Handle admin-level operations
// - Access system-wide data
```
### Checking Admin Status
```typescript
import { isSuperAdmin } from '@kit/admin';
function criticalAdminFeature() {
const isAdmin = await isSuperAdmin(client);
if (!isAdmin) {
throw new Error('Access denied: Admin privileges required');
}
// ...
}
```
## Error Handling & Logging
### Structured Logging
Use logger from `packages/shared/src/logger/logger.ts`:
```typescript
import { getLogger } from '@kit/shared/logger';
async function featureOperation() {
const logger = await getLogger();
const ctx = {
name: 'feature-operation',
userId: user.id,
accountId: account.id
};
try {
logger.info(ctx, 'Starting feature operation');
// Perform operation
const result = await performOperation();
logger.info({ ...ctx, result }, 'Feature operation completed');
return result;
} catch (error) {
logger.error({ ...ctx, error }, 'Feature operation failed');
throw error;
}
}
```
## Permission Patterns
### Team Permissions
```typescript
import { createTeamAccountsApi } from '@kit/team-accounts/api';
const api = createTeamAccountsApi(client);
// Check if user has specific permission on account
const canManageBilling = await api.hasPermission({
accountId,
userId,
permission: 'billing.manage'
});
if (!canManageBilling) {
throw new Error('Insufficient permissions');
}
```
### Account Ownership
```typescript
// Check if user is account owner (works for both personal and team accounts)
const isOwner = await client.rpc('is_account_owner', {
account_id: accountId
});
if (!isOwner) {
throw new Error('Only account owners can perform this action');
}
```

View File

@@ -49,7 +49,9 @@ export async function initializeI18nClient(
cookieMinutes: 60 * 24 * 365, // 1 year cookieMinutes: 60 * 24 * 365, // 1 year
cookieOptions: { cookieOptions: {
sameSite: 'lax', sameSite: 'lax',
secure: typeof window !== 'undefined' && window.location.protocol === 'https:', secure:
typeof window !== 'undefined' &&
window.location.protocol === 'https:',
path: '/', path: '/',
}, },
}, },

View File

@@ -1,66 +1 @@
# Email Service Instructions @AGENTS.md
This file contains guidance for working with the email service supporting Resend and Nodemailer.
## Basic Usage
```typescript
import { getMailer } from '@kit/mailers';
import { renderAccountDeleteEmail } from '@kit/email-templates';
async function sendSimpleEmail() {
// Get mailer instance
const mailer = await getMailer();
// Send simple email
await mailer.sendEmail({
to: 'user@example.com',
from: 'noreply@yourdomain.com',
subject: 'Welcome!',
html: '<h1>Welcome!</h1><p>Thank you for joining us.</p>',
});
}
async function sendComplexEmail() {
// Send with email template
const { html, subject } = await renderAccountDeleteEmail({
userDisplayName: user.name,
productName: 'My SaaS App',
});
await mailer.sendEmail({
to: user.email,
from: 'noreply@yourdomain.com',
subject,
html,
});
}
```
## Email Templates
Email templates are located in `@kit/email-templates` and return `{ html, subject }`:
```typescript
import {
renderAccountDeleteEmail,
renderWelcomeEmail,
renderPasswordResetEmail
} from '@kit/email-templates';
// Render template
const { html, subject } = await renderWelcomeEmail({
userDisplayName: 'John Doe',
loginUrl: 'https://app.com/login'
});
// Send rendered email
const mailer = await getMailer();
await mailer.sendEmail({
to: user.email,
from: 'welcome@yourdomain.com',
subject,
html,
});
```

View File

@@ -1,478 +1,58 @@
# Next.js Utilities Instructions # Next.js Utilities
This file contains instructions for working with Next.js utilities including server actions and route handlers. ## Quick Reference
| Function | Import | Purpose |
|----------|--------|---------|
| `enhanceAction` | `@kit/next/actions` | Server actions with auth + validation |
| `enhanceRouteHandler` | `@kit/next/routes` | API routes with auth + validation |
## Guidelines ## Guidelines
- Don't use Server Actions for data-fetching, use for mutations only - Server Actions for mutations only, not data-fetching
- Best Practice: Keep actions light, move business logic to ad-hoc services - Keep actions light - move business logic to services
- Authorization logic must be defined in RLS and DB, not Server Actions or application code (unless using the admin client, use sparinlgy!) - Authorization via RLS, not application code
- Do not expose sensitive data - Use `'use server'` at top of file
- Log async operations - Always validate with Zod schema
- Validate body with Zod
- Use 'use server' at the top of the file. No need for 'server only';
## Server Actions Implementation ## Skills
Always use `enhanceAction` from `@packages/next/src/actions/index.ts`. For detailed implementation patterns:
- `/server-action-builder` - Complete server action workflow
Define a schema: ## Server Action Pattern
```tsx
import { z } from 'zod';
// Define your schema in its own file
export const CreateNoteSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(1, 'Content is required'),
accountId: z.string().uuid('Invalid account ID'),
});
```
Then we define a service for crossing the network boundary:
```tsx
import { CreateNoteSchema } from '../schemas/notes.schemas.ts';
import * as z from 'zod';
export function createNotesService() {
return new NotesService();
}
class NotesService {
createNote(data: z.infer<CreateNoteSchema>) {
const client = getSupabaseServerClient();
const { data: note, error } = await client
.from('notes')
.insert({
title: data.title,
content: data.content,
account_id: data.accountId,
user_id: user.id,
})
.select()
.single();
if (error) {
throw error;
}
}
}
```
Finally, we use Server Actions for exposing POST handlers:
```typescript ```typescript
'use server'; 'use server';
import { enhanceAction } from '@kit/next/actions'; import { enhanceAction } from '@kit/next/actions';
import { createNotesService } from '../notes.service.ts';
export const createNoteAction = enhanceAction(
async function (data, user) {
// data is automatically validated against the schema
// user is automatically authenticated if auth: true
const service = createNotesService();
const logger = await getLogger();
logger.info({
userId: user.id,
}, `Creating note...`);
const { data: note, error } = await service.createNote(data);
if (error) {
logger.error({
error: error.message
}, `Error creating note`);
throw error;
}
logger.info({
noteId: note.id
}, `Note successfully created`);
return {
success: true,
note
};
},
{
auth: true, // Require authentication (true by default, can omit)
schema: CreateNoteSchema, // Validate input with Zod
},
);
```
### Server Action Examples
- Team billing: `@apps/web/app/home/[account]/billing/_lib/server/server-actions.ts`
- Personal settings: `@apps/web/app/home/(user)/settings/_lib/server/server-actions.ts`
### Server Action Options
```typescript
export const myAction = enhanceAction( export const myAction = enhanceAction(
async function (data, user) { async function (data, user) {
// data: validated input data // data: validated, user: authenticated
// user: authenticated user (if auth: true)
return { success: true }; return { success: true };
}, },
{ {
auth: true, // Require authentication (default: false) auth: true,
schema: MySchema, // Zod schema for validation (optional) schema: MySchema,
// Additional options available
}, },
); );
``` ```
## Route Handlers (API Routes) ## Route Handler Pattern
Use `enhanceRouteHandler` from `@packages/next/src/routes/index.ts`.
### Guidelines
- Use when data must be exposed to externally
- Use for receiving requests from external clients (such as webhooks)
- Can be used for fetching data to client side fetchers (such as React Query) if cannot use client-side Supabase queries
### Usage
```typescript ```typescript
import { enhanceRouteHandler } from '@kit/next/routes'; import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';
import { z } from 'zod';
// Define your schema
const CreateItemSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
});
export const POST = enhanceRouteHandler( export const POST = enhanceRouteHandler(
async function ({ body, user, request }) { async function ({ body, user, request }) {
// body is validated against schema
// user is available if auth: true
// request is the original NextRequest
const client = getSupabaseServerClient();
const { data, error } = await client
.from('items')
.insert({
name: body.name,
description: body.description,
user_id: user.id,
})
.select()
.single();
if (error) {
return NextResponse.json(
{ error: 'Failed to create item' },
{ status: 500 }
);
}
return NextResponse.json({ success: true, data });
},
{
auth: true, // Require authentication
schema: CreateItemSchema, // Validate request body
},
);
export const GET = enhanceRouteHandler(
async function ({ user, request }) {
const url = new URL(request.url);
const limit = url.searchParams.get('limit') || '10';
const client = getSupabaseServerClient();
const { data, error } = await client
.from('items')
.select('*')
.eq('user_id', user.id)
.limit(parseInt(limit));
if (error) {
return NextResponse.json(
{ error: 'Failed to fetch items' },
{ status: 500 }
);
}
return NextResponse.json({ data });
},
{
auth: true,
// No schema needed for GET requests
},
);
```
### Route Handler Options
```typescript
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// Handler function
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });
}, },
{ { auth: true, schema: MySchema },
auth: true, // Require authentication (default: false)
schema: MySchema, // Zod schema for body validation (optional)
// Additional options available
},
); );
``` ```
## Revalidation ## Revalidation
- Use `revalidatePath` for revalidating data after a migration. - Use `revalidatePath` after mutations
- Avoid calling `router.refresh()` or `router.push()` following a Server Action. Use `revalidatePath` and `redirect` from the server action instead. - Never use `router.refresh()` or `router.push()` after Server Actions
## Error Handling Patterns
### Server Actions with Error Handling
```typescript
export const createNoteAction = enhanceAction(
async function (data, user) {
const logger = await getLogger();
const ctx = { name: 'create-note', userId: user.id };
try {
logger.info(ctx, 'Creating note');
const client = getSupabaseServerClient();
const { data: note, error } = await client
.from('notes')
.insert({
title: data.title,
content: data.content,
user_id: user.id,
})
.select()
.single();
if (error) {
logger.error({ ...ctx, error }, 'Failed to create note');
throw error;
}
logger.info({ ...ctx, noteId: note.id }, 'Note created successfully');
return { success: true, note };
} catch (error) {
if (!isRedirectError(error)) {
logger.error({ ...ctx, error }, 'Create note action failed');
throw error;
}
}
},
{
auth: true,
schema: CreateNoteSchema,
},
);
```
### Server Action Redirects - Client Handling
When server actions call `redirect()`, it throws a special error that should NOT be treated as a failure:
```typescript
import { isRedirectError } from 'next/dist/client/components/redirect-error';
async function handleSubmit(formData: FormData) {
try {
await myServerAction(formData);
} catch (error) {
// Don't treat redirects as errors
if (!isRedirectError(error)) {
// Handle actual errors
toast.error('Something went wrong');
}
}
}
### Route Handler with Error Handling
```typescript
export const POST = enhanceRouteHandler(
async function ({ body, user }) {
const logger = await getLogger();
const ctx = { name: 'api-create-item', userId: user.id };
try {
logger.info(ctx, 'Processing API request');
// Process request
const result = await processRequest(body, user);
logger.info({ ...ctx, result }, 'API request successful');
return NextResponse.json({ success: true, data: result });
} catch (error) {
logger.error({ ...ctx, error }, 'API request failed');
if (error.message.includes('validation')) {
return NextResponse.json(
{ error: 'Invalid input data' },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
},
{
auth: true,
schema: CreateItemSchema,
},
);
```
## Client-Side Integration
### Using Server Actions in Components
```tsx
'use client';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from '@kit/ui/sonner';
import { Button } from '@kit/ui/button';
import { createNoteAction } from './actions';
import { CreateNoteSchema } from './schemas';
function CreateNoteForm() {
const [isPending, startTransition] = useTransition();
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
defaultValues: {
title: '',
content: '',
},
});
const onSubmit = (data) => {
startTransition(async () => {
try {
const result = await createNoteAction(data);
if (result.success) {
toast.success('Note created successfully!');
form.reset();
}
} catch (error) {
toast.error('Failed to create note');
console.error('Create note error:', error);
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields */}
<Button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Note'}
</Button>
</form>
);
}
```
NB: When using `redirect`, we must handle it using `isRedirectError` otherwise we display an error after the server action succeeds
### Using Route Handlers with Fetch
```typescript
'use client';
async function createItem(data: CreateItemInput) {
const response = await fetch('/api/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create item');
}
return response.json();
}
// Usage in component
const handleCreateItem = async (data) => {
try {
const result = await createItem(data);
toast.success('Item created successfully!');
return result;
} catch (error) {
toast.error('Failed to create item');
throw error;
}
};
```
### Authorization Checks
```typescript
export const deleteAccountAction = enhanceAction(
async function (data, user) {
const client = getSupabaseServerClient();
// Verify user owns the account
const { data: account, error } = await client
.from('accounts')
.select('id, primary_owner_user_id')
.eq('id', data.accountId)
.single();
if (error || !account) {
throw new Error('Account not found');
}
if (account.primary_owner_user_id !== user.id) {
throw new Error('Only account owners can delete accounts');
}
// Additional checks
const hasActiveSubscription = await client
.rpc('has_active_subscription', { account_id: data.accountId });
if (hasActiveSubscription) {
throw new Error('Cannot delete account with active subscription');
}
// Proceed with deletion
await deleteAccount(data.accountId);
return { success: true };
},
{
auth: true,
schema: DeleteAccountSchema,
},
);
```

View File

@@ -1,478 +1 @@
# Next.js Utilities Instructions @AGENTS.md
This file contains instructions for working with Next.js utilities including server actions and route handlers.
## Guidelines
- Don't use Server Actions for data-fetching, use for mutations only
- Best Practice: Keep actions light, move business logic to ad-hoc services
- Authorization logic must be defined in RLS and DB, not Server Actions or application code (unless using the admin client, use sparinlgy!)
- Do not expose sensitive data
- Log async operations
- Validate body with Zod
- Use 'use server' at the top of the file. No need for 'server only';
## Server Actions Implementation
Always use `enhanceAction` from `@packages/next/src/actions/index.ts`.
Define a schema:
```tsx
import { z } from 'zod';
// Define your schema in its own file
export const CreateNoteSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(1, 'Content is required'),
accountId: z.string().uuid('Invalid account ID'),
});
```
Then we define a service for crossing the network boundary:
```tsx
import { CreateNoteSchema } from '../schemas/notes.schemas.ts';
import * as z from 'zod';
export function createNotesService() {
return new NotesService();
}
class NotesService {
createNote(data: z.infer<CreateNoteSchema>) {
const client = getSupabaseServerClient();
const { data: note, error } = await client
.from('notes')
.insert({
title: data.title,
content: data.content,
account_id: data.accountId,
user_id: user.id,
})
.select()
.single();
if (error) {
throw error;
}
}
}
```
Finally, we use Server Actions for exposing POST handlers:
```typescript
'use server';
import { enhanceAction } from '@kit/next/actions';
import { createNotesService } from '../notes.service.ts';
export const createNoteAction = enhanceAction(
async function (data, user) {
// data is automatically validated against the schema
// user is automatically authenticated if auth: true
const service = createNotesService();
const logger = await getLogger();
logger.info({
userId: user.id,
}, `Creating note...`);
const { data: note, error } = await service.createNote(data);
if (error) {
logger.error({
error: error.message
}, `Error creating note`);
throw error;
}
logger.info({
noteId: note.id
}, `Note successfully created`);
return {
success: true,
note
};
},
{
auth: true, // Require authentication (true by default, can omit)
schema: CreateNoteSchema, // Validate input with Zod
},
);
```
### Server Action Examples
- Team billing: `@apps/web/app/home/[account]/billing/_lib/server/server-actions.ts`
- Personal settings: `@apps/web/app/home/(user)/settings/_lib/server/server-actions.ts`
### Server Action Options
```typescript
export const myAction = enhanceAction(
async function (data, user) {
// data: validated input data
// user: authenticated user (if auth: true)
return { success: true };
},
{
auth: true, // Require authentication (default: false)
schema: MySchema, // Zod schema for validation (optional)
// Additional options available
},
);
```
## Route Handlers (API Routes)
Use `enhanceRouteHandler` from `@packages/next/src/routes/index.ts`.
### Guidelines
- Use when data must be exposed to externally
- Use for receiving requests from external clients (such as webhooks)
- Can be used for fetching data to client side fetchers (such as React Query) if cannot use client-side Supabase queries
### Usage
```typescript
import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';
import { z } from 'zod';
// Define your schema
const CreateItemSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
});
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// body is validated against schema
// user is available if auth: true
// request is the original NextRequest
const client = getSupabaseServerClient();
const { data, error } = await client
.from('items')
.insert({
name: body.name,
description: body.description,
user_id: user.id,
})
.select()
.single();
if (error) {
return NextResponse.json(
{ error: 'Failed to create item' },
{ status: 500 }
);
}
return NextResponse.json({ success: true, data });
},
{
auth: true, // Require authentication
schema: CreateItemSchema, // Validate request body
},
);
export const GET = enhanceRouteHandler(
async function ({ user, request }) {
const url = new URL(request.url);
const limit = url.searchParams.get('limit') || '10';
const client = getSupabaseServerClient();
const { data, error } = await client
.from('items')
.select('*')
.eq('user_id', user.id)
.limit(parseInt(limit));
if (error) {
return NextResponse.json(
{ error: 'Failed to fetch items' },
{ status: 500 }
);
}
return NextResponse.json({ data });
},
{
auth: true,
// No schema needed for GET requests
},
);
```
### Route Handler Options
```typescript
export const POST = enhanceRouteHandler(
async function ({ body, user, request }) {
// Handler function
return NextResponse.json({ success: true });
},
{
auth: true, // Require authentication (default: false)
schema: MySchema, // Zod schema for body validation (optional)
// Additional options available
},
);
```
## Revalidation
- Use `revalidatePath` for revalidating data after a migration.
- Avoid calling `router.refresh()` or `router.push()` following a Server Action. Use `revalidatePath` and `redirect` from the server action instead.
## Error Handling Patterns
### Server Actions with Error Handling
```typescript
export const createNoteAction = enhanceAction(
async function (data, user) {
const logger = await getLogger();
const ctx = { name: 'create-note', userId: user.id };
try {
logger.info(ctx, 'Creating note');
const client = getSupabaseServerClient();
const { data: note, error } = await client
.from('notes')
.insert({
title: data.title,
content: data.content,
user_id: user.id,
})
.select()
.single();
if (error) {
logger.error({ ...ctx, error }, 'Failed to create note');
throw error;
}
logger.info({ ...ctx, noteId: note.id }, 'Note created successfully');
return { success: true, note };
} catch (error) {
if (!isRedirectError(error)) {
logger.error({ ...ctx, error }, 'Create note action failed');
throw error;
}
}
},
{
auth: true,
schema: CreateNoteSchema,
},
);
```
### Server Action Redirects - Client Handling
When server actions call `redirect()`, it throws a special error that should NOT be treated as a failure:
```typescript
import { isRedirectError } from 'next/dist/client/components/redirect-error';
async function handleSubmit(formData: FormData) {
try {
await myServerAction(formData);
} catch (error) {
// Don't treat redirects as errors
if (!isRedirectError(error)) {
// Handle actual errors
toast.error('Something went wrong');
}
}
}
### Route Handler with Error Handling
```typescript
export const POST = enhanceRouteHandler(
async function ({ body, user }) {
const logger = await getLogger();
const ctx = { name: 'api-create-item', userId: user.id };
try {
logger.info(ctx, 'Processing API request');
// Process request
const result = await processRequest(body, user);
logger.info({ ...ctx, result }, 'API request successful');
return NextResponse.json({ success: true, data: result });
} catch (error) {
logger.error({ ...ctx, error }, 'API request failed');
if (error.message.includes('validation')) {
return NextResponse.json(
{ error: 'Invalid input data' },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
},
{
auth: true,
schema: CreateItemSchema,
},
);
```
## Client-Side Integration
### Using Server Actions in Components
```tsx
'use client';
import { useTransition } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from '@kit/ui/sonner';
import { Button } from '@kit/ui/button';
import { createNoteAction } from './actions';
import { CreateNoteSchema } from './schemas';
function CreateNoteForm() {
const [isPending, startTransition] = useTransition();
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
defaultValues: {
title: '',
content: '',
},
});
const onSubmit = (data) => {
startTransition(async () => {
try {
const result = await createNoteAction(data);
if (result.success) {
toast.success('Note created successfully!');
form.reset();
}
} catch (error) {
toast.error('Failed to create note');
console.error('Create note error:', error);
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields */}
<Button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Note'}
</Button>
</form>
);
}
```
NB: When using `redirect`, we must handle it using `isRedirectError` otherwise we display an error after the server action succeeds
### Using Route Handlers with Fetch
```typescript
'use client';
async function createItem(data: CreateItemInput) {
const response = await fetch('/api/items', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create item');
}
return response.json();
}
// Usage in component
const handleCreateItem = async (data) => {
try {
const result = await createItem(data);
toast.success('Item created successfully!');
return result;
} catch (error) {
toast.error('Failed to create item');
throw error;
}
};
```
### Authorization Checks
```typescript
export const deleteAccountAction = enhanceAction(
async function (data, user) {
const client = getSupabaseServerClient();
// Verify user owns the account
const { data: account, error } = await client
.from('accounts')
.select('id, primary_owner_user_id')
.eq('id', data.accountId)
.single();
if (error || !account) {
throw new Error('Account not found');
}
if (account.primary_owner_user_id !== user.id) {
throw new Error('Only account owners can delete accounts');
}
// Additional checks
const hasActiveSubscription = await client
.rpc('has_active_subscription', { account_id: data.accountId });
if (hasActiveSubscription) {
throw new Error('Cannot delete account with active subscription');
}
// Proceed with deletion
await deleteAccount(data.accountId);
return { success: true };
},
{
auth: true,
schema: DeleteAccountSchema,
},
);
```

View File

@@ -1,684 +1 @@
# FeaturePolicy API - Registry-Based Policy System @AGENTS.md
A unified, registry-based foundation for implementing business rules across all Makerkit features.
## Overview
The FeaturePolicy API provides:
- **Registry-based architecture** - centralized policy management with IDs
- **Configuration support** - policies can accept typed configuration objects
- **Stage-aware evaluation** - policies can be filtered by execution stage
- **Immutable contexts** for safe policy evaluation
- **Customer extensibility** - easy to add custom policies without forking
## Quick Start
### 1. Register Policies
```typescript
import { z } from 'zod';
import { allow, createPolicyRegistry, definePolicy, deny } from '@kit/policies';
const registry = createPolicyRegistry();
// Register a basic policy
registry.registerPolicy(
definePolicy({
id: 'email-validation',
stages: ['preliminary', 'submission'],
evaluate: async (context) => {
if (!context.userEmail?.includes('@')) {
return deny({
code: 'INVALID_EMAIL_FORMAT',
message: 'Invalid email format',
remediation: 'Please provide a valid email address',
});
}
return allow();
},
}),
);
// Register a configurable policy
registry.registerPolicy(
definePolicy({
id: 'max-invitations',
configSchema: z.object({
maxInvitations: z.number().positive(),
}),
evaluate: async (context, config = { maxInvitations: 5 }) => {
if (context.invitations.length > config.maxInvitations) {
return deny({
code: 'MAX_INVITATIONS_EXCEEDED',
message: `Cannot invite more than ${config.maxInvitations} members`,
remediation: `Reduce invitations to ${config.maxInvitations} or fewer`,
});
}
return allow();
},
}),
);
```
### 2. Use Policies from Registry
```typescript
import {
createPoliciesFromRegistry,
createPolicyEvaluator,
createPolicyRegistry,
} from '@kit/policies';
const registry = createPolicyRegistry();
// Load policies from registry
const policies = await createPoliciesFromRegistry(registry, [
'email-validation',
'subscription-required',
['max-invitations', { maxInvitations: 5 }], // with configuration
]);
const evaluator = createPolicyEvaluator();
const result = await evaluator.evaluatePolicies(policies, context, 'ALL');
if (!result.allowed) {
console.log('Failed reasons:', result.reasons);
}
```
### 3. Group Policies with Complex Logic
```typescript
// Basic group example
const preliminaryGroup = {
operator: 'ALL' as const,
policies: [emailValidationPolicy, authenticationPolicy],
};
const billingGroup = {
operator: 'ANY' as const,
policies: [subscriptionPolicy, trialPolicy],
};
// Evaluate groups in sequence
const result = await evaluator.evaluateGroups(
[preliminaryGroup, billingGroup],
context,
);
```
## Complex Group Flows
### Real-World Multi-Stage Team Invitation Flow
```typescript
import { createPolicy, createPolicyEvaluator } from '@kit/policies';
// Complex business logic: (Authentication AND Email Validation) AND (Subscription OR Trial) AND Billing Limits
async function validateTeamInvitation(context: InvitationContext) {
const evaluator = createPolicyEvaluator();
// Stage 1: Authentication Requirements (ALL must pass)
const authenticationGroup = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.userId
? allow({ step: 'authenticated' })
: deny('Authentication required'),
),
createPolicy(async (ctx) =>
ctx.email.includes('@')
? allow({ step: 'email-valid' })
: deny('Valid email required'),
),
createPolicy(async (ctx) =>
ctx.permissions.includes('invite')
? allow({ step: 'permissions' })
: deny('Insufficient permissions'),
),
],
};
// Stage 2: Subscription Validation (ANY sufficient - flexible billing)
const subscriptionGroup = {
operator: 'ANY' as const,
policies: [
createPolicy(async (ctx) =>
ctx.subscription?.active && ctx.subscription.plan === 'enterprise'
? allow({ billing: 'enterprise' })
: deny('Enterprise subscription required'),
),
createPolicy(async (ctx) =>
ctx.subscription?.active && ctx.subscription.plan === 'pro'
? allow({ billing: 'pro' })
: deny('Pro subscription required'),
),
createPolicy(async (ctx) =>
ctx.trial?.active && ctx.trial.daysRemaining > 0
? allow({ billing: 'trial', daysLeft: ctx.trial.daysRemaining })
: deny('Active trial required'),
),
],
};
// Stage 3: Final Constraints (ALL must pass)
const constraintsGroup = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.team.memberCount < ctx.subscription?.maxMembers
? allow({ constraint: 'member-limit' })
: deny('Member limit exceeded'),
),
createPolicy(async (ctx) =>
ctx.invitations.length <= 10
? allow({ constraint: 'batch-size' })
: deny('Cannot invite more than 10 members at once'),
),
],
};
// Execute all groups sequentially - ALL groups must pass
const result = await evaluator.evaluateGroups(
[authenticationGroup, subscriptionGroup, constraintsGroup],
context,
);
return {
allowed: result.allowed,
reasons: result.reasons,
metadata: {
stagesCompleted: result.results.length,
authenticationPassed: result.results.some(
(r) => r.metadata?.step === 'authenticated',
),
billingType: result.results.find((r) => r.metadata?.billing)?.metadata
?.billing,
constraintsChecked: result.results.some((r) => r.metadata?.constraint),
},
};
}
```
### Middleware-Style Policy Chain
```typescript
// Simulate middleware pattern: Auth → Rate Limiting → Business Logic
async function processApiRequest(context: ApiContext) {
const evaluator = createPoliciesEvaluator();
// Layer 1: Security (ALL required)
const securityLayer = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.apiKey && ctx.apiKey.length > 0
? allow({ security: 'api-key-valid' })
: deny('API key required'),
),
createPolicy(async (ctx) =>
ctx.rateLimitRemaining > 0
? allow({ security: 'rate-limit-ok' })
: deny('Rate limit exceeded'),
),
createPolicy(async (ctx) =>
!ctx.blacklisted
? allow({ security: 'not-blacklisted' })
: deny('Client is blacklisted'),
),
],
};
// Layer 2: Authorization (ANY sufficient - flexible access levels)
const authorizationLayer = {
operator: 'ANY' as const,
policies: [
createPolicy(async (ctx) =>
ctx.user.role === 'admin'
? allow({ access: 'admin' })
: deny('Admin access denied'),
),
createPolicy(async (ctx) =>
ctx.user.permissions.includes(ctx.requestedResource)
? allow({ access: 'resource-specific' })
: deny('Resource access denied'),
),
createPolicy(async (ctx) =>
ctx.user.subscription?.includes('api-access')
? allow({ access: 'subscription-based' })
: deny('Subscription access denied'),
),
],
};
// Layer 3: Business Rules (ALL required)
const businessLayer = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.request.size <= ctx.maxRequestSize
? allow({ business: 'size-valid' })
: deny('Request too large'),
),
createPolicy(async (ctx) =>
ctx.user.dailyQuota > ctx.user.dailyUsage
? allow({ business: 'quota-available' })
: deny('Daily quota exceeded'),
),
],
};
return evaluator.evaluateGroups(
[securityLayer, authorizationLayer, businessLayer],
context,
);
}
```
### Complex Nested Logic with Short-Circuiting
```typescript
// Complex scenario: (Premium User OR (Basic User AND Low Usage)) AND Security Checks
async function validateFeatureAccess(context: FeatureContext) {
const evaluator = createPoliciesEvaluator();
// Group 1: User Tier Logic - demonstrates complex OR conditions
const userTierGroup = {
operator: 'ANY' as const,
policies: [
// Premium users get immediate access
createPolicy(async (ctx) =>
ctx.user.plan === 'premium'
? allow({ tier: 'premium', reason: 'premium-user' })
: deny('Not premium user'),
),
// Enterprise users get immediate access
createPolicy(async (ctx) =>
ctx.user.plan === 'enterprise'
? allow({ tier: 'enterprise', reason: 'enterprise-user' })
: deny('Not enterprise user'),
),
// Basic users need additional validation (sub-group logic)
createPolicy(async (ctx) => {
if (ctx.user.plan !== 'basic') {
return deny('Not basic user');
}
// Simulate nested AND logic for basic users
const basicUserRequirements = [
ctx.user.monthlyUsage < 1000,
ctx.user.accountAge > 30, // days
!ctx.user.hasViolations,
];
const allBasicRequirementsMet = basicUserRequirements.every(
(req) => req,
);
return allBasicRequirementsMet
? allow({ tier: 'basic', reason: 'low-usage-basic-user' })
: deny('Basic user requirements not met');
}),
],
};
// Group 2: Security Requirements (ALL must pass)
const securityGroup = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.user.emailVerified
? allow({ security: 'email-verified' })
: deny('Email verification required'),
),
createPolicy(async (ctx) =>
ctx.user.twoFactorEnabled || ctx.user.plan === 'basic'
? allow({ security: '2fa-compliant' })
: deny('Two-factor authentication required for premium plans'),
),
createPolicy(async (ctx) =>
!ctx.user.suspiciousActivity
? allow({ security: 'activity-clean' })
: deny('Suspicious activity detected'),
),
],
};
return evaluator.evaluateGroups([userTierGroup, securityGroup], context);
}
```
### Dynamic Policy Composition
```typescript
// Dynamically compose policies based on context
async function createContextAwarePolicyFlow(context: DynamicContext) {
const evaluator = createPoliciesEvaluator();
const groups = [];
// Always include base security
const baseSecurityGroup = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.isAuthenticated ? allow() : deny('Authentication required'),
),
],
};
groups.push(baseSecurityGroup);
// Add user-type specific policies
if (context.user.type === 'admin') {
const adminGroup = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.user.adminLevel >= ctx.requiredAdminLevel
? allow({ admin: 'level-sufficient' })
: deny('Insufficient admin level'),
),
createPolicy(async (ctx) =>
ctx.user.lastLogin > Date.now() - 24 * 60 * 60 * 1000 // 24 hours
? allow({ admin: 'recent-login' })
: deny('Admin must have logged in within 24 hours'),
),
],
};
groups.push(adminGroup);
}
// Add feature-specific policies based on requested feature
if (context.feature.requiresBilling) {
const billingGroup = {
operator: 'ANY' as const,
policies: [
createPolicy(async (ctx) =>
ctx.subscription?.active
? allow({ billing: 'subscription' })
: deny('Active subscription required'),
),
createPolicy(async (ctx) =>
ctx.credits && ctx.credits > ctx.feature.creditCost
? allow({ billing: 'credits' })
: deny('Insufficient credits'),
),
],
};
groups.push(billingGroup);
}
// Add rate limiting for high-impact features
if (context.feature.highImpact) {
const rateLimitGroup = {
operator: 'ALL' as const,
policies: [
createPolicy(async (ctx) =>
ctx.rateLimit.remaining > 0
? allow({ rateLimit: 'within-limits' })
: deny('Rate limit exceeded for high-impact features'),
),
],
};
groups.push(rateLimitGroup);
}
return evaluator.evaluateGroups(groups, context);
}
```
### Performance-Optimized Large Group Evaluation
```typescript
// Handle large numbers of policies efficiently
async function validateComplexBusinessRules(context: BusinessContext) {
const evaluator = createPoliciesEvaluator({ maxCacheSize: 200 });
// Group policies by evaluation cost and criticality
const criticalFastGroup = {
operator: 'ALL' as const,
policies: [
// Fast critical checks first
createPolicy(async (ctx) =>
ctx.isActive ? allow() : deny('Account inactive'),
),
createPolicy(async (ctx) =>
ctx.hasPermission ? allow() : deny('No permission'),
),
createPolicy(async (ctx) =>
!ctx.isBlocked ? allow() : deny('Account blocked'),
),
],
};
const businessLogicGroup = {
operator: 'ANY' as const,
policies: [
// Complex business rules
createPolicy(async (ctx) => {
// Simulate complex calculation
const score = await calculateRiskScore(ctx);
return score < 0.8
? allow({ risk: 'low' })
: deny('High risk detected');
}),
createPolicy(async (ctx) => {
// Simulate external API call
const verification = await verifyWithThirdParty(ctx);
return verification.success
? allow({ external: 'verified' })
: deny('External verification failed');
}),
],
};
const finalValidationGroup = {
operator: 'ALL' as const,
policies: [
// Final checks after complex logic
createPolicy(async (ctx) =>
ctx.complianceCheck ? allow() : deny('Compliance check failed'),
),
],
};
// Use staged evaluation for better performance
const startTime = Date.now();
const result = await evaluator.evaluateGroups(
[
criticalFastGroup, // Fast critical checks first
businessLogicGroup, // Complex logic only if critical checks pass
finalValidationGroup, // Final validation
],
context,
);
const evaluationTime = Date.now() - startTime;
return {
...result,
performance: {
evaluationTimeMs: evaluationTime,
groupsEvaluated: result.results.length > 0 ? 3 : 1,
},
};
}
// Helper functions for complex examples
async function calculateRiskScore(context: any): Promise<number> {
// Simulate complex risk calculation
await new Promise((resolve) => setTimeout(resolve, 10));
return Math.random();
}
async function verifyWithThirdParty(
context: any,
): Promise<{ success: boolean }> {
// Simulate external API call
await new Promise((resolve) => setTimeout(resolve, 5));
return { success: Math.random() > 0.2 };
}
```
## Advanced Usage
### Configurable Policies
```typescript
// Create policy factories for configuration
const createMaxInvitationsPolicy = (maxInvitations: number) =>
createPolicy(async (context) => {
if (context.invitations.length > maxInvitations) {
return deny({
code: 'MAX_INVITATIONS_EXCEEDED',
message: `Cannot invite more than ${maxInvitations} members`,
remediation: `Reduce invitations to ${maxInvitations} or fewer`,
});
}
return allow();
});
// Use with different configurations
const strictPolicy = createMaxInvitationsPolicy(1);
const standardPolicy = createMaxInvitationsPolicy(5);
const permissivePolicy = createMaxInvitationsPolicy(25);
```
### Feature-Specific evaluators
```typescript
// Create feature-specific evaluator with preset configurations
export function createInvitationevaluator(
preset: 'strict' | 'standard' | 'permissive',
) {
const configs = {
strict: { maxInvitationsPerBatch: 1 },
standard: { maxInvitationsPerBatch: 5 },
permissive: { maxInvitationsPerBatch: 25 },
};
const config = configs[preset];
return {
async validateInvitations(context: InvitationContext) {
const policies = [
emailValidationPolicy,
createMaxInvitationsPolicy(config.maxInvitationsPerBatch),
subscriptionRequiredPolicy,
paddleBillingPolicy,
];
const evaluator = createPoliciesEvaluator();
return evaluator.evaluatePolicies(policies, context, 'ALL');
},
};
}
// Usage
const evaluator = createInvitationevaluator('standard');
const result = await evaluator.validateInvitations(context);
```
### Error Handling
```typescript
const result = await evaluator.evaluate();
if (!result.allowed) {
result.reasons.forEach((reason) => {
console.log(`Policy ${reason.policyId} failed:`);
console.log(` Code: ${reason.code}`);
console.log(` Message: ${reason.message}`);
if (reason.remediation) {
console.log(` Fix: ${reason.remediation}`);
}
});
}
```
### 1. Register Complex Policy with Configuration
```typescript
import { createPolicyRegistry, definePolicy } from '@kit/policies';
const registry = createPolicyRegistry();
const customConfigurablePolicy = definePolicy({
id: 'custom-domain-check',
configSchema: z.object({
allowedDomains: z.array(z.string()),
strictMode: z.boolean(),
}),
evaluate: async (context, config) => {
const emailDomain = context.userEmail?.split('@')[1];
if (config?.strictMode && !config.allowedDomains.includes(emailDomain)) {
return deny({
code: 'DOMAIN_NOT_ALLOWED',
message: `Email domain ${emailDomain} is not in the allowed list`,
remediation: 'Use an email from an approved domain',
});
}
return allow();
},
});
registry.registerPolicy(customConfigurablePolicy);
```
## Key Concepts
### Group Operators
- **`ALL` (AND logic)**: All policies in the group must pass
- **Short-circuits on first failure** for performance
- Use for mandatory requirements where every condition must be met
- Example: Authentication AND permissions AND rate limiting
- **`ANY` (OR logic)**: At least one policy in the group must pass
- **Short-circuits on first success** for performance
- Use for flexible requirements where multiple options are acceptable
- Example: Premium subscription OR trial access OR admin override
### Group Evaluation Flow
1. **Sequential Group Processing**: Groups are evaluated in order
2. **All Groups Must Pass**: If any group fails, entire evaluation fails
3. **Short-Circuiting**: Stops on first group failure for performance
4. **Metadata Preservation**: All policy results and metadata are collected
### Performance Considerations
- **Order groups by criticality**: Put fast, critical checks first
- **Use caching**: Configure `maxCacheSize` for frequently used policies
- **Group by evaluation cost**: Separate expensive operations
- **Monitor evaluation time**: Track performance for optimization
## Stage-Aware Evaluation
Policies can be filtered by execution stage. This is useful for running a subset of policies depending on the situation:
```typescript
// Only run preliminary checks
const prelimResult = await evaluator.evaluate(
registry,
context,
'ALL',
'preliminary',
);
// Run submission validation
const submitResult = await evaluator.evaluate(
registry,
context,
'ALL',
'submission',
);
// Run all applicable policies
const fullResult = await evaluator.evaluate(registry, context, 'ALL');
```

View File

@@ -1,122 +1,51 @@
# Database & Authentication Instructions # Database & Authentication
This file contains instructions for working with Supabase, database security, and authentication. ## Skills
## Schemas and Migrations ⚠️ For database work:
- `/postgres-expert` - Schemas, RLS, migrations
**Critical Understanding**: Schema files are NOT automatically applied to the database! ## Client Usage
- **Schemas** (`supabase/schemas/`) represent the desired database state (source of truth) ### Server Components (Preferred)
- **Migrations** (`supabase/migrations/`) are the actual SQL commands that modify the database
### The Required Workflow ```typescript
import { getSupabaseServerClient } from '@kit/supabase/server-client';
1. **Edit schema file** (e.g., `supabase/schemas/18-projects.sql`) const client = getSupabaseServerClient();
2. **Generate migration**: `pnpm --filter web supabase:db:diff -f migration_name` const { data } = await client.from('table').select('*');
- This compares your schema against the current database and creates a migration // RLS automatically enforced
3. **Apply migration**: `pnpm --filter web supabase migrations up`
- This actually executes the SQL changes in the database
**⚠️ CRITICAL**: Editing a schema file alone does NOTHING to your database. You MUST generate and apply a migration for changes to take effect. Schema files are templates - migrations are the actual database operations.
## Database Security Guidelines ⚠️
**Critical Security Guidelines - Read Carefully!**
### Database Security Fundamentals
- **Always enable RLS** on new tables unless explicitly instructed otherwise
- **NEVER use SECURITY DEFINER functions** without explicit access controls - they bypass RLS entirely
- **Always use security_invoker=true for views** to maintain proper access control
- **Storage buckets MUST validate access** using account_id in the path structure. See `apps/web/supabase/schemas/16-storage.sql` for proper implementation.
- **Use locks if required**: Database locks prevent race conditions and timing attacks in concurrent operations. Make sure to take these into account for all database operations.
### Security Definer Function - Dangerous Pattern ❌
```sql
-- NEVER DO THIS - Allows any authenticated user to call function
CREATE OR REPLACE FUNCTION public.dangerous_function()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER AS $
BEGIN
-- This bypasses all RLS policies!
DELETE FROM sensitive_table; -- Anyone can call this!
END;
$;
GRANT EXECUTE ON FUNCTION public.dangerous_function() TO authenticated;
``` ```
### Security Definer Function - Safe Pattern ✅ ### Client Components
```sql ```typescript
-- ONLY use SECURITY DEFINER with explicit access validation 'use client';
CREATE OR REPLACE FUNCTION public.safe_admin_function(target_account_id uuid) import { useSupabase } from '@kit/supabase/hooks/use-supabase';
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = '' AS $
BEGIN
-- MUST validate caller has permission FIRST
IF NOT public.is_account_owner(target_account_id) THEN
RAISE EXCEPTION 'Access denied: insufficient permissions';
END IF;
-- Now safe to proceed with elevated privileges const supabase = useSupabase();
-- Your admin operation here
END;
$;
``` ```
Only grant critical functions to `service_role`: ### Admin Client (Use Sparingly)
```sql ```typescript
grant execute on public.dangerous_function to service_role; import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
// CRITICAL: Bypasses RLS - validate manually!
const adminClient = getSupabaseServerAdminClient();
``` ```
## Existing Helper Functions - Use These! 📚 ## Existing Helper Functions
**DO NOT recreate these functions - they already exist:**
```sql ```sql
-- Account Access Control public.has_role_on_account(account_id, role?)
public.has_role_on_account(account_id, role?) -- Check team membership public.has_permission(user_id, account_id, permission)
public.has_permission(user_id, account_id, permission) -- Check permissions public.is_account_owner(account_id)
public.is_account_owner(account_id) -- Verify ownership public.has_active_subscription(account_id)
public.has_active_subscription(account_id) -- Subscription status public.is_team_member(account_id, user_id)
public.is_team_member(account_id, user_id) -- Direct membership check public.is_super_admin()
public.can_action_account_member(target_account_id, target_user_id) -- Member action rights
-- Administrative Functions
public.is_super_admin() -- Super admin check
public.is_aal2() -- MFA verification
public.is_mfa_compliant() -- MFA compliance
-- Configuration
public.is_set(field_name) -- Feature flag checks
``` ```
Always check `apps/web/supabase/schemas/` before creating new functions!
## RLS Policy Best Practices ✅
```sql
-- Proper RLS using existing helper functions
CREATE POLICY "notes_read" ON public.notes FOR SELECT
TO authenticated USING (
account_id = (select auth.uid()) OR
public.has_role_on_account(account_id)
);
-- For operations requiring specific permissions
CREATE POLICY "notes_manage" ON public.notes FOR ALL
TO authenticated USING (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
```
- **Never modify database.types.ts**: Instead, use the Supabase CLI using our package.json scripts to re-generate the types after resetting the DB
## Type Generation ## Type Generation
```typescript ```typescript
@@ -125,191 +54,21 @@ import { Tables } from '@kit/supabase/database';
type Account = Tables<'accounts'>; type Account = Tables<'accounts'>;
``` ```
Always prefer inferring types from generated Database types. Never modify `database.types.ts` - regenerate with `pnpm supabase:web:typegen`.
## Client Usage Patterns ## Authentication
### Server Components (Preferred)
```typescript
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function NotesPage() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
if (error) return <ErrorMessage error={error} />;
return <NotesList notes={data} />;
}
```
### Client Components
```typescript
'use client';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
function InteractiveNotes() {
const supabase = useSupabase();
// Use with React Query for optimal data fetching
}
```
### Admin Client (Use with Extreme Caution) ⚠️
```typescript
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
async function adminFunction() {
const adminClient = getSupabaseServerAdminClient();
// CRITICAL: Manual authorization required - bypasses RLS!
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient.from('table').select('*');
}
```
## Authentication Patterns
### Multi-Factor Authentication
```typescript
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
const requiresMultiFactorAuthentication =
await checkRequiresMultiFactorAuthentication(supabase);
if (requiresMultiFactorAuthentication) {
// Redirect to MFA page
}
```
### User Requirements
```typescript ```typescript
import { requireUser } from '@kit/supabase/require-user'; import { requireUser } from '@kit/supabase/require-user';
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
const client = getSupabaseServerClient(); const user = await requireUser(client);
const user = await requireUser(client, { verifyMfa: false }); const requiresMfa = await checkRequiresMultiFactorAuthentication(client);
``` ```
## Storage Security ## Security Guidelines
Storage buckets must validate access using account_id in the path structure: - Standard client: Trust RLS
- Admin client: Validate everything manually
```sql - Always add indexes for foreign keys
-- RLS policies for storage bucket account_image - Storage paths must include account_id
create policy account_image on storage.objects for all using (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
)
with check (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_permission(
auth.uid(),
kit.get_storage_filename_as_uuid(name),
'settings.manage'
)
)
);
```
## Common Database Operations
### Creating Tables with RLS
```sql
-- Create table
create table if not exists public.notes (
id uuid unique not null default extensions.uuid_generate_v4(),
account_id uuid references public.accounts(id) on delete cascade not null,
title varchar(255) not null,
content text,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
primary key (id)
);
-- Enable RLS
alter table "public"."notes" enable row level security;
-- Grant permissions
grant select, insert, update, delete on table public.notes to authenticated;
-- Create RLS policies
create policy "notes_read" on public.notes for select
to authenticated using (
account_id = (select auth.uid()) or
public.has_role_on_account(account_id)
);
create policy "notes_write" on public.notes for insert
to authenticated with check (
account_id = (select auth.uid()) or
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
```
### Indexes for Performance
```sql
-- Create indexes for common queries
create index if not exists ix_notes_account_id on public.notes (account_id);
create index if not exists ix_notes_created_at on public.notes (created_at);
```
## Error Handling
```typescript
import { getLogger } from '@kit/shared/logger';
async function databaseOperation() {
const logger = await getLogger();
const ctx = { name: 'database-operation', accountId: 'account-123' };
try {
logger.info(ctx, 'Starting database operation');
const result = await client.from('table').select('*');
if (result.error) {
logger.error({ ...ctx, error: result.error }, 'Database query failed');
throw result.error;
}
return result.data;
} catch (error) {
logger.error({ ...ctx, error }, 'Database operation failed');
throw error;
}
}
```
## Migration Best Practices
1. Always test migrations locally first
2. Use transactions for complex operations
3. Add proper indexes for new columns
4. Update RLS policies when adding new tables
5. Generate TypeScript types after schema changes
6. Take into account constraints
7. Do not add breaking changes that would distrupt the DB to new migrations
## Common Gotchas
1. **RLS bypass**: Admin client bypasses all RLS - validate manually
2. **Missing indexes**: Always add indexes for foreign keys and commonly queried columns
3. **Security definer functions**: Only use with explicit permission checks
4. **Storage paths**: Must include account_id for proper access control
5. **Type safety**: Always regenerate types after schema changes

View File

@@ -1,317 +1 @@
# Database & Authentication Instructions @AGENTS.md
This file contains instructions for working with Supabase, database security, and authentication.
## Schemas and Migrations ⚠️
**Critical Understanding**: Schema files are NOT automatically applied to the database!
- **Schemas** (`supabase/schemas/`) represent the desired database state (source of truth)
- **Migrations** (`supabase/migrations/`) are the actual SQL commands that modify the database
### The Required Workflow
1. **Edit schema file** (e.g., `supabase/schemas/18-projects.sql`)
2. **Generate migration**:
- Either copy content from schema to migration with `pnpm --filter web supabase migrations new my-feature` and `cp apps/web/supabase/schemas/18-my-new-feature.sql apps/web/supabase/migrations/$(ls -t apps/web/supabase/migrations/ | head -n1)`. Ideal for **new** tables, enums, etc.
- or use `pnpm --filter web supabase:db:diff -f migration_name` (ideal when modifying existing entities)
- This compares your schema against the current database and creates a migration
3. **Apply migration**: `pnpm --filter web supabase migrations up`
- This actually executes the SQL changes in the database
**⚠️ CRITICAL**: Editing a schema file alone does NOTHING to your database. You MUST generate and apply a migration for changes to take effect. Schema files are templates - migrations are the actual database operations.
## Database Security Guidelines ⚠️
**Critical Security Guidelines - Read Carefully!**
### Database Security Fundamentals
- **Always enable RLS** on new tables unless explicitly instructed otherwise
- **NEVER use SECURITY DEFINER functions** without explicit access controls - they bypass RLS entirely
- **Always use security_invoker=true for views** to maintain proper access control
- **Storage buckets MUST validate access** using account_id in the path structure. See `apps/web/supabase/schemas/16-storage.sql` for proper implementation.
- **Use locks if required**: Database locks prevent race conditions and timing attacks in concurrent operations. Make sure to take these into account for all database operations.
### Security Definer Function - Dangerous Pattern ❌
```sql
-- NEVER DO THIS - Allows any authenticated user to call function
CREATE OR REPLACE FUNCTION public.dangerous_function()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER AS $
BEGIN
-- This bypasses all RLS policies!
DELETE FROM sensitive_table; -- Anyone can call this!
END;
$;
GRANT EXECUTE ON FUNCTION public.dangerous_function() TO authenticated;
```
### Security Definer Function - Safe Pattern ✅
```sql
-- ONLY use SECURITY DEFINER with explicit access validation
CREATE OR REPLACE FUNCTION public.safe_admin_function(target_account_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = '' AS $
BEGIN
-- MUST validate caller has permission FIRST
IF NOT public.is_account_owner(target_account_id) THEN
RAISE EXCEPTION 'Access denied: insufficient permissions';
END IF;
-- Now safe to proceed with elevated privileges
-- Your admin operation here
END;
$;
```
Only grant critical functions to `service_role`:
```sql
grant execute on public.dangerous_function to service_role;
```
## Existing Helper Functions - Use These! 📚
**DO NOT recreate these functions - they already exist:**
```sql
-- Account Access Control
public.has_role_on_account(account_id, role?) -- Check team membership
public.has_permission(user_id, account_id, permission) -- Check permissions
public.is_account_owner(account_id) -- Verify ownership
public.has_active_subscription(account_id) -- Subscription status
public.is_team_member(account_id, user_id) -- Direct membership check
public.can_action_account_member(target_account_id, target_user_id) -- Member action rights
-- Administrative Functions
public.is_super_admin() -- Super admin check
public.is_aal2() -- MFA verification
public.is_mfa_compliant() -- MFA compliance
-- Configuration
public.is_set(field_name) -- Feature flag checks
```
Always check `apps/web/supabase/schemas/` before creating new functions!
## RLS Policy Best Practices ✅
```sql
-- Proper RLS using existing helper functions
CREATE POLICY "notes_read" ON public.notes FOR SELECT
TO authenticated USING (
account_id = (select auth.uid()) OR
public.has_role_on_account(account_id)
);
-- For operations requiring specific permissions
CREATE POLICY "notes_manage" ON public.notes FOR ALL
TO authenticated USING (
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
```
- **Never modify database.types.ts**: Instead, use the Supabase CLI using our package.json scripts to re-generate the types after resetting the DB
## Type Generation
```typescript
import { Tables } from '@kit/supabase/database';
type Account = Tables<'accounts'>;
```
Always prefer inferring types from generated Database types.
## Client Usage Patterns
### Server Components (Preferred)
```typescript
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function NotesPage() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
if (error) return <ErrorMessage error={error} />;
return <NotesList notes={data} />;
}
```
### Client Components
```typescript
'use client';
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
function InteractiveNotes() {
const supabase = useSupabase();
// Use with React Query for optimal data fetching
}
```
### Admin Client (Use with Extreme Caution) ⚠️
```typescript
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
async function adminFunction() {
const adminClient = getSupabaseServerAdminClient();
// CRITICAL: Manual authorization required - bypasses RLS!
const currentUser = await getCurrentUser();
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient.from('table').select('*');
}
```
## Authentication Patterns
### Multi-Factor Authentication
```typescript
import { checkRequiresMultiFactorAuthentication } from '@kit/supabase/check-requires-mfa';
const requiresMultiFactorAuthentication =
await checkRequiresMultiFactorAuthentication(supabase);
if (requiresMultiFactorAuthentication) {
// Redirect to MFA page
}
```
### User Requirements
```typescript
import { requireUser } from '@kit/supabase/require-user';
const client = getSupabaseServerClient();
const user = await requireUser(client, { verifyMfa: false });
```
## Storage Security
Storage buckets must validate access using account_id in the path structure:
```sql
-- RLS policies for storage bucket account_image
create policy account_image on storage.objects for all using (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_role_on_account(kit.get_storage_filename_as_uuid(name))
)
)
with check (
bucket_id = 'account_image'
and (
kit.get_storage_filename_as_uuid(name) = auth.uid()
or public.has_permission(
auth.uid(),
kit.get_storage_filename_as_uuid(name),
'settings.manage'
)
)
);
```
## Common Database Operations
### Creating Tables with RLS
```sql
-- Create table
create table if not exists public.notes (
id uuid unique not null default extensions.uuid_generate_v4(),
account_id uuid references public.accounts(id) on delete cascade not null,
title varchar(255) not null,
content text,
created_at timestamp with time zone default now(),
updated_at timestamp with time zone default now(),
primary key (id)
);
-- Enable RLS
alter table "public"."notes" enable row level security;
-- Grant permissions
grant select, insert, update, delete on table public.notes to authenticated;
-- Create RLS policies
create policy "notes_read" on public.notes for select
to authenticated using (
account_id = (select auth.uid()) or
public.has_role_on_account(account_id)
);
create policy "notes_write" on public.notes for insert
to authenticated with check (
account_id = (select auth.uid()) or
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
);
```
### Indexes for Performance
```sql
-- Create indexes for common queries
create index if not exists ix_notes_account_id on public.notes (account_id);
create index if not exists ix_notes_created_at on public.notes (created_at);
```
## Error Handling
```typescript
import { getLogger } from '@kit/shared/logger';
async function databaseOperation() {
const logger = await getLogger();
const ctx = { name: 'database-operation', accountId: 'account-123' };
try {
logger.info(ctx, 'Starting database operation');
const result = await client.from('table').select('*');
if (result.error) {
logger.error({ ...ctx, error: result.error }, 'Database query failed');
throw result.error;
}
return result.data;
} catch (error) {
logger.error({ ...ctx, error }, 'Database operation failed');
throw error;
}
}
```
## Migration Best Practices
1. Always test migrations locally first
2. Use transactions for complex operations
3. Add proper indexes for new columns
4. Update RLS policies when adding new tables
5. Generate TypeScript types after schema changes
6. Take into account constraints
7. Do not add breaking changes that would distrupt the DB to new migrations
## Common Gotchas
1. **RLS bypass**: Admin client bypasses all RLS - validate manually
2. **Missing indexes**: Always add indexes for foreign keys and commonly queried columns
3. **Security definer functions**: Only use with explicit permission checks
4. **Storage paths**: Must include account_id for proper access control
5. **Type safety**: Always regenerate types after schema changes

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,43 @@
# UI Components & Styling Instructions # UI Components & Styling
This file contains instructions for working with UI components, styling, and forms. ## Skills
## Core UI Library For forms:
- `/react-form-builder` - Forms with validation and server actions
Import from `packages/ui/src/`: ## Import Convention
Always use `@kit/ui/{component}`:
```tsx ```tsx
// Shadcn components
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { Card } from '@kit/ui/card'; import { Card } from '@kit/ui/card';
// Makerkit components
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
``` import { toast } from '@kit/ui/sonner';
NB: imports must follow the convention "@kit/ui/<name>", no matter the folder they're placed in
## Styling Guidelines
- Use **Tailwind CSS v4** with semantic classes
- Prefer Shadcn-ui classes like `bg-background`, `text-muted-foreground`
- Use `cn()` utility from `@kit/ui/utils` for class merging
```tsx
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
function MyComponent({ className }) {
return (
<div className={cn('bg-background text-foreground', className)}>
Content
</div>
);
}
``` ```
### Conditional Rendering ## Styling
Use the `If` component from `packages/ui/src/makerkit/if.tsx`: - Tailwind CSS v4 with semantic classes
- Prefer: `bg-background`, `text-muted-foreground`, `border-border`
- Use `cn()` for class merging
- Never use hardcoded colors like `bg-white`
## Key Components
| Component | Usage |
|-----------|-------|
| `If` | Conditional rendering |
| `Trans` | Internationalization |
| `toast` | Notifications |
| `Form*` | Form fields |
| `Button` | Actions |
| `Card` | Content containers |
| `Alert` | Error/info messages |
## Conditional Rendering
```tsx ```tsx
import { If } from '@kit/ui/if'; import { If } from '@kit/ui/if';
@@ -48,257 +45,27 @@ import { If } from '@kit/ui/if';
<If condition={isLoading} fallback={<Content />}> <If condition={isLoading} fallback={<Content />}>
<Spinner /> <Spinner />
</If> </If>
// With type inference
<If condition={error}>
{(err) => <ErrorMessage error={err} />}
</If>
``` ```
### Testing Attributes
```tsx
<button data-test="submit-button">Submit</button>
<div data-test="user-profile" data-user-id={user.id}>Profile</div>
```
## Forms with React Hook Form & Zod
```typescript
// 1. Schema in separate file
export const CreateNoteSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
// 2. Client component with form
'use client';
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
});
const onSubmit = (data) => {
startTransition(async () => {
await toast.promise(createNoteAction(data), {
loading: 'Creating...',
success: 'Created!',
error: 'Failed!',
}).unwrap();
});
};
```
### Form Examples
- Contact form: `apps/web/app/(marketing)/contact/_components/contact-form.tsx`
- Verify OTP form: `packages/otp/src/components/verify-otp-form.tsx`
### Guidelines
- Place Zod resolver outside so it can be reused with Server Actions
- Never add generics to `useForm`, use Zod resolver to infer types instead
- Never use `watch()` instead use hook `useWatch`
- Add `FormDescription` (optionally) and always add `FormMessage` to display errors
## Internationalization ## Internationalization
Always use `Trans` component from `packages/ui/src/makerkit/trans.tsx`:
```tsx ```tsx
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
<Trans <Trans i18nKey="namespace:key" values={{ name }} />
i18nKey="user:welcomeMessage"
values={{ name: user.name }}
/>
// With HTML elements
<Trans
i18nKey="terms:agreement"
components={{
TermsLink: <a href="/terms" className="underline" />,
}}
/>
``` ```
## Toast Notifications ## Testing Attributes
Use the `toast` utility from `@kit/ui/sonner`: Always add `data-test` for E2E:
```tsx ```tsx
import { toast } from '@kit/ui/sonner'; <button data-test="submit-button">Submit</button>
// Simple toast
toast.success('Success message');
toast.error('Error message');
// Promise-based toast
await toast.promise(asyncFunction(), {
loading: 'Processing...',
success: 'Done!',
error: 'Failed!',
});
``` ```
## Common Component Patterns ## Form Guidelines
### Loading States - Use `react-hook-form` with `zodResolver`
- Never add generics to `useForm`
```tsx - Use `useWatch` instead of `watch()`
import { Spinner } from '@kit/ui/spinner'; - Always include `FormMessage` for errors
<If condition={isLoading} fallback={<Content />}>
<Spinner className="h-4 w-4" />
</If>
```
### Error Handling
```tsx
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
<If condition={Boolean(error)}>
<Alert variant="destructive">
<ExclamationTriangleIcon className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
</If>
```
### Button Patterns
```tsx
import { Button } from '@kit/ui/button';
// Loading button
<Button disabled={isPending}>
{isPending ? (
<>
<Spinner className="mr-2 h-4 w-4" />
Loading...
</>
) : (
'Submit'
)}
</Button>
// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
```
### Card Layouts
```tsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kit/ui/card';
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description</CardDescription>
</CardHeader>
<CardContent>
Card content goes here
</CardContent>
</Card>
```
## Form Components
### Input Fields
```tsx
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';
<FormField
name="title"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Enter title" {...field} />
</FormControl>
<FormDescription>
The title of your task
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
```
### Select Components
```tsx
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kit/ui/select';
<FormField
name="category"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
</SelectContent>
</Select>
<FormDescription>
The category of your task
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
```
## Accessibility Guidelines
- Always include proper ARIA labels
- Use semantic HTML elements
- Ensure proper keyboard navigation
```tsx
<button
aria-label="Close modal"
aria-describedby="modal-description"
onClick={onClose}
>
<X className="h-4 w-4" />
</button>
```
## Dark Mode Support
The UI components automatically support dark mode through CSS variables. Use semantic color classes:
```tsx
// Good - semantic colors
<div className="bg-background text-foreground border-border">
<p className="text-muted-foreground">Secondary text</p>
</div>
// Avoid - hardcoded colors
<div className="bg-white text-black border-gray-200">
<p className="text-gray-500">Secondary text</p>
</div>
```

View File

@@ -1,289 +1 @@
# UI Components & Styling Instructions @AGENTS.md
This file contains instructions for working with UI components, styling, and forms.
## Core UI Library
Import from `packages/ui/src/`:
```tsx
// Shadcn components
import { Button } from '@kit/ui/button';
import { Card } from '@kit/ui/card';
// Makerkit components
import { If } from '@kit/ui/if';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { toast } from '@kit/ui/sonner';
import { Trans } from '@kit/ui/trans';
```
## Styling Guidelines
- Use **Tailwind CSS v4** with semantic classes
- Prefer Shadcn-ui classes like `bg-background`, `text-muted-foreground`
- Use `cn()` utility from `@kit/ui/utils` for class merging
```tsx
import { cn } from '@kit/ui/utils';
function MyComponent({ className }) {
return (
<div className={cn('bg-background text-foreground', className)}>
Content
</div>
);
}
```
### Conditional Rendering
Use the `If` component from `packages/ui/src/makerkit/if.tsx`:
```tsx
import { If } from '@kit/ui/if';
<If condition={isLoading} fallback={<Content />}>
<Spinner />
</If>
// With type inference
<If condition={error}>
{(err) => <ErrorMessage error={err} />}
</If>
```
### Testing Attributes
```tsx
<button data-test="submit-button">Submit</button>
<div data-test="user-profile" data-user-id={user.id}>Profile</div>
```
## Forms with React Hook Form & Zod
```typescript
// 1. Schema in separate file
export const CreateNoteSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
// 2. Client component with form
'use client';
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
});
const onSubmit = (data) => {
startTransition(async () => {
await toast.promise(createNoteAction(data), {
loading: 'Creating...',
success: 'Created!',
error: 'Failed!',
}).unwrap();
});
};
```
### Guidelines
- Place Zod resolver outside so it can be reused with Server Actions
- Never add generics to `useForm`, use Zod resolver to infer types instead
- Never use `watch()` instead use hook `useWatch`
- Add `FormDescription` (optionally) and always add `FormMessage` to display errors
### Form Examples
- Contact form: `apps/web/app/(marketing)/contact/_components/contact-form.tsx`
- Verify OTP form: `packages/otp/src/components/verify-otp-form.tsx`
## Internationalization
Always use `Trans` component from `packages/ui/src/makerkit/trans.tsx`:
```tsx
import { Trans } from '@kit/ui/trans';
<Trans
i18nKey="user:welcomeMessage"
values={{ name: user.name }}
/>
// With HTML elements
<Trans
i18nKey="terms:agreement"
components={{
TermsLink: <a href="/terms" className="underline" />,
}}
/>
```
## Toast Notifications
Use the `toast` utility from `@kit/ui/sonner`:
```tsx
import { toast } from '@kit/ui/sonner';
// Simple toast
toast.success('Success message');
toast.error('Error message');
// Promise-based toast
await toast.promise(asyncFunction(), {
loading: 'Processing...',
success: 'Done!',
error: 'Failed!',
});
```
## Common Component Patterns
### Loading States
```tsx
import { Spinner } from '@kit/ui/spinner';
<If condition={isLoading} fallback={<Content />}>
<Spinner className="h-4 w-4" />
</If>
```
### Error Handling
```tsx
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
import { ExclamationTriangleIcon } from '@radix-ui/react-icons';
<If condition={Boolean(error)}>
<Alert variant="destructive">
<ExclamationTriangleIcon className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
</If>
```
### Button Patterns
```tsx
import { Button } from '@kit/ui/button';
// Loading button
<Button disabled={isPending}>
{isPending ? (
<>
<Spinner className="mr-2 h-4 w-4" />
Loading...
</>
) : (
'Submit'
)}
</Button>
// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Cancel</Button>
<Button variant="ghost">Ghost</Button>
```
### Card Layouts
```tsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@kit/ui/card';
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description</CardDescription>
</CardHeader>
<CardContent>
Card content goes here
</CardContent>
</Card>
```
## Form Components
### Input Fields
```tsx
import { Input } from '@kit/ui/input';
import { Label } from '@kit/ui/label';
import { FormField, FormItem, FormLabel, FormControl, FormMessage } from '@kit/ui/form';
<FormField
name="title"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Enter title" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
```
### Select Components
```tsx
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kit/ui/select';
<FormField
name="category"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select category" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="option1">Option 1</SelectItem>
<SelectItem value="option2">Option 2</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
```
## Accessibility Guidelines
- Always include proper ARIA labels
- Use semantic HTML elements
- Ensure proper keyboard navigation
```tsx
<button
aria-label="Close modal"
aria-describedby="modal-description"
onClick={onClose}
>
<X className="h-4 w-4" />
</button>
```
## Dark Mode Support
The UI components automatically support dark mode through CSS variables. Use semantic color classes:
```tsx
// Good - semantic colors
<div className="bg-background text-foreground border-border">
<p className="text-muted-foreground">Secondary text</p>
</div>
// Avoid - hardcoded colors
<div className="bg-white text-black border-gray-200">
<p className="text-gray-500">Secondary text</p>
</div>
```

View File

@@ -31,6 +31,7 @@ export function MobileModeToggle(props: { className?: string }) {
} }
function setCookieTheme(theme: string) { function setCookieTheme(theme: string) {
const secure = typeof window !== 'undefined' && window.location.protocol === 'https:'; const secure =
typeof window !== 'undefined' && window.location.protocol === 'https:';
document.cookie = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax${secure ? '; Secure' : ''}`; document.cookie = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax${secure ? '; Secure' : ''}`;
} }

View File

@@ -126,7 +126,8 @@ export function SubMenuModeToggle() {
} }
function setCookieTheme(theme: string) { function setCookieTheme(theme: string) {
const secure = typeof window !== 'undefined' && window.location.protocol === 'https:'; const secure =
typeof window !== 'undefined' && window.location.protocol === 'https:';
document.cookie = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax${secure ? '; Secure' : ''}`; document.cookie = `theme=${theme}; path=/; max-age=31536000; SameSite=Lax${secure ? '; Secure' : ''}`;
} }

View File

@@ -96,7 +96,8 @@ const SidebarProvider: React.FC<
_setOpen(value); _setOpen(value);
// This sets the cookie to keep the sidebar state. // This sets the cookie to keep the sidebar state.
const secure = typeof window !== 'undefined' && window.location.protocol === 'https:'; const secure =
typeof window !== 'undefined' && window.location.protocol === 'https:';
document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax${secure ? '; Secure' : ''}`; document.cookie = `${SIDEBAR_COOKIE_NAME}=${open}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax${secure ? '; Secure' : ''}`;
}, },
[setOpenProp, open], [setOpenProp, open],