The SDK works seamlessly with Next.js applications, supporting both client and server-side rendering with proper token management.

Universal Token Storage

For Next.js applications, you need token storage that works in both client and server environments. The built-in CookieTokenStorage only works client-side, so you’ll need a custom implementation:

import { StorefrontSDK, type TokenStorage } from '@commercengine/storefront-sdk';

class NextJSTokenStorage implements TokenStorage {
  async getAccessToken(): Promise<string | null> {
    if (typeof window !== 'undefined') {
      // Client-side: use document.cookie
      const value = `; ${document.cookie}`;
      const parts = value.split(`; storefront_access_token=`);
      return parts.length === 2 ? parts.pop()?.split(';').shift() || null : null;
    } else {
      // Server-side: use Next.js cookies
      try {
        const { cookies } = await import('next/headers');
        const cookieStore = await cookies();
        return cookieStore.get('storefront_access_token')?.value || null;
      } catch {
        return null;
      }
    }
  }

  async setAccessToken(token: string): Promise<void> {
    if (typeof window !== 'undefined') {
      // Client-side
      const expires = new Date();
      expires.setFullYear(expires.getFullYear() + 1);
      document.cookie = `storefront_access_token=${token}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
    } else {
      // Server-side
      try {
        const { cookies } = await import('next/headers');
        const cookieStore = await cookies();
        cookieStore.set('storefront_access_token', token, {
          expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
          httpOnly: false,
          secure: process.env.NODE_ENV === 'production',
          sameSite: 'lax',
          path: '/'
        });
      } catch {
        console.warn('Could not set access token on server side');
      }
    }
  }

  async getRefreshToken(): Promise<string | null> {
    if (typeof window !== 'undefined') {
      const value = `; ${document.cookie}`;
      const parts = value.split(`; storefront_refresh_token=`);
      return parts.length === 2 ? parts.pop()?.split(';').shift() || null : null;
    } else {
      try {
        const { cookies } = await import('next/headers');
        const cookieStore = await cookies();
        return cookieStore.get('storefront_refresh_token')?.value || null;
      } catch {
        return null;
      }
    }
  }

  async setRefreshToken(token: string): Promise<void> {
    if (typeof window !== 'undefined') {
      const expires = new Date();
      expires.setFullYear(expires.getFullYear() + 1);
      document.cookie = `storefront_refresh_token=${token}; expires=${expires.toUTCString()}; path=/; SameSite=Lax`;
    } else {
      try {
        const { cookies } = await import('next/headers');
        const cookieStore = await cookies();
        cookieStore.set('storefront_refresh_token', token, {
          expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
          httpOnly: false,
          secure: process.env.NODE_ENV === 'production',
          sameSite: 'lax',
          path: '/'
        });
      } catch {
        console.warn('Could not set refresh token on server side');
      }
    }
  }

  async clearTokens(): Promise<void> {
    if (typeof window !== 'undefined') {
      document.cookie = 'storefront_access_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
      document.cookie = 'storefront_refresh_token=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
    } else {
      try {
        const { cookies } = await import('next/headers');
        const cookieStore = await cookies();
        cookieStore.delete('storefront_access_token');
        cookieStore.delete('storefront_refresh_token');
      } catch {
        console.warn('Could not remove tokens on server side');
      }
    }
  }
}

SDK Configuration

Create a single SDK instance that works everywhere:

// lib/storefront.ts
import { StorefrontSDK, Environment } from '@commercengine/storefront-sdk';

const storefront = new StorefrontSDK({
  storeId: process.env.NEXT_PUBLIC_STORE_ID!,
  environment: Environment.Production,
  apiKey: process.env.CE_API_KEY || process.env.NEXT_PUBLIC_CE_API_KEY,
  tokenStorage: new NextJSTokenStorage()
});

export default storefront;

Environment Variables

Set up your environment variables:

# .env.local
NEXT_PUBLIC_STORE_ID=your_store_id
CE_API_KEY=your_api_key
NEXT_PUBLIC_CE_API_KEY=your_api_key  # For client-side access

Only expose the API key client-side if your API endpoints support it. Keep sensitive keys server-side only when possible.

Server-Side Rendering

Use the SDK in server components and API routes:

// app/products/page.tsx
import storefront from '@/lib/storefront';

export default async function ProductsPage() {
  const result = await storefront.catalog.listProducts({ limit: 20 });
  
  if (!result.success) {
    return <div>Error loading products: {result.error.message}</div>;
  }

  return (
    <div>
      <h1>Products</h1>
      {result.data.map(product => (
        <div key={product.id}>
          <h2>{product.name}</h2>
          <p>{product.selling_price}</p>
        </div>
      ))}
    </div>
  );
}

Client-Side Usage

Use the SDK in client components:

'use client';

import { useState, useEffect } from 'react';
import storefront from '@/lib/storefront';

export default function CartComponent() {
  const [cart, setCart] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadCart() {
      const result = await storefront.cart.retrieveCartUsingUserId('user-123');
      if (result.success) {
        setCart(result.data);
      }
      setLoading(false);
    }
    
    loadCart();
  }, []);

  const addToCart = async (productId: string) => {
    const result = await storefront.cart.addDeleteCartItem({
      product_id: productId,
      variant_id: null,
      quantity: 1
    });
    
    if (result.success) {
      setCart(result.data);
    }
  };

  if (loading) return <div>Loading...</div>;

  return (
    <div>
      <h2>Shopping Cart</h2>
      {cart ? (
        <div>Items: {cart.items?.length || 0}</div>
      ) : (
        <div>Cart is empty</div>
      )}
    </div>
  );
}

API Routes

Use the SDK in API routes for backend operations:

// app/api/cart/route.ts
import { NextRequest, NextResponse } from 'next/server';
import storefront from '@/lib/storefront';

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { productId, quantity } = body;

    const result = await storefront.cart.addDeleteCartItem({
      product_id: productId,
      variant_id: null,
      quantity
    });

    if (result.success) {
      return NextResponse.json(result.data);
    } else {
      return NextResponse.json(
        { error: result.error.message },
        { status: 400 }
      );
    }
  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Authentication Flow

Handle authentication in both server and client contexts:

// app/login/page.tsx
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import storefront from '@/lib/storefront';

export default function LoginPage() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);
  const router = useRouter();

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();
    setLoading(true);

    const result = await storefront.auth.loginWithEmail({
      email,
      register_if_not_exists: true
    });

    if (result.success) {
      // Tokens are automatically stored by the SDK
      router.push('/account');
    } else {
      alert('Login failed: ' + result.error.message);
    }
    
    setLoading(false);
  };

  return (
    <form onSubmit={handleLogin}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Enter your email"
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Logging in...' : 'Login'}
      </button>
    </form>
  );
}

Middleware for Protected Routes

Create middleware to protect routes:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const accessToken = request.cookies.get('storefront_access_token');
  
  // Check if accessing a protected route
  if (request.nextUrl.pathname.startsWith('/account')) {
    if (!accessToken) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/account/:path*', '/checkout/:path*']
};

Best Practices

Cookie-Only Authentication

Use cookies for authentication in full-stack Next.js apps to ensure tokens are available to both client and server components

Environment Variables

Keep API keys secure and use appropriate Next.js environment variable patterns

Error Boundaries

Implement error boundaries to handle SDK errors gracefully in your React components

Type Safety

Leverage the SDK’s TypeScript support for better development experience and fewer runtime errors

Cross-References

This pattern ensures your authentication works consistently across Next.js server components, client components, and API routes. The cookie-based approach is recommended for full-stack Next.js applications.