Enopax Infrastructure Deployment Platform Development Guidelines
This document consolidates best practices from across the project documentation to ensure consistent, maintainable, and secure code development for the infrastructure deployment platform.
- Code Standards
- Component Development
- Database & Query Optimisation
- Form Development
- API & Service Layer
- Authentication & Security
- Resource Management
- Testing
- Performance
- Deployment
- Development Workflow
- ALWAYS use British English spelling throughout the codebase
- NEVER add comments unless explicitly requested
- Follow existing code conventions and patterns
- Mimic the style of surrounding code
- Use established libraries and utilities
- NEVER expose or log secrets and keys
- ALWAYS match exact file name casing in import statements
- CRITICAL: File systems may be case-sensitive (macOS/Linux) - webpack will warn about case mismatches
- Example issues:
- ❌
import { Callout } from '@/components/common/callout'(lowercase 'c') - ✅
import { Callout } from '@/components/common/Callout'(matchesCallout.tsx) - ❌
import { Input } from '@/components/common/input'(lowercase 'i') - ✅
import { Input } from '@/components/common/Input'(matchesInput.tsx)
- ❌
- How to check: File name must match import path exactly, character-by-character
- Why it matters: Prevents webpack warnings, build issues, and runtime errors on case-sensitive systems
- Use strict TypeScript mode
- Leverage Prisma-generated types for full type safety
- Prefer union types for form states over complex interfaces
- Keep state complexity minimal
- ALWAYS prefer editing existing files over creating new ones
- NEVER proactively create documentation files unless explicitly requested
- NEVER put newly created components in
/components/common/folder (reserved for Tremor components and UI-related) - NEVER put client components in
/appunless they arepage.tsxfiles - NEVER push files outside of the
next-appfolder
When creating new files, ALWAYS follow these placement rules:
- Data Fetching Queries: ALWAYS place in
/lib/query/folder (read-only database operations)- ✅
/lib/query/files.ts— User file queries - ✅
/lib/query/tokens.ts— Token balance queries - ✅
/lib/query/collections.ts— Collection queries - ❌
/actions/files.ts— NEVER use actions for data fetching
- ✅
- Server Actions: ALWAYS place in
/actions/folder (only user-triggered mutations)- ✅
/actions/admin.ts— Create/update/delete operations - ❌
/actions/files.ts— Do NOT put queries here - ❌
/app/admin/actions.ts— Never colocate with pages
- ✅
- Components: ALWAYS place in
/components/folder (never colocated with pages in/app)- ✅
/components/AdminNav.tsx - ❌
/app/admin/AdminNav.tsx
- ✅
- Form Components: ALWAYS place in
/components/forms/subdirectory- ✅
/components/forms/NewCollectionForm.tsx - ❌
/components/NewCollectionForm.tsx
- ✅
- Table Column Definitions: Place in
/components/table/subdirectory - UI Primitives: Reserved for
/components/common/(Tremor/Radix components only) - These rules apply to ALL new files being created - always check file placement before creating
- ALWAYS use CamelCase for component filenames (e.g.,
LoginForm.tsx, notlogin-form.tsx) - Form components should be placed in
/components/forms/directory - Page-specific client components should be placed in
/components/directory (not colocated with pages in/app) - Follow consistent naming patterns across the codebase
Visual design should serve information architecture. Use these techniques to guide users without additional UI patterns:
1. Gradient Backgrounds for Section Emphasis
- Main background: subtle gradient (gray-50 → gray-100)
- Key sections: more pronounced gradients (indigo → blue)
- Creates visual hierarchy without additional borders or containers
- Helps users understand section importance and relationships
2. Coloured Headers to Group Related Operations
{/* Upload & Storage operations - blue header */}
<div className="bg-gradient-to-r from-blue-50 to-blue-100 dark:from-blue-950/20">
<h3>Upload & Storage</h3>
</div>
{/* IPFS Pinning operations - orange header */}
<div className="bg-gradient-to-r from-orange-50 to-orange-100 dark:from-orange-950/20">
<h3>IPFS Pinning</h3>
</div>- Semantic colour coding (blue = cold/storage, orange = hot/IPFS) matches system mental model
- Header styling creates visual "container" without card-in-card nesting
- Dark mode variants maintain hierarchy
3. Icons + Text + Numbers for Scannable Sections
- Icons draw attention and aid visual recognition
- Text provides context (description below operation name)
- Numbers (costs, counts) stand out with colour and size
- Combined = information architecture visible at a glance
4. Spacing as Information Structure
- Large spacing (py-20, space-y-20) between major sections
- Medium spacing (space-y-4) within sections
- Tight spacing within related items (space-y-1, space-y-2)
- Spacing hierarchy mirrors information hierarchy
5. Border & Rounded Corner Subtlety
- Use
border-gray-200 dark:border-gray-700for calm structure - Rounded corners (
rounded-2xlfor major sections,rounded-xlfor cards) - Avoid bold, dark borders - let spacing and colour do the work
- Subtle borders provide structure without distraction
When building resource template showcase or deployment configuration pages, follow the Progressive Disclosure pattern:
1. Multiple Template Representations
- Show templates in card grid for visual discovery
- Include detailed template specifications in comparison format
- Support different user journeys: quick deployment vs detailed configuration
2. Contextual Information Grouping
- Group templates by resource type (e.g., "Databases", "IPFS Clusters", "Storage")
- Organise template information: specs, features, deployment time
- Headers with visual distinction (colour, icon) help scanning
3. Progressive Disclosure of Complexity
- Hero section: One clear call-to-action ("Deploy Infrastructure")
- Primary content: Main template choices (cards with icons)
- Secondary content: Detailed specifications and features
- Tertiary content: Advanced configuration options
- Final CTA: Clear deployment action with estimated time
4. Feature-Driven Information
- Lead with key features (CPU, storage, replication)
- Include deployment specifications upfront
- Show infrastructure details transparently
- Use icons to help users quickly identify resource types
5. Visual Hierarchy Through Design
- Use gradient backgrounds to distinguish template categories
- Status badges indicate deployment readiness
- Icons + specs + CTA = scannable template cards
- Clear visual feedback for deployment progress
6. Template Selection + Configuration Flow
- Step 1: Browse and select template
- Step 2: Configure resource name and team
- Step 3: Review specifications before deployment
- Step 4: Deploy and monitor progress in real-time
- ❌ No template preview or specifications
- ❌ Unclear deployment timeline or resource specifications
- ❌ No progress feedback during deployment
- ❌ Credentials hidden after deployment complete
- ❌ No error recovery or retry options on failure
- Every component in
/components/folder (never colocate with pages in/app/) - Tremor components in
/components/common/folder (first priority) - Radix UI primitives when Tremor not available
- Custom components following established patterns
When building components that display data in different formats, implement a variant-based layout system:
Example: ResourceTemplates Component
// src/components/resource/ResourceTemplates.tsx
interface ResourceTemplatesProps {
templates: ResourceTemplate[];
variant?: 'grid' | 'cards' | 'table' | 'comparison';
onSelectTemplate?: (template: ResourceTemplate) => void;
}
export function ResourceTemplates({
templates,
variant = 'cards',
onSelectTemplate
}: ResourceTemplatesProps) {
// Single component, multiple layouts
// - grid: 3-4 column card layout (visual discovery)
// - cards: 2-column compact layout (mobile-friendly)
// - table: detailed comparison table (specification review)
// - comparison: side-by-side resource comparison
}Benefits:
- Single source of truth for template data
- Reuse across pages with different layout needs
- Easier maintenance (update once, affects all layouts)
- Type-safe variant switching
- Consistent styling and deployment flows
Usage Pattern:
// Same data, different contexts
<ResourceTemplates templates={data} variant="grid" /> {/* Discovery page */}
<ResourceTemplates templates={data} variant="comparison" /> {/* Detailed specs */}
<ResourceTemplates templates={data} variant="table" /> {/* Admin review */}- Use union types for component states
- Implement proper error boundaries
- Follow progressive enhancement principles
- Work with Radix UI's focus management
- Prefer composition over inheritance
- NEVER nest cards within cards - this creates visual clutter and breaks the design system
- RULE: If a section already has card styling (padding, border, background), do NOT add another card inside it
- Instead: Use
<Divider />for separation and plain<div>containers for content grouping
<section className="p-6 rounded-lg border border-green-200 bg-green-50">
<h2>Growing Trees</h2>
<div className="p-4 rounded-lg border border-brand-200 bg-brand-50">
{/* Nested card - creates double border/padding */}
<p>Upgrade content</p>
</div>
</section><section className="p-6 rounded-lg border border-green-200 bg-green-50">
<h2>Growing Trees</h2>
<Divider className="my-6" />
<div className="space-y-3">
{/* Plain div, no card styling */}
<p>Upgrade content</p>
<Button>Upgrade</Button>
</div>
</section>- Visual hierarchy: Double borders create confusion about information structure
- Design system consistency: Cards are meant for top-level sections, not nested content
- Accessibility: Screen readers struggle with deeply nested semantic containers
- Maintenance: Card-in-card makes it harder to refactor layout changes
When displaying multiple items (cards, tiers, packages) with buttons or links below variable-height content, ALWAYS align buttons on the same y-axis so they form a single horizontal line.
Use flexbox with flex-1 on expandable content to push buttons to the bottom:
<div className="grid grid-cols-4 gap-6">
{items.map(item => (
<div key={item.id} className="p-6 rounded-xl border">
<h3>{item.name}</h3>
<p>{item.price}</p>
<ul className="mb-6"> {/* Fixed margin - causes misalignment */}
{item.features.map(f => <li>{f}</li>)}
</ul>
<button>Buy</button>
</div>
))}
</div>Problem: Different content heights cause buttons to sit at different y-coordinates
<div className="grid grid-cols-4 gap-6">
{items.map(item => (
<div key={item.id} className="p-6 rounded-xl border flex flex-col h-full">
{/* Header section with fixed spacing */}
<div className="mb-4">
<h3>{item.name}</h3>
<p>{item.price}</p>
</div>
{/* Flexible content that expands/shrinks */}
<ul className="space-y-2 flex-1">
{item.features.map(f => <li>{f}</li>)}
</ul>
{/* Button always at bottom with consistent margin */}
<button className="mt-6 w-full">Buy</button>
</div>
))}
</div>Solution: flex flex-col h-full on container + flex-1 on expandable content + mt-6 on button
- Container: Add
flex flex-col h-fullto card container - Header: Wrap fixed-height content (title, price) in
<div className="mb-4"> - Content: Apply
flex-1to expandable list/content (e.g.,<ul className="space-y-2 flex-1">) - Button: Use
mt-6(or appropriate margin) to create consistent spacing from content - Width: Use
w-fullon button for full card width
- Grid layouts with multiple cards of varying content height
- Pricing cards where different packages have different feature lists
- Product cards where descriptions vary in length
- Tier comparisons where tier information varies
- Any list of similar items with clickable actions below
- Consistency: All buttons align horizontally, creating visual cohesion
- Professionalism: Aligned elements feel polished and intentional
- Scanability: Users can quickly scan across all action buttons
- User experience: Users expect clickable elements to be in the same row
- Use
UserSearch/GenericSearchcomponents over static Select dropdowns - Implement real database queries with debouncing and pagination
- Avoid static data in search components
- Use Tremor DatePicker components over native date inputs
- Handle timezone considerations properly
- Validate date ranges server-side
- ALWAYS use
GenericTablecomponent for data tables - Define column configurations using TanStack Table's
ColumnDeftype - Create column definitions in
/components/table/directory (e.g.,AdminUsers.tsx,Files.tsx) - Pass data from server component to
GenericTableclient component - Example pattern:
// page.tsx (Server Component) import GenericTable from '@/components/GenericTable'; import { columns } from '@/components/table/EntityName'; export default async function Page() { const data = await fetchData(); return <GenericTable tableData={data} tableColumns={columns} />; } // /components/table/EntityName.tsx (Column definitions) 'use client'; import { ColumnDef } from '@tanstack/react-table'; export const columns: ColumnDef<EntityType>[] = [ { header: 'Name', accessorKey: 'name', cell: ({ row }) => ... } ];
- Server-first approach: Keep most content in server components (page.tsx)
- Minimal client components: Only create client components for interactive elements
- Follow established patterns: Look at existing pages like
/main/developer/page.tsxfor structure - Avoid unnecessary abstraction: Don't split into multiple components unless there's clear benefit
// page.tsx (Server Component)
export default async function ProjectsPage() {
const data = await fetchData();
return (
<div>
{/* Static content directly in page */}
<header>...</header>
<section>...</section>
{/* Only interactive parts as client components */}
<InteractiveFilter data={data} />
<DisplayGrid items={filteredData} />
{/* More static content */}
<footer>...</footer>
</div>
);
}// Don't wrap everything in client components
return <PageWrapper><AllContent /></PageWrapper>- Server components: Headers, static sections, forms, quick actions
- Client components: Dropdowns, filters, interactive grids, modals
- URL-based state: Use searchParams instead of client state when possible
- Component size: Keep client components focused and small (< 150 lines typically)
- ALWAYS await
searchParamsandparamsin page components (Next.js 15+) - Type them as
Promise<{ key?: string }>in function signatures - Example:
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ page?: string }>;
}) {
const params = await searchParams;
const pageNumber = Number(params.page) || 1;
// ...
}- ❌ Don't import server-only modules in client components: Never import
auth(),prisma, or Node.js modules likefsin'use client'components - ❌ Don't make entire pages client components: This causes Node.js module resolution errors
- ❌ Don't over-abstract: Creating too many small components makes code harder to follow
- ❌ Don't duplicate state: Use either URL params OR client state, not both for the same data
// Error: Can't resolve 'fs' in client component
'use client';
import { auth } from '@/lib/auth'; // ❌ Server-only module
// Solution: Separate server and client concerns
// page.tsx (Server)
const session = await auth();
return <ClientComponent data={data} />;
// ClientComponent.tsx
'use client';
export default function ClientComponent({ data }) {
// ✅ No server imports
}- Keep queries simple and focused - avoid overly complex joins
- Don't always query organisation membership - only fetch when needed
- Limit results by default - use pagination for large datasets
- Select only needed fields - avoid
SELECT *patterns - Use appropriate indices - ensure queries are optimised
// Get user's teams (simple membership check)
const userTeams = await prisma.team.findMany({
where: {
OR: [
{ ownerId: userId },
{ members: { some: { userId } } }
]
},
select: {
id: true,
name: true,
isPersonal: true
}
});// Don't do this - too complex and slow
const userTeams = await prisma.team.findMany({
where: {
OR: [
{ ownerId: userId },
{ members: { some: { userId } } },
{
organisation: {
members: {
some: {
userId,
role: { in: ['OWNER', 'MANAGER'] }
}
}
}
}
]
},
include: {
organisation: {
include: {
members: {
include: {
user: true
}
}
}
},
members: {
include: {
user: true
}
}
}
});- Use
Promise.all()for parallel queries when data is independent - Implement database-level pagination
- Cache frequently accessed, slow-changing data
- Monitor query performance in development
- Use optional relationships (
Organisation?) when entities can exist independently - Implement proper cascading deletes with
onDeleteconstraints - Use
SetNullfor optional relationships to maintain data integrity
- ALWAYS place form components in
/components/forms/subdirectory - ALWAYS use
useActionStatehook for form state management - ALWAYS create corresponding server action in
/actions/folder - Follow progressive enhancement principles (forms work without JavaScript)
- Use server actions for mutations, API routes for external integrations
The useActionState hook (React 19+) provides automatic form state management with server actions:
// /components/forms/NewCollectionForm.tsx
'use client';
import { useActionState } from 'react';
import { createCollection, type CreateCollectionState } from '@/actions/collections';
export function NewCollectionForm({ initialData }: FormProps) {
const [state, formAction, isPending] = useActionState<CreateCollectionState | null, FormData>(
createCollection,
null
);
return (
<form action={formAction} className="space-y-6">
{state?.error && (
<Callout variant="error" title="Error">
{state.error}
</Callout>
)}
<Input
name="name"
placeholder="Collection name"
required
disabled={isPending}
/>
<Button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create Collection'}
</Button>
</form>
);
}// /actions/collections.ts
'use server';
import { auth } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
import { redirect } from 'next/navigation';
export interface CreateCollectionState {
success: boolean;
error?: string;
collectionId?: string;
}
export async function createCollection(
_prevState: CreateCollectionState | null,
formData: FormData
): Promise<CreateCollectionState> {
try {
const session = await auth();
if (!session?.user) {
return { success: false, error: 'Unauthorised. Please sign in.' };
}
const name = formData.get('name')?.toString().trim();
if (!name) {
return { success: false, error: 'Collection name is required' };
}
const collection = await prisma.collection.create({
data: { userId: session.user.id, name }
});
redirect(`/collections/${collection.id}`);
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create collection'
};
}
}- Automatic loading states:
isPendingtracks submission status - Progressive enhancement: Forms work without JavaScript
- Type safety: Full TypeScript support for state and form data
- Error handling: Consistent error state management
- Redirect support: Use
redirect()in server actions for navigation
Detect whether form is in create or update mode by entity presence:
// Form component
export function EntityForm({ entity }: { entity?: Entity }) {
const action = entity ? updateEntity : createEntity;
const [state, formAction, isPending] = useActionState(action, null);
return (
<form action={formAction}>
{entity && <input type="hidden" name="entityId" value={entity.id} />}
<Input name="name" defaultValue={entity?.name} />
<Button type="submit">
{isPending ? 'Saving...' : entity ? 'Update' : 'Create'}
</Button>
</form>
);
}- Use Prisma-first validation approach instead of Zod schemas
- Validate on both client and server sides
- Provide clear, specific error messages
- Handle field-level and form-level errors consistently
- Return validation errors in state object for display
- Use entity-based action selection with hidden ID fields
- Implement proper loading states during submission (
isPending) - Disable form inputs during submission to prevent double-submission
- Provide success/error feedback via Callout components
- Handle edge cases (network errors, timeouts)
- Show loading spinner in submit button during submission
- Use shared business logic between API routes and Server Actions
- Implement progressive enhancement with Server Actions for forms
- Use API routes for external integration
- Maintain unified API interfaces
- Follow RESTful conventions where appropriate
- Use consistent response formats
- Implement proper error handling
- Version APIs when necessary
When to Use Server Actions:
- Form submissions and user-triggered mutations (create, update, delete)
- Actions that respond to user input or button clicks
- Operations that need to return state for
useActionStatehook
When NOT to Use Server Actions:
- Data fetching/loading functions
- Queries that run on page load or component mount
- Functions called directly from server components
Data fetching should happen directly in server components, not wrapped as server actions. All query functions live in /lib/query/:
// ❌ WRONG: Data fetching as a server action
// /actions/files.ts
'use server'
export async function getUserFiles() {
const session = await auth();
const files = await prisma.file.findMany({...});
return files;
}
// ✅ RIGHT: Data fetching functions in /lib/query/
// /lib/query/files.ts
export async function getUserFiles() {
const session = await auth();
const files = await prisma.file.findMany({...});
return files;
}
// /lib/query/tokens.ts
export async function getUserTokenBalance(userId: string | undefined) {
if (!userId) return null;
return prisma.tokenBalance.findUnique({...});
}
// ✅ Call directly from server component wrapper
// /app/(main)/files/page.tsx (Server Component)
import { getUserFiles } from '@/lib/query/files';
import { getUserTokenBalance } from '@/lib/query/tokens';
import { auth } from '@/lib/auth';
export default async function FilesPage() {
const session = await auth();
// Parallel queries for efficiency
const [tokenBalance, filesResult] = await Promise.all([
getUserTokenBalance(session?.user?.id),
getUserFiles({ page: 1, pageSize: 50 }),
]);
return (
<>
<FilesTable data={filesResult.files} />
<TokenDisplay balance={tokenBalance?.balance} />
</>
);
}File organisation:
/lib/query/: All data-fetching queries (read-only database operations)files.ts— File queriestokens.ts— Token balance queriescollections.ts— Collection queries, etc.
/actions/: Only user-triggered mutations (create, update, delete, state changes)- Key difference: Functions in
/lib/query/are called directly in server components; functions in/actions/with'use server'are RPC endpoints for client-side calls
- Use NextAuth.js v5 for authentication
- Implement role-based access control (GUEST, CUSTOMER, ADMIN)
- Use session-based authentication with secure cookies
- Validate permissions on both client and server
- NEVER add redundant authentication checks in page components
- Authentication is handled by parent
layout.tsxfiles in protected route groups - Pages under
(main)route group are already protected by/app/(main)/layout.tsx - Pages under
(auth)route group are public and handled by/app/(auth)/layout.tsx
// app/(main)/storage/page.tsx
export default async function StoragePage() {
const session = await auth();
if (!session?.user?.email) {
redirect('/login'); // ❌ Unnecessary - layout already checks this
}
// ... rest of page
}// app/(main)/storage/page.tsx
export default async function StoragePage() {
const session = await auth();
// ✅ Session is guaranteed to exist, layout handles redirect
const user = await prisma.user.findUnique({
where: { email: session.user.email },
// ... query
});
// ... rest of page
}- Layout files: Add
auth()check with redirect inlayout.tsxfor route groups - Server actions: Always check authentication (not protected by layout)
- API routes: Always check authentication (not protected by layout)
- Page components: Skip redirect check if layout already protects the route
- NEVER expose secrets or API keys in client code
- NEVER log sensitive information
- Validate all user inputs server-side
- Implement proper CSRF protection
- Use secure headers and HTTPS in production
- Check user permissions at the route level
- Implement team and organisation-based access controls
- Use granular permissions (read, write, execute, lead)
- Audit important actions with membership logs
- Use ResourceWizard component for guided resource creation (see
/src/components/resource/ResourceWizard.tsx) - Implement proper template system with ResourceTemplate types
- Track deployment progress with real-time status updates
- Handle both mock deployments (development) and real deployments (production)
- PROVISIONING: Deployment in progress, poll status every 2 seconds
- ACTIVE: Resource ready for use, endpoint and credentials accessible
- INACTIVE: Resource stopped or failed, show recovery options
- MAINTENANCE: Scheduled maintenance window (optional state)
- Located in
/src/lib/deployment-service.ts - Provides
deployResource(),getDeploymentStatus(),simulateDeployment()functions - Handles deployment stage progression (init → allocate → configure → provision → verify → complete)
- Returns structured DeploymentProgress with stage, progress percentage, and user-friendly message
- Store configuration in database
configurationJSON field - Include template ID, deployment stage, progress, and stage-specific settings
- Example structure:
{ templateId, deploymentStage, deploymentProgress, deploymentMessage, deployedAt, ...templateConfig } - Retrieve configuration through
/api/resources/[resourceId]/deployment-statusendpoint
- Multi-environment Jest setup for different concerns
- Test validation logic separately from business logic
- Test service layer functions independently
- Mock external dependencies (IPFS, external APIs)
npm run test:validation # Schema and validation tests
npm run test:actions # Server action tests
npm run test:services # Service layer tests
npm run test:components # React component tests- Write tests for critical business logic
- Test error cases and edge conditions
- Use meaningful test descriptions
- Keep tests isolated and independent
- Use Next.js App Router for optimal performance
- Implement proper code splitting
- Optimise images and static assets
- Use streaming and suspense boundaries
- Optimise database queries (see Database section)
- Implement caching where appropriate
- Use efficient serialisation
- Monitor performance metrics
- Tree-shake unused code
- Optimise bundle size
- Use dynamic imports for heavy components
- Implement proper lazy loading
- Use environment-specific configurations
- Implement proper secrets management
- Use Docker for consistent deployments
- Monitor application health
- Use simplified npm scripts for Docker operations
- Prefer manual Next.js development over containerised
- ALWAYS stop running processes after testing (
npm run dev) - If
npxcommands fail, ask user to run them manually
- ALWAYS run migrations before deployment
- If migration problems occur, drop and migrate forcefully
- Backup data before major schema changes
- Monitor database performance
- ALWAYS ask or prompt for missing dependencies or commands
- NEVER work around commands that cannot be executed
- Follow the established workflow patterns
- Implement proper error handling
- NEVER do all steps at once after planning
- Split work into smaller chunks and proceed step by step
- Use TodoWrite tool for tracking complex tasks
- Follow the established git workflow
- Follow the standardised pull request workflow
- Create focused, single-purpose PRs
- Use proper branch naming:
feature/descriptionorfix/issue-description - Include meaningful commit messages
- ALWAYS use the structured logger from
@/lib/loggerinstead ofconsole.log - ALWAYS include a
servicetag for filtering logs (e.g.,{ service: 'payment' }) - Use appropriate log levels:
logger.info(),logger.warn(),logger.error(),logger.debug() - Include relevant context in the metadata object (first parameter)
- Keep log messages concise and descriptive (second parameter)
- NEVER log secrets, API keys, or sensitive user data
import { logger } from '@/lib/logger';
// ✅ Good: Structured logging with service tag
logger.info(
{
billId: bill.id,
amount: bill.amount,
userId: bill.userId,
service: 'payment'
},
'Processing payment'
);
// ❌ Bad: Console.log without structure
console.log('[Payment] Processing payment for bill:', bill.id);- Logs appear in
/admin/logspage - Can be filtered by service, level, and time
- Structured data is searchable
- Better for debugging and monitoring
- Production-ready logging format
When logging resource deployment completion, ALWAYS use a comprehensive summary format:
// ✅ Good: Comprehensive deployment summary
logger.info(
{
service: 'resource-deployment',
resourceId: resource.id,
templateId: resource.templateId,
totalDuration: deploymentDuration,
stages: completedStages,
endpoint: resource.endpoint,
},
`Deployment complete: ${resource.name} (${resource.type}) deployed in ${deploymentDuration}ms - Endpoint: ${resource.endpoint}`
);
// ❌ Bad: Minimal logging without context
logger.info(
{
service: 'deployment',
resourceId: resource.id,
},
'Deployment complete'
);Why the comprehensive summary format is better:
- Shows deployment progress and outcome at a glance
- Includes resource type and endpoint for monitoring
- Includes timing information for performance analysis
- Provides actionable data for debugging failed deployments
- Tracks deployment success metrics for analytics
- Use proper error handling throughout the application
- Implement comprehensive structured logging (without exposing secrets)
- Monitor application performance and errors
- Document known issues and their solutions
This document should be updated as the project evolves and new patterns emerge. Always refer to the latest version for current best practices.