refactor: consolidate AGENTS.md and CLAUDE.md files, update tech stac… (#444)
* refactor: consolidate AGENTS.md and CLAUDE.md files, update tech stack and architecture details - Merged content from CLAUDE.md into AGENTS.md for better organization. - Updated tech stack section to reflect the current technologies used, including Next.js, Supabase, and Tailwind CSS. - Enhanced monorepo structure documentation with detailed directory purposes. - Streamlined multi-tenant architecture explanation and essential commands. - Added key patterns for naming conventions and server actions. - Removed outdated agent files related to Playwright and PostgreSQL, ensuring a cleaner codebase. - Bumped version to 2.23.7 to reflect changes.
This commit is contained in:
committed by
GitHub
parent
bebd56238b
commit
cfa137795b
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user