* Update AGENTS.md and CLAUDE.md for improved clarity and structure * Added MCP Server * Added missing triggers to tables that should have used them * Updated all dependencies * Fixed rare bug in React present in the Admin layout which prevents navigating to pages (sometimes...)
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: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