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,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 the tests for testing single file:
```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:
```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
# All tests
pnpm test
```
### Page Object Pattern (Required)
Always use Page Objects for test organization and reusability:
## Page Object Pattern (Required)
```typescript
// Example: auth.po.ts
export class AuthPageObject {
constructor(private readonly page: Page) {}
@@ -35,91 +26,35 @@ export class AuthPageObject {
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
## Selectors
**Use `toPass()` for flaky operations** - Always wrap unreliable operations:
Always use `data-test` attributes:
```typescript
// ✅ CORRECT - Reliable email/OTP operations
await expect(async () => {
const otpCode = await this.getOtpCodeFromEmail(email);
expect(otpCode).not.toBeNull();
await this.enterOtpCode(otpCode);
}).toPass();
await this.page.click('[data-test="submit-button"]');
await this.page.getByTestId('submit-button').click();
```
// ✅ CORRECT - Network requests with validation
## Reliability with `toPass()`
```typescript
await expect(async () => {
const response = await this.page.waitForResponse(resp =>
resp.url().includes('auth/v1/user')
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
## Test Organization
**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'
});
tests/
├── authentication/
├── billing/
├── *.po.ts # Page Objects
└── utils/
```
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 @@
## 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
@AGENTS.md

View File

@@ -1,329 +1,78 @@
# Web Application Instructions
# Web Application
This file contains instructions specific to the main Next.js web application.
## Application Structure
### Route Organization
## 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
├── (marketing)/ # Public pages
├── (auth)/ # Authentication
├── home/ # Authenticated routes
│ ├── (user)/ # Personal account
│ └── [account]/ # Team account (slug, not ID)
├── admin/ # Super admin
└── api/ # API routes
```
Key Examples:
## Component Organization
- 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`
- Route-specific: `_components/`
- Route utilities: `_lib/` (client), `_lib/server/` (server)
### Component Organization
## Skills
- **Route-specific**: Use `_components/` directories
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side
- **Global components**: Root-level directories
For specialized implementation:
- `/feature-builder` - End-to-end feature implementation
- `/server-action-builder` - Server actions
- `/forms-builder` - Forms with validation
- `/navigation-config` - Adding routes and menu items
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:
## Next.js 16 Params Pattern
```typescript
// ❌ WRONG - Don't use React.use() in async functions
// CORRECT - await params directly
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
const { account } = await params;
}
```
## 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
- **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) ✅
## Workspace Contexts
```typescript
import { getSupabaseServerClient } from '@kit/supabase/server-client';
// Personal: app/home/(user)
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
async function NotesPage() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
if (error) return <ErrorMessage error={error} />;
return <NotesList notes={data} />;
}
// Team: app/home/[account]
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
```
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed!
## Key Config Files
### 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.
| Purpose | Location |
|---------|----------|
| Feature flags | `config/feature-flags.config.ts` |
| Paths | `config/paths.config.ts` |
| Personal nav | `config/personal-account-navigation.config.tsx` |
| Team nav | `config/team-account-navigation.config.tsx` |
| i18n | `lib/i18n/i18n.settings.ts` |
## Internationalization
Always use `Trans` component from `@kit/ui/trans`:
Always use `Trans` component:
```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" />,
}}
/>
<Trans i18nKey="namespace:key" values={{ name }} />
```
### Adding New Languages
## Security
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
- Authentication enforced by middleware
- Authorization handled by RLS
- Never pass sensitive data to Client Components
- Never expose server env vars to client

View File

@@ -1,329 +1 @@
# Web Application Instructions
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
@AGENTS.md

View File

@@ -1,119 +1,55 @@
# 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
- **NEVER** bypass authentication or authorization checks
- **CRITICAL**: Use admin Supabase client with manual authorization validation
- Validate permissions for every admin operation
```typescript
import { AdminGuard } from '@kit/admin/components/admin-guard';
import { PageBody, PageHeader } from '@kit/ui/page';
### Admin Client Usage Pattern
async function AdminPage() {
return (
<>
<PageHeader title="Admin" />
<PageBody>{/* Content */}</PageBody>
</>
);
}
export default AdminGuard(AdminPage);
```
## Admin Client Usage
```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();
// CRITICAL: Validate first - admin client bypasses RLS
if (!(await isSuperAdmin(currentUser))) {
throw new Error('Unauthorized: Admin access required');
throw new Error('Unauthorized');
}
// Now safe to proceed with admin privileges
const { data } = await adminClient.from('accounts').select('*');
return data;
const adminClient = getSupabaseServerAdminClient();
// Safe to proceed
}
```
## Page Structure Patterns
### Standard Admin Page Template
## Audit Logging
```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);
const logger = await getLogger();
logger.info({
name: 'admin-audit',
action: 'delete-user',
adminId: currentUser.id,
targetId: userId,
}, 'Admin action performed');
```
### 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
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**
@AGENTS.md

View File

@@ -1,265 +1,73 @@
# Supabase Database Schema Management
This file contains guidance for working with database schemas, migrations, and Supabase development workflows.
# Supabase Database
## 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
# Create new schema file
touch apps/web/supabase/schemas/15-my-new-feature.sql
# Create schema file
touch schemas/20-feature.sql
# Create Migration
pnpm --filter web run supabase migrations new my-new-feature
# Create migration
pnpm --filter web run supabase migrations new feature_name
# 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
# Copy content, apply, generate types
pnpm --filter web supabase migrations up
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:
### Modify Existing
```bash
# Edit schema file (e.g., schemas/03-accounts.sql)
# Make your changes...
# Edit schema, generate diff
pnpm --filter web run supabase:db:diff -f update_feature
# 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
# Apply and regenerate
pnpm --filter web supabase migrations up
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
ALTER TYPE public.app_permissions ADD VALUE 'notes.manage';
COMMIT;
```
### Table Creation with RLS
```sql
-- Create table
create table if not exists public.notes (
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,
-- ...
created_at timestamp with time zone default now(),
primary key (id)
);
-- CRITICAL: Always enable RLS
alter table "public"."notes" enable row level security;
alter table "public"."feature" 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
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
-- Use helper functions for policies
create policy "feature_read" on public.feature 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
## Commands
```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
pnpm supabase:web:reset # Reset database
pnpm supabase:web:typegen # Generate TypeScript types
pnpm --filter web supabase migrations list # View migrations
```

View File

@@ -1,265 +1 @@
# Supabase Database Schema Management
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
```
@AGENTS.md