* Add CSP nonce support and enhance security headers Introduced secure headers and CSP nonce to improve app security by integrating `@nosecone/next`. Updated middleware, root providers, and layout to handle nonce propagation, enabling stricter CSP policies when configured. Also upgraded dependencies and tooling versions. * Add OTP and security guidelines documentation and additional checks on client-provided values - Introduced additional checks on client-provided values such as cookies - Introduced a new OTP API documentation outlining the creation and verification of OTP tokens for sensitive operations. - Added comprehensive security guidelines for writing secure code in Next.js, covering client and server components, environment variables, authentication, and error handling. These additions enhance the project's security posture and provide clear instructions for developers on implementing secure practices.
62 KiB
Typescript
- Write clean, clear, well-designed, explicit Typescript
- Make sure types are validated strictly
- Use implicit type inference, unless impossible
- Consider using classes for server-side services, but export a function instead of the class
// service.ts
class UserService {
getUser(id: number) {
// ... implementation ...
return { id, name: 'Example User' };
}
}
export function createUserService() {
return new UserService();
}
- Follow the Single Responsibility Principle (SRP). Each module/function/class should have one reason to change.
- Favor composition over inheritance.
- Handle errors gracefully using try/catch and appropriate error types.
- Keep functions short and focused.
- Use descriptive names for variables, functions, and classes.
- Avoid unnecessary complexity.
- Avoid using
anytype as much as possible. If necessary, useunknown - Use enums only when appropriate. Consider union types of string literals as an alternative.
- Be aware of performance implications of your code.
React
Core Principles
- Component-Driven Development: Build applications as a composition of isolated, reusable components
- One-Way Data Flow: Follow React's unidirectional data flow pattern
- Single Responsibility: Each component should have a clear, singular purpose
- TypeScript First: Use TypeScript for type safety and better developer experience
- Internationalization (i18n) By Default: All user-facing text should be translatable
React Components
Component Structure
- Always use functional components with TypeScript
- Name components using PascalCase (e.g.,
UserProfile) - Use named exports for components, not default exports
- Split components by responsibility and avoid "god components"
- Name files to match their component name (e.g.,
user-profile.tsx)
Props
- Always type props using TypeScript interfaces or type aliases
- Use discriminated unions for complex prop types with conditional rendering
- Destructure props at the start of component functions
- Use prop spreading cautiously and only when appropriate
- Provide default props for optional parameters when it makes sense
type ButtonProps = {
variant: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
children: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
};
function Button({
variant,
size = 'md',
children,
disabled = false,
onClick
}: ButtonProps) {
// Component implementation
}
State Management
- Keep state as local as possible
- Lift state up when multiple components need access
- Use Context sparingly and only for truly global state
- Prefer the "Container/Presenter" pattern when separating data and UI
// Container component (manages data)
function UserProfileContainer() {
const userData = useUserData();
if (userData.isLoading) {
return <LoadingSpinner />;
}
if (userData.error) {
return <ErrorMessage error={userData.error} />;
}
return <UserProfilePresenter data={userData.data} />;
}
// Presenter component (renders UI)
function UserProfilePresenter({ data }: { data: UserData }) {
return (
<div>
<h1>{data.name}</h1>
{/* Rest of the UI */}
</div>
);
}
Hooks
- Follow the Rules of Hooks (only call hooks at the top level, only call them from React functions)
- Create custom hooks for reusable logic
- Keep custom hooks focused on a single concern
- Name custom hooks with a 'use' prefix (e.g.,
useUserProfile) - Extract complex effect logic into separate functions
- Always provide a complete dependencies array to
useEffect
Performance Optimization
- Apply
useMemofor expensive calculations - Use
useCallbackfor functions passed as props to child components - Split code using dynamic imports and
React.lazy()
const MemoizedComponent = React.memo(function Component(props: Props) {
// Component implementation
});
// For expensive calculations
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
// For callback functions passed as props
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
Internationalization (i18n)
- Always use the
Transcomponent for text rendering (no hardcoded strings) - Ensure all i18n keys are available in locale files
- Use namespaces to organize translations logically
- Include interpolation variables in translation keys
- Test UI with different languages, especially those with longer text
// Correct
<Trans i18nKey="user:profile.welcomeMessage" values={{ name: user.name }} />
// Incorrect
<p>Welcome, {user.name}!</p>
Server Components
Fundamentals
- Server Components render React server-side and never run on the client
- Use Server Components as the default choice, especially for data fetching
- No use of hooks, browser APIs, or event handlers in Server Components
- No use of
useState,useEffect, or any other React hooks - Server Components can render Client Components but not vice versa
Data Fetching
- Fetch data directly using async/await in Server Components
- Use Suspense boundaries around data-fetching components
- Apply security checks before fetching sensitive data
- Never pass sensitive data (API keys, tokens) to Client Components
- Use React's
cache()function for caching data requests
Error Handling
- Implement error boundaries at appropriate levels
- Use the Next.js
error.tsxfile for route-level error handling - Create fallback UI for when data fetching fails
- Log server errors appropriately without exposing details to clients
Streaming and Suspense
- Use React Suspense for progressive loading experiences if specified
- Implement streaming rendering for large or complex pages
- Structure components to enable meaningful loading states
- Prioritize above-the-fold content when using streaming
Client Components
Fundamentals
- Add the
'use client'directive at the top of files for Client Components - Keep Client Components focused on interactivity and browser APIs
- Use hooks appropriately following the Rules of Hooks
- Implement controlled components for form elements
- Handle all browser events in Client Components
Data Fetching
- Use React Query (TanStack Query) for data fetching in Client Components
- Create custom hooks for data fetching logic (e.g.,
useUserData) - Always handle loading, success, and error states
Form Handling
- Use libraries like React Hook Form for complex forms
- Implement proper validation with libraries like Zod
- Create reusable form components
- Handle form submissions with loading and error states
- Use controlled components for form inputs
Error Handling
- Implement error boundaries to catch and handle component errors if using client components
- Always handle network request errors
- Provide user-friendly error messages
- Log errors appropriately
- Implement retry mechanisms where applicable
'use client';
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export function UserProfileWithErrorHandling() {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// Reset application state here if needed
}}
>
<UserProfile userId="123" />
</ErrorBoundary>
);
}
Project Structure
apps/web/app/ # Root directory (apps/web/app)
│
├── (marketing)/ # Marketing pages group
│ ├── _components/ # Shared components for marketing routes
│ │ ├── site-footer.tsx
│ │ ├── site-header.tsx
│ │ ├── site-navigation.tsx
│ │ └── site-page-header.tsx
│ │
│ ├── (legal)/ # Legal pages subgroup
│ │ ├── cookie-policy/
│ │ │ └── page.tsx
│ │ ├── privacy-policy/
│ │ │ └── page.tsx
│ │ └── terms-of-service/
│ │ └── page.tsx
│ │
│ ├── blog/ # Blog section
│ │ ├── _components/ # Blog-specific components
│ │ │ ├── blog-pagination.tsx
│ │ │ ├── post-header.tsx
│ │ │ └── post-preview.tsx
│ │ ├── [slug]/ # Dynamic route for blog posts
│ │ │ └── page.tsx
│ │ └── page.tsx # Blog listing page
│ │
│ ├── contact/ # Contact page
│ │ ├── _components/
│ │ │ └── contact-form.tsx
│ │ ├── _lib/ # Contact page utilities
│ │ │ ├── contact-email.schema.ts
│ │ │ └── server/
│ │ │ └── server-actions.ts
│ │ └── page.tsx
│ │
│ ├── docs/ # Documentation pages
│ │ ├── _components/
│ │ ├── _lib/
│ │ │ ├── server/
│ │ │ │ └── docs.loader.ts
│ │ │ └── utils.ts
│ │ ├── [slug]/
│ │ │ └── page.tsx
│ │ ├── layout.tsx # Layout specific to docs section
│ │ └── page.tsx
│ │
│ ├── faq/
│ │ └── page.tsx
│ │
│ ├── pricing/
│ │ └── page.tsx
│ │
│ ├── layout.tsx # Layout for all marketing pages
│ ├── loading.tsx # Loading state for marketing pages
│ └── page.tsx # Home/landing page
│
├── (auth)/ # Authentication pages group
│ ├── callback/ # Auth callback routes
│ │ ├── error/
│ │ │ └── page.tsx
│ │ └── route.ts # API route handler for auth callback
│ │
│ ├── confirm/
│ │ └── route.ts
│ │
│ ├── password-reset/
│ │ └── page.tsx
│ │
│ ├── sign-in/
│ │ └── page.tsx
│ │
│ ├── sign-up/
│ │ └── page.tsx
│ │
│ ├── verify/
│ │ └── page.tsx
│ │
│ ├── layout.tsx # Layout for auth pages
│ └── loading.tsx # Loading state for auth pages
│
├── admin/ # Admin section
│ ├── _components/
│ │ ├── admin-sidebar.tsx
│ │ └── mobile-navigation.tsx
│ │
│ ├── accounts/
│ │ ├── [id]/
│ │ │ └── page.tsx
│ │ └── page.tsx
│ │
│ ├── layout.tsx
│ ├── loading.tsx
│ └── page.tsx
│
├── api/ # API routes
│ ├── billing/
│ │ └── webhook/
│ │ └── route.ts
│ │
│ └── db/
│ └── webhook/
│ └── route.ts
│
├── home/ # User dashboard area
│ ├── (user)/ # Personal user routes
│ │ ├── _components/ # User dashboard components
│ │ │ ├── home-account-selector.tsx
│ │ │ └── home-sidebar.tsx
│ │ │
│ │ ├── _lib/ # User dashboard utilities
│ │ │ └── server/
│ │ │ └── load-user-workspace.ts
│ │ │
│ │ ├── billing/ # Personal account billing
│ │ │ ├── _components/
│ │ │ ├── _lib/
│ │ │ │ ├── schema/
│ │ │ │ │ └── personal-account-checkout.schema.ts
│ │ │ │ └── server/
│ │ │ │ ├── personal-account-billing-page.loader.ts
│ │ │ │ ├── server-actions.ts
│ │ │ │ └── user-billing.service.ts
│ │ │ │
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── return/
│ │ │ └── page.tsx
│ │ │
│ │ ├── settings/
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ │
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │
│ ├── [account]/ # Team account routes (dynamic)
│ │ ├── _components/ # Team account components
│ │ │ ├── dashboard-demo.tsx
│ │ │ ├── team-account-accounts-selector.tsx
│ │ │ └── team-account-layout-sidebar.tsx
│ │ │
│ │ ├── _lib/ # Team account utilities
│ │ │ └── server/
│ │ │ ├── team-account-billing-page.loader.ts
│ │ │ └── team-account-workspace.loader.ts
│ │ │
│ │ ├── billing/ # Team billing section
│ │ │ ├── _components/
│ │ │ ├── _lib/
│ │ │ │ ├── schema/
│ │ │ │ │ └── team-billing.schema.ts
│ │ │ │ └── server/
│ │ │ │ ├── server-actions.ts
│ │ │ │ └── team-billing.service.ts
│ │ │ │
│ │ │ ├── error.tsx
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ └── return/
│ │ │ └── page.tsx
│ │ │
│ │ ├── members/ # Team members management
│ │ │ ├── _lib/
│ │ │ │ └── server/
│ │ │ │ └── members-page.loader.ts
│ │ │ └── page.tsx
│ │ │
│ │ ├── settings/
│ │ │ └── page.tsx
│ │ │
│ │ ├── layout.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │
│ └── loading.tsx
│
├── join/ # Team join page
│ └── page.tsx
│
├── update-password/
│ └── page.tsx
│
├── error.tsx # Global error page
├── global-error.tsx # Global error component
├── layout.tsx # Root layout
├── not-found.tsx # 404 page
├── robots.ts # Robots.txt config
├── sitemap.xml/ # Sitemap generation
│ └── route.ts
└── version/ # Version info endpoint
└── route.ts
Key Organization Patterns
-
Route Groups
(marketing)- Groups all marketing/public pages(auth)- Groups all authentication related pages(user)- Groups all personal user dashboard pages
-
Component Organization
_components/- Route-specific components- Global components are in the root
/componentsdirectory (not shown)
-
Utilities & Data
_lib/- Route-specific utilities, types, and helpers_lib/server/- Server-side utilities including data loaders/lib/- Global utilities (not shown)
-
Data Fetching
- Use of React's
cache()function for request deduplication
- Use of React's
-
Server Actions
server-actions.ts- Server-side actions for mutating data- Follows 'use server' directive pattern
-
Special Files
layout.tsx- Define layouts for routesloading.tsx- Loading UI for routeserror.tsx- Error handling for routespage.tsx- Page component for routesroute.ts- API route handlers
-
Dynamic Routes
[account]- Dynamic route for team accounts. The [account] property is the account slug in the tablepublic.accounts.[slug]- Dynamic route for blog posts and documentation
Creating Pages
Makerkit Page & Layout Guidelines
Page Structure Overview
Makerkit uses Next.js App Router architecture with a clear separation of concerns for layouts and pages. The application's structure reflects the multi-tenant approach with specific routing patterns:
- app
- home # protected routes
- (user) # user workspace (personal account context)
- [account] # team workspace (team account context)
- (marketing) # marketing pages
- auth # auth pages
Key Components
Layouts
Layouts in Makerkit provide the structure for various parts of the application:
- Root Layout: The base structure for the entire application
- Workspace Layouts:
- User Workspace Layout (
app/home/(user)/layout.tsx): For personal account context - Team Workspace Layout (
app/home/[account]/layout.tsx): For team account context
- User Workspace Layout (
Layouts handle:
- Workspace context providers
- Navigation components
- Authentication requirements
- UI structure (sidebar vs header style)
Pages
Pages represent the actual content for each route and follow a consistent pattern:
- Metadata Generation: Using
generateMetadata()for SEO and page titles - Content Structure:
- Page headers with titles and descriptions
- Page body containing the main content
- i18n Implementation: Wrapped with
withI18nHOC
Creating a New Page
1. Define the Page Structure
Create a new file within the appropriate route folder:
// app/home/(user)/my-feature/page.tsx
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
// Import components from the _components folder if needed
import { MyFeatureHeader } from './_components/my-feature-header';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('account:myFeaturePage');
return {
title,
};
};
function MyFeaturePage() {
return (
<>
<MyFeatureHeader
title={<Trans i18nKey={'common:routes.myFeature'} />}
description={<Trans i18nKey={'common:myFeatureDescription'} />}
/>
<PageBody>
{/* Main page content */}
</PageBody>
</>
);
}
export default withI18n(MyFeaturePage);
- Authentication is enforced already in the middleware
- Authorization is normally enforced by RLS at the database level
- In the rare case you use the Supabase Admin client, you must enforce both authentication and authorization manually
2. Create a Loading State
// app/home/(user)/my-feature/loading.tsx
import { GlobalLoader } from '@kit/ui/global-loader';
export default GlobalLoader;
3. Create a Layout (if needed)
If the feature requires a specific layout, create a layout file:
// app/home/(user)/my-feature/layout.tsx
import { use } from 'react';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { Page, PageNavigation } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
// Import components from the _components folder
import { MyFeatureNavigation } from './_components/my-feature-navigation';
function MyFeatureLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
return (
<UserWorkspaceContextProvider value={workspace}>
<Page>
<PageNavigation>
<MyFeatureNavigation workspace={workspace} />
</PageNavigation>
{children}
</Page>
</UserWorkspaceContextProvider>
);
}
export default withI18n(MyFeatureLayout);
Layout Patterns
1. User Workspace Layout
For pages in the personal account context, use the user workspace layout pattern:
import { use } from 'react';
import { UserWorkspaceContextProvider } from '@kit/accounts/components';
import { Page } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { loadUserWorkspace } from './_lib/server/load-user-workspace';
function MyLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
return (
<UserWorkspaceContextProvider value={workspace}>
<Page>
{/* Navigation components */}
{children}
</Page>
</UserWorkspaceContextProvider>
);
}
export default withI18n(MyLayout);
2. Team Workspace Layout
For pages in the team account context, use the team workspace layout pattern:
import { use } from 'react';
import { TeamAccountWorkspaceContextProvider } from '@kit/team-accounts/components';
import { Page } from '@kit/ui/page';
import { withI18n } from '~/lib/i18n/with-i18n';
import { loadTeamWorkspace } from './_lib/server/load-team-workspace';
function TeamLayout({ children, params }: LayoutParams) {
const workspace = use(loadTeamWorkspace(params.account));
return (
<TeamAccountWorkspaceContextProvider value={workspace}>
<Page>
{/* Navigation components */}
{children}
</Page>
</TeamAccountWorkspaceContextProvider>
);
}
export default withI18n(TeamLayout);
UI Components Structure
Page Components
Break down pages into reusable components:
-
Page Headers: Create header components for consistent titling:
// _components/my-feature-header.tsx import { PageHeader } from '@kit/ui/page-header'; export function MyFeatureHeader({ title, description }: { title: React.ReactNode, description: React.ReactNode }) { return ( <PageHeader title={title} description={description} /> ); } -
Feature Components: Create components for feature-specific functionality:
// _components/my-feature-component.tsx 'use client'; import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace'; export function MyFeatureComponent() { const { user } = useUserWorkspace(); return ( <div> {/* Component content */} </div> ); }
Navigation Components
Create navigation components to handle sidebar or header navigation:
// _components/my-feature-navigation.tsx
'use client';
import { NavigationMenu } from '@kit/ui/navigation-menu';
export function MyFeatureNavigation({
workspace
}: {
workspace: UserWorkspace
}) {
return (
<NavigationMenu>
{/* Navigation items */}
</NavigationMenu>
);
}
Layout Styles
Makerkit supports different layout styles that can be toggled by the user:
- Sidebar Layout: A vertical sidebar navigation
- Header Layout: A horizontal header navigation
The layout style is stored in cookies and can be accessed server-side:
async function getLayoutState() {
const cookieStore = await cookies();
const layoutStyleCookie = cookieStore.get('layout-style');
return {
style: layoutStyleCookie?.value ?? defaultStyle,
// Other layout state properties
};
}
Best Practices
-
Server vs. Client Components:
- Use Server Components for data fetching and initial rendering
- Use Client Components ('use client') for interactive elements
-
Data Loading:
- Load workspace data in layouts using server functions
- Pass data down to components that need it
- Use React Query for client-side data fetching
-
Component Organization:
- Place feature-specific components in a
_componentsfolder - Place feature-specific server utilities in a
_lib/serverfolder - Place feature-specific client utilities in a
_lib/clientfolder
- Place feature-specific components in a
-
i18n Support:
- Always use
withI18nHOC for pages and layouts - Use
<Trans>component for translated text - Define translation keys in the appropriate namespace in
apps/web/public/locales/<locale>/<namespace>.json
- Always use
-
Metadata:
- Always include
generateMetadatafor SEO and UX - Use translations for page titles and descriptions
- Always include
-
Loading States:
- Always provide a loading state for each route
- Use the
GlobalLoaderor custom loading components
-
Error Handling:
- Implement error.tsx files for route error boundaries
- Handle data fetching errors gracefully
UI Components
- Reusable UI components are defined in the "packages/ui" package named "@kit/ui".
- By exporting the component from the "exports" field, we can import it using the "@kit/ui/{component-name}" format.
Styling
- Styling is done using Tailwind CSS. We use the "cn" function from the "@kit/ui/utils" package to generate class names.
- Avoid fixes classes such as "bg-gray-500". Instead, use Shadcn classes such as "bg-background", "text-secondary-foreground", "text-muted-foreground", etc.
Makerkit leverages two sets of UI components:
- Shadcn UI Components: Base components from the Shadcn UI library
- Makerkit-specific Components: Custom components built on top of Shadcn UI
Importing Components
// Import Shadcn UI components
import { Button } from '@kit/ui/button';
import { Card } from '@kit/ui/card';
import { toast } from '@kit/ui/sonner';
// Import Makerkit-specific components
import { If } from '@kit/ui/if';
import { Trans } from '@kit/ui/trans';
import { ProfileAvatar } from '@kit/ui/profile-avatar';
Core Shadcn UI Components
| Component | Description | Import Path |
|---|---|---|
Accordion |
Expandable/collapsible content sections | @kit/ui/accordion accordion.tsx |
AlertDialog |
Modal dialog for important actions | @kit/ui/alert-dialog alert-dialog.tsx |
Alert |
Status/notification messages | @kit/ui/alert alert.tsx |
Avatar |
User profile images with fallback | @kit/ui/avatar avatar.tsx |
Badge |
Small status indicators | @kit/ui/badge badge.tsx |
Breadcrumb |
Navigation path indicators | @kit/ui/breadcrumb breadcrumb.tsx |
Button |
Clickable action elements | @kit/ui/button button.tsx |
Calendar |
Date picker and date display | @kit/ui/calendar calendar.tsx |
Card |
Container for grouped content | @kit/ui/card card.tsx |
Checkbox |
Selection input | @kit/ui/checkbox checkbox.tsx |
Command |
Command palette interface | @kit/ui/command command.tsx |
DataTable |
Table | @kit/ui/data-table data-table.tsx |
Dialog |
Modal window for focused interactions | @kit/ui/dialog dialog.tsx |
DropdownMenu |
Menu triggered by a button | @kit/ui/dropdown-menu dropdown-menu.tsx |
Form |
Form components with validation | @kit/ui/form form.tsx |
Input |
Text input field | @kit/ui/input input.tsx |
Input OTP |
OTP Text input field | @kit/ui/input-otp input-otp.tsx |
Label |
Text label for form elements | @kit/ui/label label.tsx |
NavigationMenu |
Hierarchical navigation component | @kit/ui/navigation-menu navigation-menu.tsx |
Popover |
Floating content triggered by interaction | @kit/ui/popover popover.tsx |
RadioGroup |
Radio button selection group | @kit/ui/radio-group radio-group.tsx |
ScrollArea |
Customizable scrollable area | @kit/ui/scroll-area scroll-area.tsx |
Select |
Dropdown selection menu | @kit/ui/select select.tsx |
Separator |
Visual divider between content | @kit/ui/separator separator.tsx |
Sheet |
Sliding panel from screen edge | @kit/ui/sheet sheet.tsx |
Sidebar |
Advanced sidebar navigation | @kit/ui/shadcn-sidebar sidebar.tsx |
Skeleton |
Loading placeholder | @kit/ui/skeleton skeleton.tsx |
Switch |
Toggle control | @kit/ui/switch switch.tsx |
Toast |
Toaster | @kit/ui/sonner sonner.tsx |
Tabs |
Tab-based navigation | @kit/ui/tabs tabs.tsx |
Textarea |
Multi-line text input | @kit/ui/textarea textarea.tsx |
Tooltip |
Contextual information on hover | @kit/ui/tooltip tooltip.tsx |
Makerkit-specific Components
| Component | Description | Import Path |
|---|---|---|
If |
Conditional rendering component | @kit/ui/if if.tsx |
Trans |
Internationalization text component | @kit/ui/trans trans.tsx |
Page |
Page layout with navigation | @kit/ui/page page.tsx |
GlobalLoader |
Full-page loading indicator | @kit/ui/global-loader global-loader.tsx |
ImageUploader |
Image upload component | @kit/ui/image-uploader image-uploader.tsx |
ProfileAvatar |
User avatar with fallback | @kit/ui/profile-avatar profile-avatar.tsx |
DataTable (Enhanced) |
Extended data table with pagination | @kit/ui/enhanced-data-table data-table.tsx |
Stepper |
Multi-step process indicator | @kit/ui/stepper stepper.tsx |
CookieBanner |
GDPR-compliant cookie notice | @kit/ui/cookie-banner cookie-banner.tsx |
CardButton |
Card-styled button | @kit/ui/card-button card-button.tsx |
MultiStepForm |
Form with multiple steps | @kit/ui/multi-step-form multi-step-form.tsx |
EmptyState |
Empty data placeholder | @kit/ui/empty-state empty-state.tsx |
AppBreadcrumbs |
Application path breadcrumbs | @kit/ui/app-breadcrumbs app-breadcrumbs.tsx |
Marketing Components
Import all marketing components with:
import {
Hero,
HeroTitle,
GradientText,
// etc.
} from '@kit/ui/marketing';
Key marketing components:
Hero- Hero sections hero.tsxSecondaryHerosecondary-hero.tsxFeatureCard,FeatureGrid- Feature showcases feature-card.tsxFooter- Page Footer footer.tsxHeader- Page Header header.tsxNewsletterSignup- Email collection newsletter-signup-container.tsxComingSoon- Coming soon page template coming-soon.tsx
Forms
- Use React Hook Form for form validation and submission.
- Use Zod for form validation.
- Use the
zodResolverfunction to resolve the Zod schema to the form. - Use Server Actions server-actions.mdc for server-side code handling
- Use Sonner for writing toasters for UI feedback
Follow the example below to create all forms:
Define the schema
Zod schemas should be defined in the schema folder and exported, so we can reuse them across a Server Action and the client-side form:
// _lib/schema/create-note.schema.ts
import { z } from 'zod';
export const CreateNoteSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
Create the Server Action
Server Actions server-actions.mdc can help us create endpoints for our forms.
'use server';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
import { CreateNoteSchema } from '../schema/create-note.schema';
export const createNoteAction = enhanceAction(
async function (data, user) {
// 1. "data" has been validated against the Zod schema, and it's safe to use
// 2. "user" is the authenticated user
// ... your code here
return {
success: true,
};
},
{
auth: true,
schema: CreateNoteSchema,
},
);
Create the Form Component
Then create a client component to handle the form submission:
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Textarea } from '@kit/ui/textarea';
import { Input } from '@kit/ui/input';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
import { toast } from '@kit/ui/sonner';
import { useTranslation } from 'react-i18next';
import { CreateNoteSchema } from '../_lib/schema/create-note.schema';
export function CreateNoteForm() {
const [pending, startTransition] = useTransition();
const { t } = useTranslation();
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
defaultValues: {
title: '',
content: '',
},
});
const onSubmit = (data) => {
startTransition(async () => {
await toast.promise(createNoteAction(data), {
loading: t('notes:creatingNote`),
success: t('notes:createNoteSuccess`),
error: t('notes:createNoteError`)
})
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Form {...form}>
<FormField name={'title'} render={({ field }) => (
<FormItem>
<FormLabel>
<span className={'text-sm font-medium'}>Title</span>
</FormLabel>
<FormControl>
<Input
type={'text'}
className={'w-full'}
placeholder={'Title'}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
<FormField name={'content'} render={({ field }) => (
<FormItem>
<FormLabel>
<span className={'text-sm font-medium'}>Content</span>
</FormLabel>
<FormControl>
<Textarea
className={'w-full'}
placeholder={'Content'}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
<button disabled={pending} type={'submit'} className={'w-full'}>
Submit
</button>
</Form>
</form>
);
}
Always use @kit/ui for writing the UI of the form.
Data Fetching
General Data Flow
- In a Server Component context, please use the Supabase Client directly for data fetching
- In a Client Component context, please use the
useQueryhook from the "@tanstack/react-query" package
Data Flow works in the following way:
- Server Component uses the Supabase Client to fetch data.
- Data is rendered in Server Components or passed down to Client Components when absolutely necessary to use a client component (e.g. when using React Hooks or any interaction with the DOM).
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
// use data
}
or pass down the data to a Client Component:
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export default function ServerComponent() {
const supabase = getSupabaseServerClient();
const { data, error } = await supabase.from('notes').select('*');
if (error) {
return <SomeErrorComponent error={error} />;
}
return <SomeClientComponent data={data} />;
}
Supabase Clients
- In a Server Component context, use the
getSupabaseServerClientfunction from the "@kit/supabase/server-client" package server-client.ts - In a Client Component context, use the
useSupabasehook from the "@kit/supabase/hooks/use-supabase" package.
Admin Actions
Only in rare cases suggest using the Admin client getSupabaseServerAdminClient when needing to bypass RLS from the package @kit/supabase/server-admin-client server-admin-client.ts
React Query
When using useQuery, make sure to define the data fetching hook. Create two components: one that fetches the data and one that displays the data. For example a good usage is roles-data-provider.tsx as shown in update-member-role-dialog.tsx
Error Handling
- Logging using the
@kit/shared/loggerpackage logger.ts - Don't swallow errors, always handle them appropriately
- Handle promises and async/await gracefully
- Consider the unhappy path and handle errors appropriately
- Context without sensitive data
'use server';
import { getLogger } from '@kit/shared/logger';
export async function myServerAction() {
const logger = await getLogger();
logger.info('Request started...');
try {
// your code here
await someAsyncFunction();
logger.info('Request succeeded...');
} catch (error) {
logger.error('Request failed...');
// handle error
}
return {
success: true,
};
}
Database Rules
Database Architecture
- Supabase uses Postgres
- We strive to create a safe, robust, performant schema
- Accounts are the general concept of a user account, defined by the having the same ID as Supabase Auth's users (personal). They can be a team account or a personal account.
- Generally speaking, other tables will be used to store data related to the account. For example, a table
noteswould have a foreign keyaccount_idto link it to an account.
Schemas
- The DB schemas are available at
apps/web/supabase/schemas - To edit the DB schema, we can either change the schema files, or created new ones
- To create a new schema, create a file at
apps/web/supabase/schemas/<number>-<name>.sql
Migrations
- After creating a schema, we can create a migration
- Use the command
pnpm --filter web supabase:db:difffor creating migrations from schemas - After generating a migration, reset the database for applying the changes using the command
pnpm --filter web supabase:db:reset
Security & RLS
- Using RLS, we must ensure that only the account owner can access the data. Always write safe RLS policies and ensure that the policies are enforced.
- Unless specified, always enable RLS when creating a table. Propose the required RLS policies ensuring the safety of the data.
- Always consider any required constraints and triggers are in place for data consistency
- Always consider the compromises you need to make and explain them so I can make an educated decision. Follow up with the considerations make and explain them.
- Always consider the security of the data and explain the security implications of the data.
- Always use Postgres schemas explicitly (e.g.,
public.accounts) - Consider the required compromises between simplicity, functionality and developer experience. However, never compromise on security, which is paramount and fundamental.
- Use existing helper functions for access control instead of making your own queries, unless unavailable
Schema Overview
Makerkit uses a Supabase Postgres database with a well-defined schema focused on multi-tenancy through the concepts of accounts (both personal and team) and robust permission systems.
Database Schema
- Enums 01-enums.sql
- Config 02-config.sql
- Accounts 03-accounts.sql
- Roles 04-roles.sql
- Memberships 05-memberships.sql
- Roles Permissions 06-roles-permissions.sql
- Invitations 07-invitations.sql
- Billing Customers 08-billing-customers.sql
- Subscriptions 09-subscriptions.sql
- Orders 10-orders.sql
- Notifications 11-notifications.sql
- One Time Tokens 12-one-time-tokens.sql
- Multi Factor Auth 13-mfa.sql
- Super Admin 14-super-admin.sql
- Account Views 15-account-views.sql
- Storage 16-storage.sql
Database Best Practices
Inferring Database types
Fetch auto-generated data types using the @kit/supabase/database import. Do not write types manually if the shape is the same as the one from the database row.
import { Tables } from '@kit/supabase/database';
// public.accounts
type Account = Tables<'accounts'>;
// public.subscriptions
type Subscription = Tables<'subscriptions'>;
// public.notifications
type Notification = Tables<'notifications'>;
// ...
Security
- Always enable RLS on new tables unless explicitly instructed otherwise
- Create proper RLS policies for all CRUD operations following existing patterns
- Always associate data with accounts using a foreign key to ensure proper access control
- Use explicit schema references (
public.table_namenot justtable_name) - Private schema: Place internal functions in the
kitschema - Search Path: Always set search path to '' when defining functions
- Security Definer: Do not use
security definerfunctions unless stricly required
Data Access Patterns
- Use
public.has_role_on_account(account_id, role?)to check membership - Use
public.has_permission(user_id, account_id, permission)for permission checks - Use
public.is_account_owner(account_id)to identify account ownership
SQL Coding Style
- Use explicit transactions for multi-step operations
- Follow existing naming conventions:
- Tables: snake_case, plural nouns (
accounts,subscriptions) - Functions: snake_case, verb phrases (
create_team_account,verify_nonce) - Triggers: descriptive action names (
set_slug_from_account_name)
- Tables: snake_case, plural nouns (
- Document functions and complex SQL with comments
- Use parameterized queries to prevent SQL injection
Common Patterns
- Account Lookup: Typically by
id(UUID) orslug(for team accounts) - Permission Check: Always verify proper permissions before mutations
- Timestamp Automation: Use the
trigger_set_timestamps()function - User Tracking: Use the
trigger_set_user_tracking()function - Configuration: Use
is_set(field_name)to check enabled features
Best Practices for Database Code
1. RLS Policy Management
Always Enable RLS
Always enable RLS for your tables unless you have a specific reason not to.
ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;
Use Helper Functions to validate permissions and access control
Use the existing structure for policies:
-- SELECT policy
CREATE POLICY "my_table_read" ON public.my_table FOR SELECT
TO authenticated USING (
account_id = (select auth.uid()) OR
public.has_role_on_account(account_id)
);
-- INSERT/UPDATE/DELETE policies follow similar patterns
When using RLS at team-account level, use public.has_role_on_account(account_id) for a generic check to understand if a user is part of a team.
When using RLS at user-account level, use account_id = (select auth.uid()).
When an entity can belong to both, use both.
When requiring a specific role, use the role parameter public.has_role_on_account(account_id, 'owner')
2. Account Association
- Associate Data with Accounts: Always link data to accounts using a foreign key:
CREATE TABLE if not exists public.my_data ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID REFERENCES public.accounts(id) ON DELETE CASCADE NOT NULL, /* other fields */ );
3. Permission System
- Use the Permission System: Leverage the built-in permission system for access control:
-- Check if a user has a specific permission SELECT public.has_permission( auth.uid(), account_id, 'my.permission'::public.app_permissions );
4. Schema Organization
-
Use Schemas Explicitly: Always use schema prefixes explicitly:
-- Good SELECT * FROM public.accounts; -- Avoid SELECT * FROM accounts; -
Put Internal Functions in 'kit' Schema: Use the 'kit' schema for internal helper functions
CREATE OR REPLACE FUNCTION kit.my_helper_function() RETURNS void AS $$ -- function body $$ LANGUAGE plpgsql;
5. Types and Constraints
-
Use Enums for Constrained Values: Create and use enum types for values with a fixed set:
CREATE TYPE public.my_status AS ENUM('active', 'inactive', 'pending'); CREATE TABLE if not exists public.my_table ( status public.my_status NOT NULL DEFAULT 'pending' ); -
Apply Appropriate Constraints: Use constraints to ensure data integrity:
CREATE TABLE if not exists public.my_table ( email VARCHAR(255) NOT NULL CHECK (email ~* '^.+@.+\..+$'), count INTEGER NOT NULL CHECK (count >= 0), /* other fields */ );
6. Authentication and User Management
- Use Supabase Auth: Leverage auth.users for identity management
- Handle User Creation: Use triggers like
kit.setup_new_userto set up user data after registration
7. Function Security
-
Apply Security Definer Carefully: For functions that need elevated privileges:
CREATE OR REPLACE FUNCTION public.my_function() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ -- function body $$; -
Set Proper Function Permissions:
GRANT EXECUTE ON FUNCTION public.my_function() TO authenticated, service_role;
8. Error Handling and Validation
- Use Custom Error Messages: Return meaningful errors:
IF NOT validation_passed THEN RAISE EXCEPTION 'Validation failed: %', error_message; END IF;
9. Triggers for Automation
- Use Triggers for Derived Data: Automate updates to derived fields:
CREATE TRIGGER update_timestamp BEFORE UPDATE ON public.my_table FOR EACH ROW EXECUTE FUNCTION public.trigger_set_timestamps();
10. View Structure for Commonly Used Queries
- Create Views for Complex Joins: As done with
user_account_workspaceCREATE OR REPLACE VIEW public.my_view WITH (security_invoker = true) AS SELECT ...
You always must use (security_invoker = true) for views.
Key Functions to Know
-
Account Access
public.has_role_on_account(account_id, account_role)public.is_account_owner(account_id)public.is_team_member(account_id, user_id)
-
Permissions
public.has_permission(user_id, account_id, permission_name)public.has_more_elevated_role(target_user_id, target_account_id, role_name)
-
Team Management
public.create_team_account(account_name)
-
Billing & Subscriptions
public.has_active_subscription(target_account_id)
-
One-Time Tokens
public.create_nonce(...)public.verify_nonce(...)public.revoke_nonce(...)
-
Super Admins
public.is_super_admin()
-
MFA:
public.is_aal2()public.is_mfa_compliant()
Configuration Control
- Use the
configTable: The application has a central configuration table - Check Features with
public.is_set(field_name):-- Check if team accounts are enabled SELECT public.is_set('enable_team_accounts');
Server Actions
- For Data Mutations from Client Components, always use Server Actions
- Always name the server actions file as "server-actions.ts"
- Always name exported Server Actions suffixed as "Action", ex. "createPostAction"
- Always use the
enhanceActionfunction from the "@kit/supabase/actions" package index.ts - Always use the 'use server' directive at the top of the file
- Place the Zod schema in a separate file so it can be reused with
react-hook-form
'use server';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
import { EntitySchema } from '../entity.schema.ts`;
export const myServerAction = enhanceAction(
async function (data, user) {
// 1. "data" is already a valid EntitySchema and it's safe to use
// 2. "user" is the authenticated user
// ... your code here
return {
success: true,
};
},
{
auth: true,
schema: EntitySchema,
},
);
Route Handler / API Routes
- Use Route Handlers when data fetching from Client Components
- To create API routes (route.ts), always use the
enhanceRouteHandlerfunction from the "@kit/supabase/routes" package. index.ts
import { z } from 'zod';
import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';
const ZodSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export const POST = enhanceRouteHandler(
async function({ body, user, request }) {
// 1. "body" is already a valid ZodSchema and it's safe to use
// 2. "user" is the authenticated user
// 3. "request" is NextRequest
// ... your code here
return NextResponse.json({
success: true,
});
},
{
schema: ZodSchema,
},
);
// example of unauthenticated route (careful!)
export const GET = enhanceRouteHandler(
async function({ user, request }) {
// 1. "user" is null, as "auth" is false and we don't require authentication
// 2. "request" is NextRequest
// ... your code here
return NextResponse.json({
success: true,
});
},
{
auth: false,
},
);
Access Control & Permissions Guidelines
This rule provides guidance for implementing access control, permissions, and subscription-related functionality in the application.
Role-Based Access Control
Account Roles
- Roles are defined in the
rolestable with hierarchy levels (lower number = higher privilege) - Default roles include
owner(hierarchy_level=1) andmemberwith specific permissions - Primary account owner has special privileges that cannot be revoked
- Role hierarchy controls what actions users can perform on other members
Role Permissions
- Permissions are stored in
role_permissionstable mapping roles to specific permissions - Core permissions:
roles.manage: Manage roles of users with lower hierarchybilling.manage: Access/update billing informationsettings.manage: Update account settingsmembers.manage: Add/remove membersinvites.manage: Create/update/delete invitations
Permission Checking
- Use
has_permission(user_id, account_id, permission_name)to check specific permissions - Use
can_action_account_member(target_team_account_id, target_user_id)to verify if a user can act on another - Use
is_account_owner(account_id)to check if user is primary owner - Primary owners can perform any action regardless of explicit permissions
Team Account Access
Team Membership
- Use
has_role_on_account(account_id, account_role)to check if user is a member with specific role - Use
is_team_member(account_id, user_id)to check if a specific user is a member - Use the authenticated user's
TeamAccountWorkspaceContextto access current permissions array
Invitations
- Only users with
invites.managepermission can create/manage invitations - Users can only invite others with the same or lower role hierarchy than they have
- Invitations have expiry dates (default: 7 days)
- Accept invitations using
accept_invitationfunction with token
Subscription Access
Subscription Status Checking
- Check active subscriptions with
has_active_subscription(account_id) - Active status includes both
activeandtrialingsubscriptions - Guard premium features with subscription checks in both frontend and backend
Billing Access
- Only users with
billing.managepermission can access billing functions - All billing operations should be guarded with permission checks
- Per-seat billing automatically updates when members are added/removed
Row Level Security
Table RLS
- Most tables have RLS policies restricting access based on team membership
- Personal account data is only accessible by the account owner
- Team account data is accessible by all team members based on their roles
Actions on Members
- Higher roles can update/remove lower roles but not equal or higher roles
- Primary owner cannot be removed from their account
- Ownership transfer requires OTP verification and is limited to primary owners
i18n System Guide
This document provides a comprehensive overview of the internationalization (i18n) system in our Next.js application.
Architecture Overview
The i18n system consists of:
- Core i18n Package: Located in
packages/i18n, providing the foundation for i18n functionality - Application-specific Implementation: Located in
apps/web/lib/i18n, customizing the core functionality - Translation Files: Located in
apps/web/public/locales/[language]/[namespace].json
Usage Guide
1. Setting Up a Page or Layout with i18n
Wrap your page or layout component with the withI18n HOC:
import { withI18n } from '~/lib/i18n/with-i18n';
function HomePage() {
// Your component code
}
export default withI18n(HomePage);
2. Using Translations in Client Components
Use the useTranslation hook from react-i18next:
'use client';
import { useTranslation } from 'react-i18next';
export function MyComponent() {
const { t } = useTranslation('common');
return <h1>{t('homeTabLabel')}</h1>;
}
3. Using Translations with the Trans Component
For complex translations that include HTML or variables:
import { Trans } from '@kit/ui/trans';
export function MyComponent() {
return (
<div>
<Trans
i18nKey="teams:inviteAlertBody"
values={{ accountName: 'My Team' }}
/>
</div>
);
}
4. Adding Language Selection to Your UI
Use the LanguageSelector component:
import { LanguageSelector } from '@kit/ui/language-selector';
export function SettingsPage() {
return (
<div>
<h2>Language Settings</h2>
<LanguageSelector />
</div>
);
}
5. Adding New Translations
- Create or update JSON files in
apps/web/public/locales/[language]/[namespace].json - Follow the existing structure, adding your new keys
For example, in apps/web/public/locales/en/common.json:
{
"existingKey": "Existing translation",
"newKey": "New translation text"
}
6. Adding a New Language
- Add the language code to the
languagesarray ini18n.settings.ts - Create corresponding translation files in
apps/web/public/locales/[new-language]/ - Copy the structure from the English files as a template
7. Adding a New Namespace
- Add the namespace to
defaultI18nNamespacesini18n.settings.ts - Create corresponding translation files for all supported languages
Advanced Usage
Dynamic Namespace Loading
When you need translations from namespaces not included in the default set:
import { getI18nSettings } from '~/lib/i18n/i18n.settings';
// Load specific namespaces
const settings = getI18nSettings(language, ['specific-namespace']);
Language Priority
The system uses the following priority to determine the language:
- User-selected language (from cookie)
- Browser language (if priority is set to 'user')
- Default language from environment variable
Common Issues
- Translation not showing: Check that you're using the correct namespace
- Dynamic content not interpolated: Make sure to use the
valuesprop withTranscomponent
Available Namespaces and Keys
Here's a brief overview of the available namespaces:
- common: General UI elements, navigation, errors common.json
- auth: Authentication-related text auth.json
- account: Account settings and profile account.json
- teams: Team management teams.json
- billing: Subscription and payment billing.json
- marketing: Landing pages, blog, etc. marketing.json
When creating a new functionality, it can be useful to add a new namespace.
Security Best Practices
Next.js-Specific Security
Client Component Data Passing
- Never pass sensitive data to Client Components
- Never pass unsanitized data to Client Components (raw cookies, client-provided data)
Server Components Security
- Always sanitize user input before using in Server Components
- Validate cookies and headers in Server Components
Environment Variables
- Use
import 'server-only'for code that should only be run on the server side - Never expose server-only env vars to the client
- Never pass environment variables as props to client components unless they're suffixed with
NEXT_PUBLIC_ - Never use
NEXT_PUBLIC_prefix for sensitive data (ex. API keys, secrets) - Use
NEXT_PUBLIC_prefix only for client-safe variables
Client Hydration Protection
- Never expose sensitive data in initial HTML
Authentication & Authorization
Row Level Security (RLS)
- Always enable RLS on all tables unless explicitly specified otherwise database.mdc
Super Admin Protected Routes
Always perform extra checks when writing Super Admin code super-admin.mdc
Server Actions & API Routes
Server Actions
- Always use
enhanceActionwrapper for consistent security server-actions.mdc - Always use 'use server' directive at the top of the file to safely bundle server-side code
- Validate input with Zod schemas
- Implement authentication checks:
'use server';
import { enhanceAction } from '@kit/next/actions';
import { MyActionSchema } from '../schema';
export const secureAction = enhanceAction(
async function(data, user) {
// Additional permission checks
const hasPermission = await checkUserPermission(user.id, data.accountId, 'action.perform');
if (!hasPermission) throw new Error('Insufficient permissions');
// Validated data available
return processAction(data);
},
{
auth: true,
schema: MyActionSchema
}
);
API Routes
- Use
enhanceRouteHandlerfor consistent security route-handlers.mdc - Implement authentication and authorization checks:
import { enhanceRouteHandler } from '@kit/next/routes';
import { RouteSchema } from '../schema';
export const POST = enhanceRouteHandler(
async function({ body, user, request }) {
// Additional authorization checks
const canAccess = await canAccessResource(user.id, body.resourceId);
if (!canAccess) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
// Safe to process with validated body
return NextResponse.json({ success: true });
},
{
auth: true,
schema: RouteSchema
}
);
Client Components Security
Context Awareness
- Use appropriate workspace contexts for access control:
useUserWorkspace()in personal account pagesuseTeamAccountWorkspace()in team account pages
- Check permissions before rendering sensitive UI elements:
function SecureComponent() {
const { account, user } = useTeamAccountWorkspace();
const canEdit = account.permissions.includes('entity.update');
if (!canEdit) return null;
return <EditComponent />;
}
Data Fetching
- Use React Query with proper error handling
- Never trust client-side permission checks alone
One-Time Tokens
Consider using OTP tokens when implementing highly destructive operations like deleting an entity that would otherwise require a full backup: otp.mdc
Critical Data Protection
Personal Information
- Never log or expose sensitive user data
- Use proper session management
Error Handling
- Never expose internal errors to clients
- Log errors securely with appropriate context
- Return generic error messages to users
try {
await sensitiveOperation();
} catch (error) {
logger.error({ error, context }, 'Operation failed');
return { error: 'Unable to complete operation' };
}
Database Security
- Avoid dynamic SQL generation
- Use SECURITY DEFINER functions sparingly and carefully, warn user if you do so
- Implement proper foreign key constraints
- Use appropriate data types with constraints