Skip to main content

SSR Support

This guide covers server-side rendering considerations for Next.js, Remix, and other SSR frameworks.

The Challenge

The Privacy Boost SDK uses WebAssembly and browser APIs that aren’t available on the server. You need to ensure the SDK only loads on the client.

Next.js

App Router

// app/providers.tsx
'use client';

import { PrivacyBoostProvider } from '@testinprod-io/privacy-boost-react';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <PrivacyBoostProvider
      config={{
        indexerUrl: process.env.NEXT_PUBLIC_INDEXER_URL!,
        proverUrl: process.env.NEXT_PUBLIC_PROVER_URL!,
        chainId: Number(process.env.NEXT_PUBLIC_CHAIN_ID),
        shieldContract: process.env.NEXT_PUBLIC_SHIELD_CONTRACT as `0x${string}`,
      }}
      loadingComponent={<div>Loading...</div>}
    >
      {children}
    </PrivacyBoostProvider>
  );
}
// app/layout.tsx
import { Providers } from './providers';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Pages Router

// pages/_app.tsx
import type { AppProps } from 'next/app';
import dynamic from 'next/dynamic';

// Dynamic import with SSR disabled
const PrivacyBoostProvider = dynamic(
  () => import('@testinprod-io/privacy-boost-react').then(mod => mod.PrivacyBoostProvider),
  { ssr: false }
);

export default function App({ Component, pageProps }: AppProps) {
  return (
    <PrivacyBoostProvider config={config}>
      <Component {...pageProps} />
    </PrivacyBoostProvider>
  );
}

Dynamic Component Import

For components that use SDK hooks:
// components/Wallet.tsx
'use client';

import { useAuth } from '@testinprod-io/privacy-boost-react';

export function Wallet() {
  const { isAuthenticated, authenticateInjected } = useAuth();
  // ...
}

// pages/index.tsx (Pages Router)
import dynamic from 'next/dynamic';

const Wallet = dynamic(() => import('../components/Wallet'), {
  ssr: false,
  loading: () => <div>Loading wallet...</div>,
});

export default function Home() {
  return <Wallet />;
}

Remix

Client-Only Provider

// app/components/PrivacyProvider.client.tsx
import { PrivacyBoostProvider } from '@testinprod-io/privacy-boost-react';

export function PrivacyProvider({ children }: { children: React.ReactNode }) {
  return (
    <PrivacyBoostProvider config={config}>
      {children}
    </PrivacyBoostProvider>
  );
}
// app/root.tsx
import { ClientOnly } from 'remix-utils';
import { PrivacyProvider } from './components/PrivacyProvider.client';

export default function App() {
  return (
    <html>
      <body>
        <ClientOnly fallback={<div>Loading...</div>}>
          {() => (
            <PrivacyProvider>
              <Outlet />
            </PrivacyProvider>
          )}
        </ClientOnly>
      </body>
    </html>
  );
}

Hydration Mismatch Prevention

useIsClient Hook

function useIsClient() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return isClient;
}

function WalletButton() {
  const isClient = useIsClient();
  const { isAuthenticated } = useAuth();

  if (!isClient) {
    return <button disabled>Loading...</button>;
  }

  return (
    <button onClick={authenticateInjected}>
      {isAuthenticated ? 'Connected' : 'Connect'}
    </button>
  );
}

Conditional Rendering

function ClientOnlyComponent({ children }: { children: React.ReactNode }) {
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) {
    return null;
  }

  return <>{children}</>;
}

// Usage
function Page() {
  return (
    <ClientOnlyComponent>
      <WalletComponent />
    </ClientOnlyComponent>
  );
}

Environment Variables

Next.js

# .env.local
NEXT_PUBLIC_INDEXER_URL=https://test-api.privacy-boost.sunnyside.io/indexer
NEXT_PUBLIC_PROVER_URL=https://test-api.privacy-boost.sunnyside.io/prover
NEXT_PUBLIC_CHAIN_ID=1
NEXT_PUBLIC_SHIELD_CONTRACT=0x...

Remix

// app/root.tsx
export async function loader() {
  return json({
    ENV: {
      INDEXER_URL: process.env.INDEXER_URL,
      PROVER_URL: process.env.PROVER_URL,
      CHAIN_ID: process.env.CHAIN_ID,
      SHIELD_CONTRACT: process.env.SHIELD_CONTRACT,
    },
  });
}

Webpack Configuration

Next.js

// next.config.js
module.exports = {
  webpack: (config, { isServer }) => {
    config.experiments = {
      ...config.experiments,
      asyncWebAssembly: true,
    };

    // Exclude WASM from server bundle
    if (isServer) {
      config.externals = [...config.externals, '@testinprod-io/privacy-boost'];
    }

    return config;
  },
};

Testing SSR

// Check if window is available
function isBrowser() {
  return typeof window !== 'undefined';
}

function SafeComponent() {
  if (!isBrowser()) {
    return <ServerFallback />;
  }

  return <ClientComponent />;
}

Best Practices

  1. Always use ‘use client’ directive for components using SDK hooks
  2. Dynamic imports for large components to reduce bundle size
  3. Provide fallbacks for loading and server-rendered states
  4. Avoid window/document access without checking environment
  5. Use environment variables with NEXT_PUBLIC_ prefix for client access

Next Steps