Refactor documentation in AGENTS.md and CLAUDE.md for clarity and consistency
- Updated AGENTS.md and CLAUDE.md to enhance readability and organization. - Streamlined project overview, multi-tenant architecture, and essential commands sections. - Improved formatting and clarity in security guidelines and database operations. - Added new entries to .prettierignore for .hbs and .md files to ensure proper formatting exclusions.
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
database.types.ts
|
database.types.ts
|
||||||
playwright-report
|
playwright-report
|
||||||
*.hbs
|
*.hbs
|
||||||
|
*.md
|
||||||
511
AGENTS.md
511
AGENTS.md
@@ -1,18 +1,6 @@
|
|||||||
# Project Agents.md Guide for OpenAI Codex
|
# AGENTS.md
|
||||||
|
|
||||||
This Agents.md file provides comprehensive guidance for OpenAI Codex and other AI agents working with this codebase.
|
This AGENTS.md file provides comprehensive guidance for OpenAI Codex and other AI agents working with this codebase.
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Makerkit is a multi-tenant SaaS application using a Turborepo monorepo structure with distinct apps for the main web application, development tools, and e2e testing.
|
|
||||||
|
|
||||||
### Monorepo Structure
|
|
||||||
|
|
||||||
- `/apps/web` - Main Next.js SaaS application
|
|
||||||
- `/apps/dev-tool` - Development utilities (port 3010)
|
|
||||||
- `/apps/e2e` - Playwright end-to-end tests
|
|
||||||
- `/packages/` - Shared packages and utilities
|
|
||||||
- `/tooling/` - Build tools and development scripts
|
|
||||||
|
|
||||||
### Core Technologies
|
### Core Technologies
|
||||||
|
|
||||||
@@ -23,17 +11,24 @@ Makerkit is a multi-tenant SaaS application using a Turborepo monorepo structure
|
|||||||
- **Tailwind CSS 4** for styling
|
- **Tailwind CSS 4** for styling
|
||||||
- **Turborepo** for monorepo management
|
- **Turborepo** for monorepo management
|
||||||
|
|
||||||
|
### Monorepo Structure
|
||||||
|
|
||||||
|
- @apps/web - Main Next.js SaaS application
|
||||||
|
- @apps/dev-tool - Development utilities (port 3010)
|
||||||
|
- @apps/e2e - Playwright end-to-end tests
|
||||||
|
- @packages/ - Shared packages and utilities
|
||||||
|
- @tooling/ - Build tools and development scripts
|
||||||
|
|
||||||
### Multi-Tenant Architecture
|
### Multi-Tenant Architecture
|
||||||
|
|
||||||
Uses a dual account model:
|
**Personal Accounts**: Individual user accounts (auth.users.id = accounts.id)
|
||||||
|
**Team Accounts**: Shared workspaces with members, roles, and permissions
|
||||||
|
|
||||||
- **Personal Accounts**: Individual user accounts (`auth.users.id = accounts.id`)
|
Data associates with accounts via foreign keys for proper access control.
|
||||||
- **Team Accounts**: Shared workspaces with members, roles, and permissions
|
|
||||||
- Data associates with accounts via foreign keys for proper access control
|
|
||||||
|
|
||||||
## Essential Commands
|
## Essential Commands
|
||||||
|
|
||||||
### Development
|
### Development Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Start all apps
|
pnpm dev # Start all apps
|
||||||
@@ -74,7 +69,7 @@ app/
|
|||||||
└── api/ # API routes
|
└── api/ # API routes
|
||||||
```
|
```
|
||||||
|
|
||||||
See complete structure in @apps/web/app/ with examples like:
|
Key Examples:
|
||||||
|
|
||||||
- Marketing layout: @apps/web/app/(marketing)/layout.tsx
|
- Marketing layout: @apps/web/app/(marketing)/layout.tsx
|
||||||
- Personal dashboard: @apps/web/app/home/(user)/page.tsx
|
- Personal dashboard: @apps/web/app/home/(user)/page.tsx
|
||||||
@@ -83,11 +78,11 @@ See complete structure in @apps/web/app/ with examples like:
|
|||||||
|
|
||||||
### Component Organization
|
### Component Organization
|
||||||
|
|
||||||
- **Route-specific**: Use `_components/` directories
|
- **Route-specific**: Use \_components/ directories
|
||||||
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side
|
- **Route utilities**: Use \_lib/ for client, \_lib/server/ for server-side
|
||||||
- **Global components**: Root-level directories
|
- **Global components**: Root-level directories
|
||||||
|
|
||||||
Example organization:
|
Example:
|
||||||
|
|
||||||
- Team components: @apps/web/app/home/[account]/\_components/
|
- Team components: @apps/web/app/home/[account]/\_components/
|
||||||
- Team server utils: @apps/web/app/home/[account]/\_lib/server/
|
- Team server utils: @apps/web/app/home/[account]/\_lib/server/
|
||||||
@@ -95,69 +90,242 @@ Example organization:
|
|||||||
|
|
||||||
## Database Guidelines
|
## Database Guidelines
|
||||||
|
|
||||||
### Security & RLS
|
### Security & RLS Implementation
|
||||||
|
|
||||||
|
**Critical Security Guidelines - Read Carefully! ⚠️**
|
||||||
|
|
||||||
|
#### Database Security Fundamentals
|
||||||
|
|
||||||
- **Always enable RLS** on new tables unless explicitly instructed otherwise
|
- **Always enable RLS** on new tables unless explicitly instructed otherwise
|
||||||
- Use helper functions for access control:
|
- **NEVER use SECURITY DEFINER functions** without explicit access controls - they bypass RLS entirely
|
||||||
- `public.has_role_on_account(account_id, role?)` - Check team membership
|
- **Always use security_invoker=true for views** to maintain proper access control
|
||||||
- `public.has_permission(user_id, account_id, permission)` - Check permissions
|
- **Storage buckets MUST validate access** using account_id in the path structure. See @apps/web/supabase/schemas/16-storage.sql for proper implementation.
|
||||||
- `public.is_account_owner(account_id)` - Verify ownership
|
- **Use locks if required**: Database locks prevent race conditions and timing attacks in concurrent operations. Make sure to take these into account for all database operations.
|
||||||
|
|
||||||
See RLS examples in database schemas: @apps/web/supabase/schemas/
|
#### Security Definer Function - Dangerous Pattern ❌
|
||||||
|
|
||||||
### Schema Management
|
```sql
|
||||||
|
-- NEVER DO THIS - Allows any authenticated user to call function
|
||||||
|
CREATE OR REPLACE FUNCTION public.dangerous_function()
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER AS $
|
||||||
|
BEGIN
|
||||||
|
-- This bypasses all RLS policies!
|
||||||
|
DELETE FROM sensitive_table; -- Anyone can call this!
|
||||||
|
END;
|
||||||
|
$;
|
||||||
|
GRANT EXECUTE ON FUNCTION public.dangerous_function() TO authenticated;
|
||||||
|
```
|
||||||
|
|
||||||
- Schemas in `apps/web/supabase/schemas/`
|
#### Security Definer Function - Safe Pattern ✅
|
||||||
- Create as `<number>-<name>.sql`
|
|
||||||
- After changes: `pnpm --filter web supabase:db:diff` then `pnpm supabase:web:reset`
|
```sql
|
||||||
|
-- ONLY use SECURITY DEFINER with explicit access validation
|
||||||
|
CREATE OR REPLACE FUNCTION public.safe_admin_function(target_account_id uuid)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = '' AS $
|
||||||
|
BEGIN
|
||||||
|
-- MUST validate caller has permission FIRST
|
||||||
|
IF NOT public.is_account_owner(target_account_id) THEN
|
||||||
|
RAISE EXCEPTION 'Access denied: insufficient permissions';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Now safe to proceed with elevated privileges
|
||||||
|
-- Your admin operation here
|
||||||
|
END;
|
||||||
|
$;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Existing Helper Functions - Use These! 📚
|
||||||
|
|
||||||
|
**DO NOT recreate these functions - they already exist:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Account Access Control
|
||||||
|
public.has_role_on_account(account_id, role?) -- Check team membership
|
||||||
|
public.has_permission(user_id, account_id, permission) -- Check permissions
|
||||||
|
public.is_account_owner(account_id) -- Verify ownership
|
||||||
|
public.has_active_subscription(account_id) -- Subscription status
|
||||||
|
public.is_team_member(account_id, user_id) -- Direct membership check
|
||||||
|
public.can_action_account_member(target_account_id, target_user_id) -- Member action rights
|
||||||
|
|
||||||
|
-- Administrative Functions
|
||||||
|
public.is_super_admin() -- Super admin check
|
||||||
|
public.is_aal2() -- MFA verification
|
||||||
|
public.is_mfa_compliant() -- MFA compliance
|
||||||
|
|
||||||
|
-- Configuration
|
||||||
|
public.is_set(field_name) -- Feature flag checks
|
||||||
|
```
|
||||||
|
|
||||||
|
Always check @apps/web/supabase/schemas/ before creating new functions!
|
||||||
|
|
||||||
|
#### RLS Policy Best Practices ✅
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Proper RLS using existing helper functions
|
||||||
|
CREATE POLICY "notes_read" ON public.notes FOR SELECT
|
||||||
|
TO authenticated USING (
|
||||||
|
account_id = (select auth.uid()) OR
|
||||||
|
public.has_role_on_account(account_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- For operations requiring specific permissions
|
||||||
|
CREATE POLICY "notes_manage" ON public.notes FOR ALL
|
||||||
|
TO authenticated USING (
|
||||||
|
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Management Workflow
|
||||||
|
|
||||||
|
1. Create schemas in @apps/web/supabase/schemas/ as `<number>-<name>.sql`
|
||||||
|
2. After changes: `pnpm supabase:web:stop`
|
||||||
|
3. Run: `pnpm --filter web run supabase:db:diff -f <filename>`
|
||||||
|
4. Restart: `pnpm supabase:web:start` and `pnpm supabase:web:reset`
|
||||||
|
5. Generate types: `pnpm supabase:web:typegen`
|
||||||
|
|
||||||
Key schema files:
|
Key schema files:
|
||||||
|
|
||||||
- Accounts: `apps/web/supabase/schemas/03-accounts.sql`
|
- Accounts: @apps/web/supabase/schemas/03-accounts.sql
|
||||||
- Memberships: `apps/web/supabase/schemas/05-memberships.sql`
|
- Memberships: @apps/web/supabase/schemas/05-memberships.sql
|
||||||
- Permissions: `apps/web/supabase/schemas/06-roles-permissions.sql`
|
- Permissions: @apps/web/supabase/schemas/06-roles-permissions.sql
|
||||||
|
|
||||||
### Type Generation
|
### Type Generation
|
||||||
|
|
||||||
Import auto-generated types from @packages/supabase/src/types/database.ts:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
|
|
||||||
type Account = Tables<'accounts'>;
|
type Account = Tables<'accounts'>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Always prefer inferring types from generated Database types.
|
||||||
|
|
||||||
## Development Patterns
|
## Development Patterns
|
||||||
|
|
||||||
### Data Fetching
|
### Data Fetching Strategy
|
||||||
|
|
||||||
- **Server Components**: Use `getSupabaseServerClient()` from @packages/supabase/src/clients/server-client.ts
|
**Quick Decision Framework:**
|
||||||
- **Client Components**: Use `useSupabase()` hook + React Query's `useQuery`
|
|
||||||
- **Admin Operations**: Use `getSupabaseServerAdminClient()` from @packages/supabase/src/clients/server-admin-client.ts (rare cases only - bypasses RLS, use with caution!)
|
|
||||||
- Prefer Server Components and pass data down when needed
|
|
||||||
|
|
||||||
Use the Container/Presenter pattern for complex data components:
|
- **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
|
```typescript
|
||||||
// Container: handles data fetching
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
function UserProfileContainer() {
|
|
||||||
const userData = useUserData();
|
|
||||||
return <UserProfilePresenter data={userData.data} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presenter: handles UI rendering
|
async function NotesPage() {
|
||||||
function UserProfilePresenter({ data }: { data: UserData }) {
|
const client = getSupabaseServerClient();
|
||||||
return <div>{data.name}</div>;
|
const { data, error } = await client.from('notes').select('*');
|
||||||
|
|
||||||
|
if (error) return <ErrorMessage error={error} />;
|
||||||
|
return <NotesList notes={data} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Example server-side data loading:
|
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed!
|
||||||
|
|
||||||
- User workspace loader: @apps/web/app/home/(user)/\_lib/server/load-user-workspace.ts
|
#### Client Components (Interactive) 🖱️
|
||||||
- Team workspace loader: @apps/web/app/home/[account]/\_lib/server/team-account-workspace.loader.ts
|
|
||||||
- Data provider pattern: @packages/features/team-accounts/src/components/members/roles-data-provider.tsx
|
|
||||||
|
|
||||||
### Server Actions
|
```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.
|
||||||
|
|
||||||
|
### Server Actions Implementation
|
||||||
|
|
||||||
Always use `enhanceAction` from @packages/next/src/actions/index.ts:
|
Always use `enhanceAction` from @packages/next/src/actions/index.ts:
|
||||||
|
|
||||||
@@ -186,14 +354,14 @@ Example server actions:
|
|||||||
### Forms with React Hook Form & Zod
|
### Forms with React Hook Form & Zod
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 1. Define schema in separate file
|
// 1. Schema in separate file
|
||||||
export const CreateNoteSchema = z.object({
|
export const CreateNoteSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
content: z.string().min(1),
|
content: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Client component with form
|
// 2. Client component with form
|
||||||
('use client');
|
'use client';
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(CreateNoteSchema),
|
resolver: zodResolver(CreateNoteSchema),
|
||||||
});
|
});
|
||||||
@@ -209,7 +377,7 @@ const onSubmit = (data) => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
See form examples:
|
Form examples:
|
||||||
|
|
||||||
- Contact form: @apps/web/app/(marketing)/contact/\_components/contact-form.tsx
|
- Contact form: @apps/web/app/(marketing)/contact/\_components/contact-form.tsx
|
||||||
- Verify OTP form: @packages/otp/src/components/verify-otp-form.tsx
|
- Verify OTP form: @packages/otp/src/components/verify-otp-form.tsx
|
||||||
@@ -233,18 +401,21 @@ export const POST = enhanceRouteHandler(
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
Example API routes:
|
|
||||||
|
|
||||||
- Billing webhook: @apps/web/app/api/billing/webhook/route.ts
|
|
||||||
- Database webhook: @apps/web/app/api/db/webhook/route.ts
|
|
||||||
|
|
||||||
## React & TypeScript Best Practices
|
## React & TypeScript Best Practices
|
||||||
|
|
||||||
|
### TS
|
||||||
|
|
||||||
|
- Write clean, clear, well-designed, explicit Typescript
|
||||||
|
- Use implicit type inference, unless impossible
|
||||||
|
- `any` and `unknown` are a code smell and must justified if used
|
||||||
|
- Handle errors gracefully using try/catch and appropriate error types.
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
- Use functional components with TypeScript
|
- Use functional components with TypeScript
|
||||||
- Always use `'use client'` directive for client components
|
- Always use 'use client' directive for client components
|
||||||
- Destructure props with proper TypeScript interfaces
|
- Destructure props with proper TypeScript interfaces
|
||||||
|
- Name files to match component name (e.g., user-profile.tsx)
|
||||||
|
|
||||||
### Conditional Rendering
|
### Conditional Rendering
|
||||||
|
|
||||||
@@ -252,7 +423,6 @@ Use the `If` component from @packages/ui/src/makerkit/if.tsx:
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Spinner } '@kit/ui/spinner';
|
|
||||||
|
|
||||||
<If condition={isLoading} fallback={<Content />}>
|
<If condition={isLoading} fallback={<Content />}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -266,12 +436,9 @@ import { Spinner } '@kit/ui/spinner';
|
|||||||
|
|
||||||
### Testing Attributes
|
### Testing Attributes
|
||||||
|
|
||||||
Add data attributes for testing:
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<button data-test="submit-button">Submit</button>
|
<button data-test="submit-button">Submit</button>
|
||||||
<div data-test="user-profile" data-user-id={user.id}>Profile</div>
|
<div data-test="user-profile" data-user-id={user.id}>Profile</div>
|
||||||
<form data-test="signup-form">Form content</form>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Internationalization
|
### Internationalization
|
||||||
@@ -281,11 +448,9 @@ Always use `Trans` component from @packages/ui/src/makerkit/trans.tsx:
|
|||||||
```tsx
|
```tsx
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
// Basic usage
|
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="user:welcomeMessage"
|
i18nKey="user:welcomeMessage"
|
||||||
values={{ name: user.name }}
|
values={{ name: user.name }}
|
||||||
defaults="Welcome, {name}!"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
// With HTML elements
|
// With HTML elements
|
||||||
@@ -294,60 +459,35 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
components={{
|
components={{
|
||||||
TermsLink: <a href="/terms" className="underline" />,
|
TermsLink: <a href="/terms" className="underline" />,
|
||||||
}}
|
}}
|
||||||
defaults="I agree to the <TermsLink>Terms</TermsLink>."
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
// Pluralization
|
|
||||||
<Trans
|
|
||||||
i18nKey="notifications:count"
|
|
||||||
count={notifications.length}
|
|
||||||
defaults="{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `LanguageSelector` component from @packages/ui/src/makerkit/language-selector.tsx:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { LanguageSelector } from '@kit/ui/language-selector';
|
|
||||||
|
|
||||||
<LanguageSelector />;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Adding new languages:
|
Adding new languages:
|
||||||
|
|
||||||
1. Add language code to @apps/web/lib/i18n/i18n.settings.ts
|
1. Add language code to @apps/web/lib/i18n/i18n.settings.ts
|
||||||
2. Create translation files in @apps/web/public/locales/[new-language]/
|
2. Create translation files in @apps/web/public/locales/[new-language]/
|
||||||
3. Copy structure from English files as template
|
3. Copy structure from English files
|
||||||
|
|
||||||
Adding new namespaces:
|
Translation files: @apps/web/public/locales/<locale>/<namespace>.json
|
||||||
|
|
||||||
1. Add namespace to `defaultI18nNamespaces` in @apps/web/lib/i18n/i18n.settings.ts
|
## Security Guidelines 🛡️
|
||||||
2. Create corresponding translation files for all supported languages
|
|
||||||
|
|
||||||
Translation files located in @apps/web/public/locales/<locale>/<namespace>.json:
|
|
||||||
|
|
||||||
- Common translations: @apps/web/public/locales/en/common.json
|
|
||||||
- Auth translations: @apps/web/public/locales/en/auth.json
|
|
||||||
- Team translations: @apps/web/public/locales/en/teams.json
|
|
||||||
|
|
||||||
## Security Guidelines
|
|
||||||
|
|
||||||
### Authentication & Authorization
|
### Authentication & Authorization
|
||||||
|
|
||||||
- Authentication enforced by middleware
|
- Authentication enforced by middleware
|
||||||
- Authorization typically handled by RLS at database level, unless using the admin client
|
- Authorization handled by RLS at database level
|
||||||
- For rare admin client usage, enforce both manually
|
- Avoid defensive code - use RLS instead
|
||||||
- User authentication helper: @apps/web/lib/server/require-user-in-server-component.ts if required or to obtain the authed user
|
- For admin client usage, enforce both authentication and authorization
|
||||||
|
|
||||||
### Data Passing
|
### Data Passing
|
||||||
|
|
||||||
- **Never pass sensitive data** to Client Components
|
- **Never pass sensitive data** to Client Components
|
||||||
- **Never expose server environment variables** to client (unless `NEXT_PUBLIC_`)
|
- **Never expose server environment variables** to client (unless prefixed with NEXT_PUBLIC)
|
||||||
- Always validate user input before processing
|
- Always validate user input
|
||||||
|
|
||||||
### OTP for Sensitive Operations
|
### OTP for Sensitive Operations
|
||||||
|
|
||||||
Use one-time tokens from @packages/otp/src/api/index.ts for destructive operations:
|
Use one-time tokens from @packages/otp/src/api/index.ts:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { VerifyOtpForm } from '@kit/otp/components';
|
import { VerifyOtpForm } from '@kit/otp/components';
|
||||||
@@ -361,8 +501,6 @@ import { VerifyOtpForm } from '@kit/otp/components';
|
|||||||
/>;
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
OTP schema and functions: @apps/web/supabase/schemas/12-one-time-tokens.sql
|
|
||||||
|
|
||||||
### Super Admin Protection
|
### Super Admin Protection
|
||||||
|
|
||||||
For admin routes, use `AdminGuard` from @packages/features/admin/src/components/admin-guard.tsx:
|
For admin routes, use `AdminGuard` from @packages/features/admin/src/components/admin-guard.tsx:
|
||||||
@@ -373,35 +511,7 @@ import { AdminGuard } from '@kit/admin/components/admin-guard';
|
|||||||
export default AdminGuard(AdminPageComponent);
|
export default AdminGuard(AdminPageComponent);
|
||||||
```
|
```
|
||||||
|
|
||||||
For admin server actions, use `adminAction` wrapper:
|
## UI Components 🎨
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { adminAction } from '@kit/admin';
|
|
||||||
|
|
||||||
export const yourAdminAction = adminAction(
|
|
||||||
enhanceAction(
|
|
||||||
async (data) => {
|
|
||||||
// Action implementation
|
|
||||||
},
|
|
||||||
{ schema: YourActionSchema },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Admin service security pattern:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
private async assertUserIsNotCurrentSuperAdmin(targetId: string) {
|
|
||||||
const { data } = await this.client.auth.getUser();
|
|
||||||
const currentUserId = data.user?.id;
|
|
||||||
|
|
||||||
if (currentUserId === targetId) {
|
|
||||||
throw new Error('Cannot perform destructive action on your own account');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## UI Components
|
|
||||||
|
|
||||||
### Core UI Library
|
### Core UI Library
|
||||||
|
|
||||||
@@ -410,40 +520,23 @@ Import from @packages/ui/src/:
|
|||||||
```tsx
|
```tsx
|
||||||
// Shadcn components
|
// Shadcn components
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
// @packages/ui/src/shadcn/button.tsx
|
|
||||||
import { Card } from '@kit/ui/card';
|
import { Card } from '@kit/ui/card';
|
||||||
// @packages/ui/src/shadcn/sonner.tsx
|
|
||||||
|
|
||||||
// Makerkit components
|
// Makerkit components
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
// @packages/ui/src/makerkit/trans.tsx
|
|
||||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||||
// @packages/ui/src/shadcn/card.tsx
|
|
||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
// @packages/ui/src/makerkit/if.tsx
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
// @packages/ui/src/makerkit/profile-avatar.tsx
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Component Categories
|
|
||||||
|
|
||||||
- **Forms**: Form components in @packages/ui/src/shadcn/form.tsx
|
|
||||||
- **Navigation**: Navigation menu in @packages/ui/src/shadcn/navigation-menu.tsx
|
|
||||||
- **Data Display**: Data table in @packages/ui/src/makerkit/data-table.tsx
|
|
||||||
- **Marketing**: Marketing components in @packages/ui/src/makerkit/marketing/
|
|
||||||
|
|
||||||
### Styling
|
### Styling
|
||||||
|
|
||||||
- Use Tailwind CSS with semantic classes
|
- Use Tailwind CSS v4 with semantic classes
|
||||||
- Prefer `bg-background`, `text-muted-foreground` over fixed colors
|
- Prefer Shadcn-ui classes like `bg-background`, `text-muted-foreground`
|
||||||
- Use `cn()` utility from @packages/ui/src/lib/utils.ts for class merging
|
- Use `cn()` utility from @kit/ui/cn for class merging
|
||||||
|
|
||||||
## Workspace Contexts
|
## Workspace Contexts 🏢
|
||||||
|
|
||||||
### Personal Account Context (`/home/(user)`)
|
### Personal Account Context (@apps/web/app/home/(user))
|
||||||
|
|
||||||
Use hook from `packages/features/accounts/src/hooks/use-user-workspace.ts`:
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
|
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
|
||||||
@@ -454,11 +547,9 @@ function PersonalComponent() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Context provider: `packages/features/accounts/src/components/user-workspace-context-provider.tsx`
|
Context provider: @packages/features/accounts/src/components/user-workspace-context-provider.tsx
|
||||||
|
|
||||||
### Team Account Context (`/home/[account]`)
|
### Team Account Context (@apps/web/app/home/[account])
|
||||||
|
|
||||||
Use hook from `packages/features/team-accounts/src/hooks/use-team-account-workspace.ts`:
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
|
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
|
||||||
@@ -469,89 +560,59 @@ function TeamComponent() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Context provider: `packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
|
Context provider: @packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx
|
||||||
|
|
||||||
## Error Handling & Logging
|
## Error Handling & Logging 📊
|
||||||
|
|
||||||
### Structured Logging
|
### Structured Logging
|
||||||
|
|
||||||
Use logger from `packages/shared/src/logger/logger.ts`:
|
Use logger from @packages/shared/src/logger/logger.ts:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|
||||||
const logger = await getLogger();
|
async function myServerAction() {
|
||||||
const ctx = { name: 'myOperation', userId: user.id };
|
const logger = await getLogger();
|
||||||
|
const ctx = { name: 'myOperation', userId: user.id };
|
||||||
|
|
||||||
logger.info(ctx, 'Operation started');
|
try {
|
||||||
// ... operation
|
logger.info(ctx, 'Operation started');
|
||||||
logger.error({ ...ctx, error }, 'Operation failed');
|
// ...
|
||||||
```
|
} catch (error) {
|
||||||
|
logger.error({ ...ctx, error }, 'Operation failed');
|
||||||
### Error Boundaries
|
// handle error
|
||||||
|
}
|
||||||
Use proper error handling with meaningful user messages:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
try {
|
|
||||||
await operation();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ error, context }, 'Operation failed');
|
|
||||||
return { error: 'Unable to complete operation' }; // Generic message
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Feature Development Workflow
|
|
||||||
|
|
||||||
### Creating New Pages
|
|
||||||
|
|
||||||
1. Create page component in appropriate route group
|
|
||||||
2. Add `withI18n()` HOC from `apps/web/lib/i18n/with-i18n.tsx`
|
|
||||||
3. Implement `generateMetadata()` for SEO
|
|
||||||
4. Add loading state with `loading.tsx`
|
|
||||||
5. Create components in `_components/` directory
|
|
||||||
6. Add server utilities in `_lib/server/`
|
|
||||||
|
|
||||||
Example page structure:
|
|
||||||
|
|
||||||
- Marketing page: `apps/web/app/(marketing)/pricing/page.tsx`
|
|
||||||
- Dashboard page: `apps/web/app/home/(user)/page.tsx`
|
|
||||||
- Team page: `apps/web/app/home/[account]/members/page.tsx`
|
|
||||||
|
|
||||||
### Permission Patterns
|
|
||||||
|
|
||||||
- Check permissions before data operations
|
|
||||||
- Guard premium features with `public.has_active_subscription`
|
|
||||||
- Use role hierarchy for member management
|
|
||||||
- Primary account owners have special privileges
|
|
||||||
|
|
||||||
Permission helpers in database: `apps/web/supabase/schemas/06-roles-permissions.sql`
|
|
||||||
|
|
||||||
### Database Development
|
|
||||||
|
|
||||||
1. Create schema file: `apps/web/supabase/schemas/<number>-<name>.sql`
|
|
||||||
2. Enable RLS and create policies
|
|
||||||
3. Generate migration: `pnpm --filter web supabase:db:diff`
|
|
||||||
4. Reset database: `pnpm supabase:web:reset`
|
|
||||||
5. Generate types: `pnpm supabase:web:typegen`
|
|
||||||
|
|
||||||
## API Services
|
## API Services
|
||||||
|
|
||||||
### Account Services
|
### Account Services
|
||||||
|
|
||||||
- Personal accounts API: `packages/features/accounts/src/server/api.ts`
|
- Personal accounts API: @packages/features/accounts/src/server/api.ts
|
||||||
- Team accounts API: `packages/features/team-accounts/src/server/api.ts`
|
- Team accounts API: @packages/features/team-accounts/src/server/api.ts
|
||||||
- Admin service: `packages/features/admin/src/lib/server/services/admin.service.ts`
|
- Admin service: @packages/features/admin/src/lib/server/services/admin.service.ts
|
||||||
|
|
||||||
### Billing Services
|
### Billing Services
|
||||||
|
|
||||||
- Personal billing: `apps/web/app/home/(user)/billing/_lib/server/user-billing.service.ts`
|
- Personal billing: @apps/web/app/home/(user)/billing/\_lib/server/user-billing.service.ts
|
||||||
- Team billing: `apps/web/app/home/[account]/billing/_lib/server/team-billing.service.ts`
|
- Team billing: @apps/web/app/home/[account]/billing/\_lib/server/team-billing.service.ts
|
||||||
- Per-seat billing: `packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts`
|
- Per-seat billing: @packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts
|
||||||
|
|
||||||
## Key Configuration Files
|
## Key Configuration Files
|
||||||
|
|
||||||
- **Feature flags**: @apps/web/config/feature-flags.config.ts
|
- **Feature flags**: @apps/web/config/feature-flags.config.ts
|
||||||
- **i18n settings**: @apps/web/lib/i18n/i18n.settings.ts
|
- **i18n settings**: @apps/web/lib/i18n/i18n.settings.ts
|
||||||
- **Supabase local config**: @apps/web/supabase/config.toml@
|
- **Supabase config**: @apps/web/supabase/config.toml
|
||||||
- **Middleware**: @apps/web/middleware.ts`
|
- **Middleware**: @apps/web/middleware.ts
|
||||||
|
|
||||||
|
## Quick Reference Checklist ✅
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
- [ ] Enable RLS on new tables
|
||||||
|
- [ ] Generate TypeScript types after schema changes and infer types from these
|
||||||
|
- [ ] Implement proper error handling with logging
|
||||||
|
- [ ] Use Zod schemas for parsing all user input (including cookies, query params, etc.)
|
||||||
|
- [ ] Add testing attributes to interactive elements
|
||||||
|
- [ ] Validate permissions before sensitive operations
|
||||||
509
CLAUDE.md
509
CLAUDE.md
@@ -1,18 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code when working with code in this repository.
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Makerkit is a multi-tenant SaaS application using a Turborepo monorepo structure with distinct apps for the main web application, development tools, and e2e testing.
|
|
||||||
|
|
||||||
### Monorepo Structure
|
|
||||||
|
|
||||||
- `/apps/web` - Main Next.js SaaS application
|
|
||||||
- `/apps/dev-tool` - Development utilities (port 3010)
|
|
||||||
- `/apps/e2e` - Playwright end-to-end tests
|
|
||||||
- `/packages/` - Shared packages and utilities
|
|
||||||
- `/tooling/` - Build tools and development scripts
|
|
||||||
|
|
||||||
### Core Technologies
|
### Core Technologies
|
||||||
|
|
||||||
@@ -23,17 +11,24 @@ Makerkit is a multi-tenant SaaS application using a Turborepo monorepo structure
|
|||||||
- **Tailwind CSS 4** for styling
|
- **Tailwind CSS 4** for styling
|
||||||
- **Turborepo** for monorepo management
|
- **Turborepo** for monorepo management
|
||||||
|
|
||||||
|
### Monorepo Structure
|
||||||
|
|
||||||
|
- @apps/web - Main Next.js SaaS application
|
||||||
|
- @apps/dev-tool - Development utilities (port 3010)
|
||||||
|
- @apps/e2e - Playwright end-to-end tests
|
||||||
|
- @packages/ - Shared packages and utilities
|
||||||
|
- @tooling/ - Build tools and development scripts
|
||||||
|
|
||||||
### Multi-Tenant Architecture
|
### Multi-Tenant Architecture
|
||||||
|
|
||||||
Uses a dual account model:
|
**Personal Accounts**: Individual user accounts (auth.users.id = accounts.id)
|
||||||
|
**Team Accounts**: Shared workspaces with members, roles, and permissions
|
||||||
|
|
||||||
- **Personal Accounts**: Individual user accounts (`auth.users.id = accounts.id`)
|
Data associates with accounts via foreign keys for proper access control.
|
||||||
- **Team Accounts**: Shared workspaces with members, roles, and permissions
|
|
||||||
- Data associates with accounts via foreign keys for proper access control
|
|
||||||
|
|
||||||
## Essential Commands
|
## Essential Commands
|
||||||
|
|
||||||
### Development
|
### Development Workflow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Start all apps
|
pnpm dev # Start all apps
|
||||||
@@ -74,7 +69,7 @@ app/
|
|||||||
└── api/ # API routes
|
└── api/ # API routes
|
||||||
```
|
```
|
||||||
|
|
||||||
See complete structure in @apps/web/app/ with examples like:
|
Key Examples:
|
||||||
|
|
||||||
- Marketing layout: @apps/web/app/(marketing)/layout.tsx
|
- Marketing layout: @apps/web/app/(marketing)/layout.tsx
|
||||||
- Personal dashboard: @apps/web/app/home/(user)/page.tsx
|
- Personal dashboard: @apps/web/app/home/(user)/page.tsx
|
||||||
@@ -83,11 +78,11 @@ See complete structure in @apps/web/app/ with examples like:
|
|||||||
|
|
||||||
### Component Organization
|
### Component Organization
|
||||||
|
|
||||||
- **Route-specific**: Use `_components/` directories
|
- **Route-specific**: Use \_components/ directories
|
||||||
- **Route utilities**: Use `_lib/` for client, `_lib/server/` for server-side
|
- **Route utilities**: Use \_lib/ for client, \_lib/server/ for server-side
|
||||||
- **Global components**: Root-level directories
|
- **Global components**: Root-level directories
|
||||||
|
|
||||||
Example organization:
|
Example:
|
||||||
|
|
||||||
- Team components: @apps/web/app/home/[account]/\_components/
|
- Team components: @apps/web/app/home/[account]/\_components/
|
||||||
- Team server utils: @apps/web/app/home/[account]/\_lib/server/
|
- Team server utils: @apps/web/app/home/[account]/\_lib/server/
|
||||||
@@ -95,69 +90,242 @@ Example organization:
|
|||||||
|
|
||||||
## Database Guidelines
|
## Database Guidelines
|
||||||
|
|
||||||
### Security & RLS
|
### Security & RLS Implementation
|
||||||
|
|
||||||
|
**Critical Security Guidelines - Read Carefully! ⚠️**
|
||||||
|
|
||||||
|
#### Database Security Fundamentals
|
||||||
|
|
||||||
- **Always enable RLS** on new tables unless explicitly instructed otherwise
|
- **Always enable RLS** on new tables unless explicitly instructed otherwise
|
||||||
- Use helper functions for access control:
|
- **NEVER use SECURITY DEFINER functions** without explicit access controls - they bypass RLS entirely
|
||||||
- `public.has_role_on_account(account_id, role?)` - Check team membership
|
- **Always use security_invoker=true for views** to maintain proper access control
|
||||||
- `public.has_permission(user_id, account_id, permission)` - Check permissions
|
- **Storage buckets MUST validate access** using account_id in the path structure. See @apps/web/supabase/schemas/16-storage.sql for proper implementation.
|
||||||
- `public.is_account_owner(account_id)` - Verify ownership
|
- **Use locks if required**: Database locks prevent race conditions and timing attacks in concurrent operations. Make sure to take these into account for all database operations.
|
||||||
|
|
||||||
See RLS examples in database schemas: @apps/web/supabase/schemas/
|
#### Security Definer Function - Dangerous Pattern ❌
|
||||||
|
|
||||||
### Schema Management
|
```sql
|
||||||
|
-- NEVER DO THIS - Allows any authenticated user to call function
|
||||||
|
CREATE OR REPLACE FUNCTION public.dangerous_function()
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER AS $
|
||||||
|
BEGIN
|
||||||
|
-- This bypasses all RLS policies!
|
||||||
|
DELETE FROM sensitive_table; -- Anyone can call this!
|
||||||
|
END;
|
||||||
|
$;
|
||||||
|
GRANT EXECUTE ON FUNCTION public.dangerous_function() TO authenticated;
|
||||||
|
```
|
||||||
|
|
||||||
- Schemas in `apps/web/supabase/schemas/`
|
#### Security Definer Function - Safe Pattern ✅
|
||||||
- Create as `<number>-<name>.sql`
|
|
||||||
- After changes: `pnpm --filter web supabase:db:diff` then `pnpm supabase:web:reset`
|
```sql
|
||||||
|
-- ONLY use SECURITY DEFINER with explicit access validation
|
||||||
|
CREATE OR REPLACE FUNCTION public.safe_admin_function(target_account_id uuid)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = '' AS $
|
||||||
|
BEGIN
|
||||||
|
-- MUST validate caller has permission FIRST
|
||||||
|
IF NOT public.is_account_owner(target_account_id) THEN
|
||||||
|
RAISE EXCEPTION 'Access denied: insufficient permissions';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Now safe to proceed with elevated privileges
|
||||||
|
-- Your admin operation here
|
||||||
|
END;
|
||||||
|
$;
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Existing Helper Functions - Use These! 📚
|
||||||
|
|
||||||
|
**DO NOT recreate these functions - they already exist:**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Account Access Control
|
||||||
|
public.has_role_on_account(account_id, role?) -- Check team membership
|
||||||
|
public.has_permission(user_id, account_id, permission) -- Check permissions
|
||||||
|
public.is_account_owner(account_id) -- Verify ownership
|
||||||
|
public.has_active_subscription(account_id) -- Subscription status
|
||||||
|
public.is_team_member(account_id, user_id) -- Direct membership check
|
||||||
|
public.can_action_account_member(target_account_id, target_user_id) -- Member action rights
|
||||||
|
|
||||||
|
-- Administrative Functions
|
||||||
|
public.is_super_admin() -- Super admin check
|
||||||
|
public.is_aal2() -- MFA verification
|
||||||
|
public.is_mfa_compliant() -- MFA compliance
|
||||||
|
|
||||||
|
-- Configuration
|
||||||
|
public.is_set(field_name) -- Feature flag checks
|
||||||
|
```
|
||||||
|
|
||||||
|
Always check @apps/web/supabase/schemas/ before creating new functions!
|
||||||
|
|
||||||
|
#### RLS Policy Best Practices ✅
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Proper RLS using existing helper functions
|
||||||
|
CREATE POLICY "notes_read" ON public.notes FOR SELECT
|
||||||
|
TO authenticated USING (
|
||||||
|
account_id = (select auth.uid()) OR
|
||||||
|
public.has_role_on_account(account_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- For operations requiring specific permissions
|
||||||
|
CREATE POLICY "notes_manage" ON public.notes FOR ALL
|
||||||
|
TO authenticated USING (
|
||||||
|
public.has_permission(auth.uid(), account_id, 'notes.manage'::app_permissions)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Management Workflow
|
||||||
|
|
||||||
|
1. Create schemas in @apps/web/supabase/schemas/ as `<number>-<name>.sql`
|
||||||
|
2. After changes: `pnpm supabase:web:stop`
|
||||||
|
3. Run: `pnpm --filter web run supabase:db:diff -f <filename>`
|
||||||
|
4. Restart: `pnpm supabase:web:start` and `pnpm supabase:web:reset`
|
||||||
|
5. Generate types: `pnpm supabase:web:typegen`
|
||||||
|
|
||||||
Key schema files:
|
Key schema files:
|
||||||
|
|
||||||
- Accounts: `apps/web/supabase/schemas/03-accounts.sql`
|
- Accounts: @apps/web/supabase/schemas/03-accounts.sql
|
||||||
- Memberships: `apps/web/supabase/schemas/05-memberships.sql`
|
- Memberships: @apps/web/supabase/schemas/05-memberships.sql
|
||||||
- Permissions: `apps/web/supabase/schemas/06-roles-permissions.sql`
|
- Permissions: @apps/web/supabase/schemas/06-roles-permissions.sql
|
||||||
|
|
||||||
### Type Generation
|
### Type Generation
|
||||||
|
|
||||||
Import auto-generated types from @packages/supabase/src/types/database.ts:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
|
|
||||||
type Account = Tables<'accounts'>;
|
type Account = Tables<'accounts'>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Always prefer inferring types from generated Database types.
|
||||||
|
|
||||||
## Development Patterns
|
## Development Patterns
|
||||||
|
|
||||||
### Data Fetching
|
### Data Fetching Strategy
|
||||||
|
|
||||||
- **Server Components**: Use `getSupabaseServerClient()` from @packages/supabase/src/clients/server-client.ts
|
**Quick Decision Framework:**
|
||||||
- **Client Components**: Use `useSupabase()` hook + React Query's `useQuery`
|
|
||||||
- **Admin Operations**: Use `getSupabaseServerAdminClient()` from @packages/supabase/src/clients/server-admin-client.ts (rare cases only - bypasses RLS, use with caution!)
|
|
||||||
- Prefer Server Components and pass data down when needed
|
|
||||||
|
|
||||||
Use the Container/Presenter pattern for complex data components:
|
- **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
|
```typescript
|
||||||
// Container: handles data fetching
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
function UserProfileContainer() {
|
|
||||||
const userData = useUserData();
|
|
||||||
return <UserProfilePresenter data={userData.data} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Presenter: handles UI rendering
|
async function NotesPage() {
|
||||||
function UserProfilePresenter({ data }: { data: UserData }) {
|
const client = getSupabaseServerClient();
|
||||||
return <div>{data.name}</div>;
|
const { data, error } = await client.from('notes').select('*');
|
||||||
|
|
||||||
|
if (error) return <ErrorMessage error={error} />;
|
||||||
|
return <NotesList notes={data} />;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Example server-side data loading:
|
**Key Insight**: Server Components automatically inherit RLS protection - no additional authorization checks needed!
|
||||||
|
|
||||||
- User workspace loader: @apps/web/app/home/(user)/\_lib/server/load-user-workspace.ts
|
#### Client Components (Interactive) 🖱️
|
||||||
- Team workspace loader: @apps/web/app/home/[account]/\_lib/server/team-account-workspace.loader.ts
|
|
||||||
- Data provider pattern: @packages/features/team-accounts/src/components/members/roles-data-provider.tsx
|
|
||||||
|
|
||||||
### Server Actions
|
```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.
|
||||||
|
|
||||||
|
### Server Actions Implementation
|
||||||
|
|
||||||
Always use `enhanceAction` from @packages/next/src/actions/index.ts:
|
Always use `enhanceAction` from @packages/next/src/actions/index.ts:
|
||||||
|
|
||||||
@@ -186,14 +354,14 @@ Example server actions:
|
|||||||
### Forms with React Hook Form & Zod
|
### Forms with React Hook Form & Zod
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 1. Define schema in separate file
|
// 1. Schema in separate file
|
||||||
export const CreateNoteSchema = z.object({
|
export const CreateNoteSchema = z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
content: z.string().min(1),
|
content: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. Client component with form
|
// 2. Client component with form
|
||||||
('use client');
|
'use client';
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(CreateNoteSchema),
|
resolver: zodResolver(CreateNoteSchema),
|
||||||
});
|
});
|
||||||
@@ -209,7 +377,7 @@ const onSubmit = (data) => {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
See form examples:
|
Form examples:
|
||||||
|
|
||||||
- Contact form: @apps/web/app/(marketing)/contact/\_components/contact-form.tsx
|
- Contact form: @apps/web/app/(marketing)/contact/\_components/contact-form.tsx
|
||||||
- Verify OTP form: @packages/otp/src/components/verify-otp-form.tsx
|
- Verify OTP form: @packages/otp/src/components/verify-otp-form.tsx
|
||||||
@@ -233,18 +401,21 @@ export const POST = enhanceRouteHandler(
|
|||||||
);
|
);
|
||||||
```
|
```
|
||||||
|
|
||||||
Example API routes:
|
|
||||||
|
|
||||||
- Billing webhook: @apps/web/app/api/billing/webhook/route.ts
|
|
||||||
- Database webhook: @apps/web/app/api/db/webhook/route.ts
|
|
||||||
|
|
||||||
## React & TypeScript Best Practices
|
## React & TypeScript Best Practices
|
||||||
|
|
||||||
|
### TS
|
||||||
|
|
||||||
|
- Write clean, clear, well-designed, explicit Typescript
|
||||||
|
- Use implicit type inference, unless impossible
|
||||||
|
- `any` and `unknown` are a code smell and must justified if used
|
||||||
|
- Handle errors gracefully using try/catch and appropriate error types.
|
||||||
|
|
||||||
### Components
|
### Components
|
||||||
|
|
||||||
- Use functional components with TypeScript
|
- Use functional components with TypeScript
|
||||||
- Always use `'use client'` directive for client components
|
- Always use 'use client' directive for client components
|
||||||
- Destructure props with proper TypeScript interfaces
|
- Destructure props with proper TypeScript interfaces
|
||||||
|
- Name files to match component name (e.g., user-profile.tsx)
|
||||||
|
|
||||||
### Conditional Rendering
|
### Conditional Rendering
|
||||||
|
|
||||||
@@ -252,7 +423,6 @@ Use the `If` component from @packages/ui/src/makerkit/if.tsx:
|
|||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
import { Spinner } '@kit/ui/spinner';
|
|
||||||
|
|
||||||
<If condition={isLoading} fallback={<Content />}>
|
<If condition={isLoading} fallback={<Content />}>
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -266,12 +436,9 @@ import { Spinner } '@kit/ui/spinner';
|
|||||||
|
|
||||||
### Testing Attributes
|
### Testing Attributes
|
||||||
|
|
||||||
Add data attributes for testing:
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
<button data-test="submit-button">Submit</button>
|
<button data-test="submit-button">Submit</button>
|
||||||
<div data-test="user-profile" data-user-id={user.id}>Profile</div>
|
<div data-test="user-profile" data-user-id={user.id}>Profile</div>
|
||||||
<form data-test="signup-form">Form content</form>
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Internationalization
|
### Internationalization
|
||||||
@@ -281,11 +448,9 @@ Always use `Trans` component from @packages/ui/src/makerkit/trans.tsx:
|
|||||||
```tsx
|
```tsx
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
// Basic usage
|
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey="user:welcomeMessage"
|
i18nKey="user:welcomeMessage"
|
||||||
values={{ name: user.name }}
|
values={{ name: user.name }}
|
||||||
defaults="Welcome, {name}!"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
// With HTML elements
|
// With HTML elements
|
||||||
@@ -294,60 +459,35 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
components={{
|
components={{
|
||||||
TermsLink: <a href="/terms" className="underline" />,
|
TermsLink: <a href="/terms" className="underline" />,
|
||||||
}}
|
}}
|
||||||
defaults="I agree to the <TermsLink>Terms</TermsLink>."
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
// Pluralization
|
|
||||||
<Trans
|
|
||||||
i18nKey="notifications:count"
|
|
||||||
count={notifications.length}
|
|
||||||
defaults="{count, plural, =0 {No notifications} one {# notification} other {# notifications}}"
|
|
||||||
/>
|
|
||||||
```
|
|
||||||
|
|
||||||
Use `LanguageSelector` component from @packages/ui/src/makerkit/language-selector.tsx:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { LanguageSelector } from '@kit/ui/language-selector';
|
|
||||||
|
|
||||||
<LanguageSelector />;
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Adding new languages:
|
Adding new languages:
|
||||||
|
|
||||||
1. Add language code to @apps/web/lib/i18n/i18n.settings.ts
|
1. Add language code to @apps/web/lib/i18n/i18n.settings.ts
|
||||||
2. Create translation files in @apps/web/public/locales/[new-language]/
|
2. Create translation files in @apps/web/public/locales/[new-language]/
|
||||||
3. Copy structure from English files as template
|
3. Copy structure from English files
|
||||||
|
|
||||||
Adding new namespaces:
|
Translation files: @apps/web/public/locales/<locale>/<namespace>.json
|
||||||
|
|
||||||
1. Add namespace to `defaultI18nNamespaces` in @apps/web/lib/i18n/i18n.settings.ts
|
## Security Guidelines 🛡️
|
||||||
2. Create corresponding translation files for all supported languages
|
|
||||||
|
|
||||||
Translation files located in @apps/web/public/locales/<locale>/<namespace>.json:
|
|
||||||
|
|
||||||
- Common translations: @apps/web/public/locales/en/common.json
|
|
||||||
- Auth translations: @apps/web/public/locales/en/auth.json
|
|
||||||
- Team translations: @apps/web/public/locales/en/teams.json
|
|
||||||
|
|
||||||
## Security Guidelines
|
|
||||||
|
|
||||||
### Authentication & Authorization
|
### Authentication & Authorization
|
||||||
|
|
||||||
- Authentication enforced by middleware
|
- Authentication enforced by middleware
|
||||||
- Authorization typically handled by RLS at database level, unless using the admin client
|
- Authorization handled by RLS at database level
|
||||||
- For rare admin client usage, enforce both manually
|
- Avoid defensive code - use RLS instead
|
||||||
- User authentication helper: @apps/web/lib/server/require-user-in-server-component.ts if required or to obtain the authed user
|
- For admin client usage, enforce both authentication and authorization
|
||||||
|
|
||||||
### Data Passing
|
### Data Passing
|
||||||
|
|
||||||
- **Never pass sensitive data** to Client Components
|
- **Never pass sensitive data** to Client Components
|
||||||
- **Never expose server environment variables** to client (unless `NEXT_PUBLIC_`)
|
- **Never expose server environment variables** to client (unless prefixed with NEXT_PUBLIC)
|
||||||
- Always validate user input before processing
|
- Always validate user input
|
||||||
|
|
||||||
### OTP for Sensitive Operations
|
### OTP for Sensitive Operations
|
||||||
|
|
||||||
Use one-time tokens from @packages/otp/src/api/index.ts for destructive operations:
|
Use one-time tokens from @packages/otp/src/api/index.ts:
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { VerifyOtpForm } from '@kit/otp/components';
|
import { VerifyOtpForm } from '@kit/otp/components';
|
||||||
@@ -361,8 +501,6 @@ import { VerifyOtpForm } from '@kit/otp/components';
|
|||||||
/>;
|
/>;
|
||||||
```
|
```
|
||||||
|
|
||||||
OTP schema and functions: @apps/web/supabase/schemas/12-one-time-tokens.sql
|
|
||||||
|
|
||||||
### Super Admin Protection
|
### Super Admin Protection
|
||||||
|
|
||||||
For admin routes, use `AdminGuard` from @packages/features/admin/src/components/admin-guard.tsx:
|
For admin routes, use `AdminGuard` from @packages/features/admin/src/components/admin-guard.tsx:
|
||||||
@@ -373,35 +511,7 @@ import { AdminGuard } from '@kit/admin/components/admin-guard';
|
|||||||
export default AdminGuard(AdminPageComponent);
|
export default AdminGuard(AdminPageComponent);
|
||||||
```
|
```
|
||||||
|
|
||||||
For admin server actions, use `adminAction` wrapper:
|
## UI Components 🎨
|
||||||
|
|
||||||
```tsx
|
|
||||||
import { adminAction } from '@kit/admin';
|
|
||||||
|
|
||||||
export const yourAdminAction = adminAction(
|
|
||||||
enhanceAction(
|
|
||||||
async (data) => {
|
|
||||||
// Action implementation
|
|
||||||
},
|
|
||||||
{ schema: YourActionSchema },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Admin service security pattern:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
private async assertUserIsNotCurrentSuperAdmin(targetId: string) {
|
|
||||||
const { data } = await this.client.auth.getUser();
|
|
||||||
const currentUserId = data.user?.id;
|
|
||||||
|
|
||||||
if (currentUserId === targetId) {
|
|
||||||
throw new Error('Cannot perform destructive action on your own account');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## UI Components
|
|
||||||
|
|
||||||
### Core UI Library
|
### Core UI Library
|
||||||
|
|
||||||
@@ -410,40 +520,23 @@ Import from @packages/ui/src/:
|
|||||||
```tsx
|
```tsx
|
||||||
// Shadcn components
|
// Shadcn components
|
||||||
import { Button } from '@kit/ui/button';
|
import { Button } from '@kit/ui/button';
|
||||||
// @packages/ui/src/shadcn/button.tsx
|
|
||||||
import { Card } from '@kit/ui/card';
|
import { Card } from '@kit/ui/card';
|
||||||
// @packages/ui/src/shadcn/sonner.tsx
|
|
||||||
|
|
||||||
// Makerkit components
|
// Makerkit components
|
||||||
import { If } from '@kit/ui/if';
|
import { If } from '@kit/ui/if';
|
||||||
// @packages/ui/src/makerkit/trans.tsx
|
|
||||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||||
// @packages/ui/src/shadcn/card.tsx
|
|
||||||
import { toast } from '@kit/ui/sonner';
|
import { toast } from '@kit/ui/sonner';
|
||||||
// @packages/ui/src/makerkit/if.tsx
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
// @packages/ui/src/makerkit/profile-avatar.tsx
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Key Component Categories
|
|
||||||
|
|
||||||
- **Forms**: Form components in @packages/ui/src/shadcn/form.tsx
|
|
||||||
- **Navigation**: Navigation menu in @packages/ui/src/shadcn/navigation-menu.tsx
|
|
||||||
- **Data Display**: Data table in @packages/ui/src/makerkit/data-table.tsx
|
|
||||||
- **Marketing**: Marketing components in @packages/ui/src/makerkit/marketing/
|
|
||||||
|
|
||||||
### Styling
|
### Styling
|
||||||
|
|
||||||
- Use Tailwind CSS with semantic classes
|
- Use Tailwind CSS v4 with semantic classes
|
||||||
- Prefer `bg-background`, `text-muted-foreground` over fixed colors
|
- Prefer Shadcn-ui classes like `bg-background`, `text-muted-foreground`
|
||||||
- Use `cn()` utility from @packages/ui/src/lib/utils.ts for class merging
|
- Use `cn()` utility from @kit/ui/cn for class merging
|
||||||
|
|
||||||
## Workspace Contexts
|
## Workspace Contexts 🏢
|
||||||
|
|
||||||
### Personal Account Context (`/home/(user)`)
|
### Personal Account Context (@apps/web/app/home/(user))
|
||||||
|
|
||||||
Use hook from `packages/features/accounts/src/hooks/use-user-workspace.ts`:
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
|
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
|
||||||
@@ -454,11 +547,9 @@ function PersonalComponent() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Context provider: `packages/features/accounts/src/components/user-workspace-context-provider.tsx`
|
Context provider: @packages/features/accounts/src/components/user-workspace-context-provider.tsx
|
||||||
|
|
||||||
### Team Account Context (`/home/[account]`)
|
### Team Account Context (@apps/web/app/home/[account])
|
||||||
|
|
||||||
Use hook from `packages/features/team-accounts/src/hooks/use-team-account-workspace.ts`:
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
|
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
|
||||||
@@ -469,89 +560,59 @@ function TeamComponent() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Context provider: `packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx`
|
Context provider: @packages/features/team-accounts/src/components/team-account-workspace-context-provider.tsx
|
||||||
|
|
||||||
## Error Handling & Logging
|
## Error Handling & Logging 📊
|
||||||
|
|
||||||
### Structured Logging
|
### Structured Logging
|
||||||
|
|
||||||
Use logger from `packages/shared/src/logger/logger.ts`:
|
Use logger from @packages/shared/src/logger/logger.ts:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { getLogger } from '@kit/shared/logger';
|
import { getLogger } from '@kit/shared/logger';
|
||||||
|
|
||||||
const logger = await getLogger();
|
async function myServerAction() {
|
||||||
const ctx = { name: 'myOperation', userId: user.id };
|
const logger = await getLogger();
|
||||||
|
const ctx = { name: 'myOperation', userId: user.id };
|
||||||
|
|
||||||
logger.info(ctx, 'Operation started');
|
try {
|
||||||
// ... operation
|
logger.info(ctx, 'Operation started');
|
||||||
logger.error({ ...ctx, error }, 'Operation failed');
|
// ...
|
||||||
```
|
} catch (error) {
|
||||||
|
logger.error({ ...ctx, error }, 'Operation failed');
|
||||||
### Error Boundaries
|
// handle error
|
||||||
|
}
|
||||||
Use proper error handling with meaningful user messages:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
try {
|
|
||||||
await operation();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error({ error, context }, 'Operation failed');
|
|
||||||
return { error: 'Unable to complete operation' }; // Generic message
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Feature Development Workflow
|
|
||||||
|
|
||||||
### Creating New Pages
|
|
||||||
|
|
||||||
1. Create page component in appropriate route group
|
|
||||||
2. Add `withI18n()` HOC from `apps/web/lib/i18n/with-i18n.tsx`
|
|
||||||
3. Implement `generateMetadata()` for SEO
|
|
||||||
4. Add loading state with `loading.tsx`
|
|
||||||
5. Create components in `_components/` directory
|
|
||||||
6. Add server utilities in `_lib/server/`
|
|
||||||
|
|
||||||
Example page structure:
|
|
||||||
|
|
||||||
- Marketing page: `apps/web/app/(marketing)/pricing/page.tsx`
|
|
||||||
- Dashboard page: `apps/web/app/home/(user)/page.tsx`
|
|
||||||
- Team page: `apps/web/app/home/[account]/members/page.tsx`
|
|
||||||
|
|
||||||
### Permission Patterns
|
|
||||||
|
|
||||||
- Check permissions before data operations
|
|
||||||
- Guard premium features with `public.has_active_subscription`
|
|
||||||
- Use role hierarchy for member management
|
|
||||||
- Primary account owners have special privileges
|
|
||||||
|
|
||||||
Permission helpers in database: `apps/web/supabase/schemas/06-roles-permissions.sql`
|
|
||||||
|
|
||||||
### Database Development
|
|
||||||
|
|
||||||
1. Create schema file: `apps/web/supabase/schemas/<number>-<name>.sql`
|
|
||||||
2. Enable RLS and create policies
|
|
||||||
3. Generate migration: `pnpm --filter web supabase:db:diff`
|
|
||||||
4. Reset database: `pnpm supabase:web:reset`
|
|
||||||
5. Generate types: `pnpm supabase:web:typegen`
|
|
||||||
|
|
||||||
## API Services
|
## API Services
|
||||||
|
|
||||||
### Account Services
|
### Account Services
|
||||||
|
|
||||||
- Personal accounts API: `packages/features/accounts/src/server/api.ts`
|
- Personal accounts API: @packages/features/accounts/src/server/api.ts
|
||||||
- Team accounts API: `packages/features/team-accounts/src/server/api.ts`
|
- Team accounts API: @packages/features/team-accounts/src/server/api.ts
|
||||||
- Admin service: `packages/features/admin/src/lib/server/services/admin.service.ts`
|
- Admin service: @packages/features/admin/src/lib/server/services/admin.service.ts
|
||||||
|
|
||||||
### Billing Services
|
### Billing Services
|
||||||
|
|
||||||
- Personal billing: `apps/web/app/home/(user)/billing/_lib/server/user-billing.service.ts`
|
- Personal billing: @apps/web/app/home/(user)/billing/\_lib/server/user-billing.service.ts
|
||||||
- Team billing: `apps/web/app/home/[account]/billing/_lib/server/team-billing.service.ts`
|
- Team billing: @apps/web/app/home/[account]/billing/\_lib/server/team-billing.service.ts
|
||||||
- Per-seat billing: `packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts`
|
- Per-seat billing: @packages/features/team-accounts/src/server/services/account-per-seat-billing.service.ts
|
||||||
|
|
||||||
## Key Configuration Files
|
## Key Configuration Files
|
||||||
|
|
||||||
- **Feature flags**: @apps/web/config/feature-flags.config.ts
|
- **Feature flags**: @apps/web/config/feature-flags.config.ts
|
||||||
- **i18n settings**: @apps/web/lib/i18n/i18n.settings.ts
|
- **i18n settings**: @apps/web/lib/i18n/i18n.settings.ts
|
||||||
- **Supabase local config**: @apps/web/supabase/config.toml@
|
- **Supabase config**: @apps/web/supabase/config.toml
|
||||||
- **Middleware**: @apps/web/middleware.ts`
|
- **Middleware**: @apps/web/middleware.ts
|
||||||
|
|
||||||
|
## Quick Reference Checklist ✅
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
- [ ] Enable RLS on new tables
|
||||||
|
- [ ] Generate TypeScript types after schema changes and infer types from these
|
||||||
|
- [ ] Implement proper error handling with logging
|
||||||
|
- [ ] Use Zod schemas for parsing all user input (including cookies, query params, etc.)
|
||||||
|
- [ ] Add testing attributes to interactive elements
|
||||||
|
- [ ] Validate permissions before sensitive operations
|
||||||
Reference in New Issue
Block a user