# Next.js RSC Session Best Practices This document defines the authoritative pattern for handling session/authentication in Next.js Server Components and Server Actions within the `apps/website` layer. ## Core Principle **Server Actions should NOT fetch session separately.** The API handles authentication automatically via cookies. ## The Problem with Manual Session Fetching ### ❌ Anti-Pattern (Current Implementation) ```typescript // apps/website/app/onboarding/actions.ts 'use server'; import { SessionGateway } from '@/lib/gateways/SessionGateway'; async function getCurrentUserId(): Promise { const gateway = new SessionGateway(); const session = await gateway.getSession(); // ❌ Extra API call return session?.user?.userId || null; } export async function completeOnboardingAction(input: CompleteOnboardingInputDTO) { const userId = await getCurrentUserId(); // ❌ Performance overhead if (!userId) { return Result.err('Not authenticated'); } const mutation = new CompleteOnboardingMutation(); // ... rest of logic } ``` **Problems:** 1. **Performance**: Makes extra API call on every action invocation 2. **Redundancy**: Manual auth check when API handles it automatically 3. **Coupling**: Actions depend on session infrastructure 4. **Inconsistency**: Doesn't match pattern used elsewhere in codebase ## The Correct Pattern ### ✅ Server Actions (Thin Wrappers) ```typescript // apps/website/app/onboarding/actions.ts 'use server'; import { Result } from '@/lib/contracts/Result'; import { CompleteOnboardingMutation } from '@/lib/mutations/onboarding/CompleteOnboardingMutation'; import { GenerateAvatarsMutation } from '@/lib/mutations/onboarding/GenerateAvatarsMutation'; import { CompleteOnboardingInputDTO } from '@/lib/types/generated/CompleteOnboardingInputDTO'; import { revalidatePath } from 'next/cache'; /** * Complete onboarding - thin wrapper around mutation * * Pattern: Server Action → Mutation → Service → API Client * * Authentication is handled automatically by the API via cookies. * The BaseApiClient includes credentials: 'include', so cookies are sent automatically. * If authentication fails, the API returns 401/403 which gets converted to domain errors. */ export async function completeOnboardingAction( input: CompleteOnboardingInputDTO ): Promise> { const mutation = new CompleteOnboardingMutation(); const result = await mutation.execute(input); if (result.isErr()) { return Result.err(result.getError()); } revalidatePath('/dashboard'); return Result.ok({ success: true }); } /** * Generate avatars - thin wrapper around mutation * * Note: This action requires userId to be passed from the client. * The client should get userId from session and pass it as a parameter. */ export async function generateAvatarsAction(params: { userId: string; facePhotoData: string; suitColor: string; }): Promise> { const mutation = new GenerateAvatarsMutation(); const result = await mutation.execute(params); if (result.isErr()) { return Result.err(result.getError()); } const data = result.unwrap(); return Result.ok({ success: data.success, avatarUrls: data.avatarUrls }); } ``` ### ✅ Client Component Pattern ```typescript // apps/website/app/onboarding/OnboardingWizardClient.tsx 'use client'; import { completeOnboardingAction, generateAvatarsAction } from '@/app/onboarding/actions'; import { useAuth } from '@/lib/hooks/auth/useAuth'; import { useState } from 'react'; export function OnboardingWizardClient() { const { session } = useAuth(); // Get userId from session (client-side) const [isSubmitting, setIsSubmitting] = useState(false); const handleCompleteOnboarding = async (input: CompleteOnboardingInputDTO) => { setIsSubmitting(true); try { const result = await completeOnboardingAction(input); if (result.isErr()) { // Handle error (show toast, etc.) console.error('Onboarding failed:', result.getError()); return; } // Success - redirect or show success message console.log('Onboarding completed successfully'); } finally { setIsSubmitting(false); } }; const handleGenerateAvatars = async (facePhotoData: string, suitColor: string) => { if (!session?.user?.userId) { console.error('User not authenticated'); return Result.err('Not authenticated'); } setIsSubmitting(true); try { const result = await generateAvatarsAction({ userId: session.user.userId, // Pass userId from session facePhotoData, suitColor, }); if (result.isErr()) { console.error('Avatar generation failed:', result.getError()); return; } const data = result.unwrap(); console.log('Avatars generated:', data.avatarUrls); } finally { setIsSubmitting(false); } }; return (
{/* Wizard implementation */}
); } ``` ## Why This Pattern Works ### 1. Automatic Authentication ```typescript // apps/website/lib/api/base/BaseApiClient.ts protected async request(method: string, path: string, data?: object, options = {}): Promise { const config: RequestInit = { method, headers, credentials: 'include', // ✅ Automatically sends cookies signal, }; // ... } ``` The `BaseApiClient` automatically includes `credentials: 'include'`, so: - Cookies are sent with every API request - The backend API can authenticate the user - No manual session fetching needed in server actions ### 2. Error Handling Flow ``` User not authenticated ↓ API returns 401/403 ↓ BaseApiClient throws HttpUnauthorizedError ↓ Service catches and converts to DomainError ↓ Mutation returns Result.err() ↓ Server Action returns error to client ↓ Client shows appropriate message ``` ### 3. Performance Benefits - **No extra API calls**: Session fetched once on client, reused - **Reduced latency**: Server actions don't wait for session lookup - **Better scalability**: Fewer API calls per user action ### 4. Consistency with Architecture This pattern matches your existing codebase: ```typescript // apps/website/app/admin/actions.ts - ✅ Already follows this pattern export async function updateUserStatus(userId: string, status: string) { const mutation = new UpdateUserStatusMutation(); const result = await mutation.execute({ userId, status }); // ... no manual session fetching } ``` ## When Manual Session Fetching IS Needed There are rare cases where you might need to fetch session in a server action: ### Case 1: User ID Required in Request Body If the API endpoint requires userId in the request body (not just cookies): ```typescript // Client passes userId const result = await generateAvatarsAction({ userId: session.user.userId, // ✅ Pass from client facePhotoData, suitColor, }); ``` ### Case 2: Server-Side Authorization Check If you need to check permissions before calling a mutation: ```typescript export async function adminAction(input: AdminInput) { // Only fetch session if you need to check permissions server-side const gateway = new SessionGateway(); const session = await gateway.getSession(); if (!session?.user?.roles?.includes('admin')) { return Result.err('Forbidden'); } const mutation = new AdminMutation(); return await mutation.execute(input); } ``` **Note**: This is rare. Usually, the API handles authorization and returns 403 if unauthorized. ## Summary | Aspect | Manual Session Fetching | Automatic (Recommended) | |--------|------------------------|------------------------| | **Performance** | ❌ Extra API call | ✅ No overhead | | **Complexity** | ❌ More code | ✅ Simpler | | **Coupling** | ❌ Depends on session infra | ✅ Decoupled | | **Consistency** | ❌ Inconsistent | ✅ Matches codebase | | **Security** | ⚠️ Manual checks | ✅ API enforces | ## Golden Rule **Server Actions are thin wrappers. Mutations handle logic. API enforces security.** Never fetch session in server actions unless you have a specific server-side authorization requirement that cannot be handled by the API.