AR.IO LogoAR.IO Documentation

Using Turbo SDK with Next.js

Using Turbo SDK with Next.js

Firefox Compatibility: Some compatibility issues have been reported with the Turbo SDK in Firefox browsers. At this time the below framework examples may not behave as expected in Firefox.

Overview

This guide demonstrates how to configure the @ardrive/turbo-sdk in a Next.js application with proper polyfills for client-side usage. Next.js uses webpack under the hood, which requires specific configuration to handle Node.js modules that the Turbo SDK depends on.

Polyfills: Polyfills are required when using the Turbo SDK in Next.js applications. The SDK relies on Node.js modules like crypto, buffer, process, and stream that are not available in the browser by default.

Prerequisites

  • Next.js 13+ (with App Router recommended)
  • Next.js 15 users: See updated configuration examples throughout this guide
  • Node.js 18+
  • Basic familiarity with Next.js configuration and webpack

Install the main Turbo SDK package:

npm install @ardrive/turbo-sdk

Add required polyfill packages for browser compatibility:

npm install --save-dev crypto-browserify stream-browserify process buffer

Wallet Integration Dependencies: The Turbo SDK includes @dha-team/arbundles as a peer dependency, which provides the necessary signers for browser wallet integration (like InjectedEthereumSigner and ArconnectSigner). You can import these directly without additional installation.

Next.js 15+: Use TypeScript config format (next.config.ts) with ESM exports. For earlier versions, use next.config.js with module.exports.

Create or update your next.config.ts file to include the necessary polyfills:

import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  webpack: (config, { isServer, webpack }) => {
    // Only configure polyfills for client-side bundles
    if (!isServer) {
      config.resolve.fallback = {
        ...config.resolve.fallback,
        crypto: require.resolve("crypto-browserify"),
        stream: require.resolve("stream-browserify"),
        buffer: require.resolve("buffer"),
        process: require.resolve("process/browser"),
        fs: false,
        net: false,
        tls: false,
      };

      // Provide global process and Buffer
      config.plugins.push(
        new webpack.ProvidePlugin({
          process: "process/browser",
          Buffer: ["buffer", "Buffer"],
        })
      );

      // CRITICAL: Handle node: protocol imports (e.g., node:stream, node:crypto)
      // The Turbo SDK uses modern Node.js imports that webpack doesn't understand
      config.plugins.push(
        new webpack.NormalModuleReplacementPlugin(
          /^node:/,
          (resource: any) => {
            const module = resource.request.replace(/^node:/, "");
            const fallbackMap: Record<string, string> = {
              crypto: "crypto-browserify",
              stream: "stream-browserify",
              buffer: "buffer",
              process: "process/browser",
            };
            resource.request = fallbackMap[module] || module;
          }
        )
      );
    }

    return config;
  },
};

export default nextConfig;

Why NormalModuleReplacementPlugin is required: The Turbo SDK uses node:stream, node:crypto, etc. imports. Without this plugin, you'll get UnhandledSchemeError: Reading from "node:stream" build errors.

Required for SDK Compatibility: The Turbo SDK requires ES2017 features. Set your TypeScript target to ES2017 or higher.

Update your tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["es2017", "dom", "dom.iterable", "esnext"],
    "module": "esnext",
    "moduleResolution": "bundler",
    "jsx": "preserve",
    "paths": {
      "@/*": ["./src/*"]
    }
    // ... other options
  }
}

TypeScript Wallet Types

Create a types/wallet.d.ts file to properly type wallet objects:

// types/wallet.d.ts
interface Window {
  ethereum?: {
    request: (args: { method: string; params?: any[] }) => Promise<any>;
    on?: (event: string, handler: (...args: any[]) => void) => void;
    removeListener?: (event: string, handler: (...args: any[]) => void) => void;
    isMetaMask?: boolean;
  };
  arweaveWallet?: {
    connect: (permissions: string[]) => Promise<void>;
    disconnect: () => Promise<void>;
    getActiveAddress: () => Promise<string>;
    getPermissions: () => Promise<string[]>;
    sign: (transaction: any) => Promise<any>;
    getPublicKey: () => Promise<string>;
  };
}

Select between client-side wallet or server-side signing integration based on your use case:

When to Use Client-Side Wallets (Steps 6-7)

Use MetaMask or Wander wallet integration when:

  • Users control their own wallets and pay for their uploads
  • Building a decentralized app (dApp) where users own their data
  • Users need to sign transactions with their private keys
  • You want users to manage their own Turbo credits

When to Use Server-Side Signing (Step 8)

Use server-side signing when:

  • Your application pays for uploads (most common for Next.js apps)
  • Background jobs or automated uploads
  • API-driven uploads without user wallets
  • You need centralized control over signing
  • Building traditional web apps with server-side authentication

Most Next.js apps use server-side signing. If you're building a typical web application where your backend handles uploads, skip to Step 8.

Never expose private keys in browser applications! Client-side patterns use browser wallets (no private keys in code). Server-side patterns keep private keys secure on the server.

Create a React component for MetaMask wallet integration:

For MetaMask integration, you'll need to use InjectedEthereumSigner from @dha-team/arbundles, which is available as a peer dependency through the Turbo SDK.

"use client";

import { TurboFactory } from "@ardrive/turbo-sdk/web";
import { InjectedEthereumSigner } from "@dha-team/arbundles";
import { useState, useCallback } from "react";

export default function MetaMaskUploader() {
  const [connected, setConnected] = useState(false);
  const [address, setAddress] = useState("");
  const [uploading, setUploading] = useState(false);
  const [uploadResult, setUploadResult] = useState(null);

  const connectMetaMask = useCallback(async () => {
    try {
      if (!window.ethereum) {
        alert("MetaMask is not installed!");
        return;
      }

      // Request account access
      await window.ethereum.request({
        method: "eth_requestAccounts",
      });

      // Get the current account
      const accounts = await window.ethereum.request({
        method: "eth_accounts",
      });

      if (accounts.length > 0) {
        setAddress(accounts[0]);
        setConnected(true);

        // Log current chain for debugging
        const chainId = await window.ethereum.request({
          method: "eth_chainId",
        });
        console.log("Connected to chain:", chainId);
      }
    } catch (error) {
      console.error("Failed to connect to MetaMask:", error);
    }
  }, []);

  const uploadWithMetaMask = async (event) => {
    const file = event.target.files?.[0];
    if (!file || !connected) return;

    setUploading(true);

    try {
      // Create a provider wrapper for InjectedEthereumSigner
      const providerWrapper = {
        getSigner: () => ({
          signMessage: async (message: string | Uint8Array) => {
            const accounts = await window.ethereum!.request({
              method: "eth_accounts",
            });
            if (accounts.length === 0) {
              throw new Error("No accounts available");
            }

            // Convert message to hex if it's Uint8Array
            const messageToSign =
              typeof message === "string"
                ? message
                : "0x" +
                  Array.from(message)
                    .map((b) => b.toString(16).padStart(2, "0"))
                    .join("");

            return await window.ethereum!.request({
              method: "personal_sign",
              params: [messageToSign, accounts[0]],
            });
          },
        }),
      };

      // Create the signer using InjectedEthereumSigner
      const signer = new InjectedEthereumSigner(providerWrapper);
      const turbo = TurboFactory.authenticated({
        signer,
        token: "ethereum", // Important: specify token type for Ethereum
      });

      // Upload file with progress tracking
      const result = await turbo.uploadFile({
        fileStreamFactory: () => file.stream(),
        fileSizeFactory: () => file.size,
        dataItemOpts: {
          tags: [
            { name: "Content-Type", value: file.type },
            { name: "App-Name", value: "My-Next-App" },
            { name: "Funded-By", value: "Ethereum" },
          ],
        },
        events: {
          onProgress: ({ totalBytes, processedBytes, step }) => {
            console.log(
              `${step}: ${Math.round((processedBytes / totalBytes) * 100)}%`
            );
          },
          onError: ({ error, step }) => {
            console.error(`Error during ${step}:`, error);
            console.error("Error details:", JSON.stringify(error, null, 2));
          },
        },
      });

      setUploadResult(result);
    } catch (error) {
      console.error("Upload failed:", error);
      console.error("Error details:", JSON.stringify(error, null, 2));
      alert(`Upload failed: ${error.message}`);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div className="p-6">
      <h2 className="text-2xl font-bold mb-4">MetaMask Upload</h2>

      {!connected ? (
        <button
          onClick={connectMetaMask}
          className="bg-orange-500 text-white px-4 py-2 rounded hover:bg-orange-600"
        >
          Connect MetaMask
        </button>
      ) : (
        <div>
          <p className="mb-4 text-green-600">
            ✅ Connected: {address.slice(0, 6)}...{address.slice(-4)}
          </p>

          <div className="mb-4">
            <label
              htmlFor="metamask-file"
              className="block text-sm font-medium mb-2"
            >
              Select File to Upload:
            </label>
            <input
              type="file"
              id="metamask-file"
              onChange={uploadWithMetaMask}
              disabled={uploading}
              className="block w-full text-sm border rounded-lg p-2"
            />
          </div>

          {uploading && (
            <div className="mb-4 p-3 bg-yellow-100 rounded">
              🔄 Uploading... Please confirm transaction in MetaMask
            </div>
          )}

          {uploadResult && (
            <div className="mt-4 p-3 bg-green-100 rounded">
              <p>
                <strong>✅ Upload Successful!</strong>
              </p>
              <p>
                <strong>Transaction ID:</strong> {uploadResult.id}
              </p>
              <p>
                <strong>Data Size:</strong> {uploadResult.totalBytes} bytes
              </p>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Create a React component for Wander wallet integration:

"use client";

import { TurboFactory, ArconnectSigner } from "@ardrive/turbo-sdk/web";
import { useState, useCallback } from "react";

export default function WanderWalletUploader() {
  const [connected, setConnected] = useState(false);
  const [address, setAddress] = useState("");
  const [uploading, setUploading] = useState(false);
  const [uploadResult, setUploadResult] = useState(null);

  const connectWanderWallet = useCallback(async () => {
    try {
      if (!window.arweaveWallet) {
        alert("Wander wallet is not installed!");
        return;
      }

      // Required permissions for Turbo SDK
      const permissions = [
        "ACCESS_ADDRESS",
        "ACCESS_PUBLIC_KEY",
        "SIGN_TRANSACTION",
        "SIGNATURE",
      ];

      // Connect to wallet
      await window.arweaveWallet.connect(permissions);

      // Get wallet address
      const walletAddress = await window.arweaveWallet.getActiveAddress();
      setAddress(walletAddress);
      setConnected(true);
    } catch (error) {
      console.error("Failed to connect to Wander wallet:", error);
    }
  }, []);

  const uploadWithWanderWallet = async (event) => {
    const file = event.target.files?.[0];
    if (!file || !connected) return;

    setUploading(true);

    try {
      // Create ArConnect signer using Wander wallet
      const signer = new ArconnectSigner(window.arweaveWallet);
      const turbo = TurboFactory.authenticated({ signer });
      // Note: No need to specify token for Arweave as it's the default

      // Upload file with progress tracking
      const result = await turbo.uploadFile({
        fileStreamFactory: () => file.stream(),
        fileSizeFactory: () => file.size,
        dataItemOpts: {
          tags: [
            { name: "Content-Type", value: file.type },
            { name: "App-Name", value: "My-Next-App" },
            { name: "Funded-By", value: "Arweave" },
          ],
        },
        events: {
          onProgress: ({ totalBytes, processedBytes, step }) => {
            console.log(
              `${step}: ${Math.round((processedBytes / totalBytes) * 100)}%`
            );
          },
          onError: ({ error, step }) => {
            console.error(`Error during ${step}:`, error);
          },
        },
      });

      setUploadResult(result);
    } catch (error) {
      console.error("Upload failed:", error);
      alert(`Upload failed: ${error.message}`);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div className="p-6">
      <h2 className="text-2xl font-bold mb-4">Wander Wallet Upload</h2>

      {!connected ? (
        <button
          onClick={connectWanderWallet}
          className="bg-black text-white px-4 py-2 rounded hover:bg-gray-800"
        >
          Connect Wander Wallet
        </button>
      ) : (
        <div>
          <p className="mb-4 text-green-600">
            ✅ Connected: {address.slice(0, 6)}...{address.slice(-4)}
          </p>

          <div className="mb-4">
            <label
              htmlFor="wander-file"
              className="block text-sm font-medium mb-2"
            >
              Select File to Upload:
            </label>
            <input
              type="file"
              id="wander-file"
              onChange={uploadWithWanderWallet}
              disabled={uploading}
              className="block w-full text-sm border rounded-lg p-2"
            />
          </div>

          {uploading && (
            <div className="mb-4 p-3 bg-yellow-100 rounded">
              🔄 Uploading... Please confirm transaction in Wander wallet
            </div>
          )}

          {uploadResult && (
            <div className="mt-4 p-3 bg-green-100 rounded">
              <p>
                <strong>✅ Upload Successful!</strong>
              </p>
              <p>
                <strong>Transaction ID:</strong> {uploadResult.id}
              </p>
              <p>
                <strong>Data Size:</strong> {uploadResult.totalBytes} bytes
              </p>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

Recommended for most Next.js applications. This pattern keeps your private keys secure on the server while allowing uploads from the client.

Architecture

  • Client: Uses walletAdapter with Turbo SDK, delegates signing to API route
  • Server: API route signs data with ethers.js Wallet, returns signature
  • Security: Private keys stay server-side, never exposed to browser

Installation

Install ethers.js (if not already installed):

npm install ethers

Environment Setup

Getting a Private Key

You'll need an Ethereum-compatible private key for server-side signing:

For Testing/Development:

  • Generate a new wallet with any Ethereum wallet (MetaMask, Rainbow, etc.)
  • Export the private key (usually in wallet settings under "Show private keys")
  • Copy the private key (starts with 0x)

For Production:

  • Use a dedicated wallet for your application
  • Fund it with the appropriate token (USDC on Base, ETH, etc.)
  • Never use a personal wallet with significant funds

Quick Test Key Generation (Node.js):

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Add 0x prefix: 0x[generated_hex]

Create a .env.local file in your project root:

# .env.local (NEVER commit this file to git!)
ETHEREUM_PRIVATE_KEY=0xYourPrivateKeyHere

Security: Add .env.local to your .gitignore file. Never commit private keys to version control.

Funding Your Wallet: After setting up your private key, you'll need to fund the corresponding wallet address with Turbo credits. Visit https://turbo.ardrive.io to add credits to your wallet.

Token Selection

Choose your token type based on the blockchain you're using:

  • "base-usdc" - USDC on Base Network (recommended for low fees)
  • "ethereum" - ETH on Ethereum Mainnet
  • "polygon-usdc" - USDC on Polygon Network
  • "matic" or "pol" - MATIC/POL on Polygon Network
  • "solana" - SOL on Solana

Your private key must match the blockchain you select. For example, if using "base-usdc", use an Ethereum-compatible private key.

Server API Route

Create app/api/sign/route.ts:

import { NextResponse } from "next/server";
import { Wallet } from "ethers";
import "server-only";

// Validate that private key exists in environment variables
if (!process.env.ETHEREUM_PRIVATE_KEY) {
  throw new Error(
    "ETHEREUM_PRIVATE_KEY is not set. Please add it to your .env.local file."
  );
}

const wallet = new Wallet(process.env.ETHEREUM_PRIVATE_KEY);

async function signData(data: string | Uint8Array): Promise<string> {
  // ethers.js handles both UTF-8 strings and binary data
  return await wallet.signMessage(data);
}

function getPublicKey(): string {
  // Return uncompressed public key (keep '04' prefix, remove only '0x')
  return wallet.signingKey.publicKey.slice(2);
}

export async function GET() {
  return NextResponse.json({ publicKey: getPublicKey() });
}

export async function POST(req: Request) {
  const body = await req.json() as {
    signatureData: string;
    isHex?: boolean;
  };

  if (!body?.signatureData) {
    return NextResponse.json(
      { error: "Missing signatureData" },
      { status: 400 }
    );
  }

  // Handle both string messages (UTF-8) and binary data (hex-encoded)
  const dataToSign = body.isHex
    ? Buffer.from(body.signatureData, "hex")
    : body.signatureData;

  const signature = await signData(dataToSign);
  return NextResponse.json({ signature });
}

Next.js 15 Requirement: Route handlers can ONLY export HTTP method functions (GET, POST, etc.). Helper functions like signData and getPublicKey must NOT be exported.

Client Component

Create app/upload/component.tsx:

"use client";

import { TurboFactory, type EthereumWalletSigner } from "@ardrive/turbo-sdk/web";
import { useMemo, useState } from "react";

// Buffer is available globally from webpack polyfills configured in next.config.ts
// No import needed - it's provided by the ProvidePlugin

// API endpoint for server-side signing (corresponds to app/api/sign/route.ts)
const signEndpoint = "/api/sign";

export function UploadComponent() {
  const [uploading, setUploading] = useState(false);

  // Create turbo instance with useMemo to avoid recreating on every render
  // This prevents SSR issues and unnecessary re-initialization
  const turbo = useMemo(() => {
    return TurboFactory.authenticated({
      token: "base-usdc", // Match the token type from your environment
      walletAdapter: {
        getSigner: () => {
          return {
            signMessage: async (_message: string | Uint8Array) => {
              // Determine if message is binary (Uint8Array) or string
              const isHex = typeof _message !== "string";
              const message = isHex
                ? Buffer.from(_message).toString("hex")
                : _message;

              const res = await fetch(signEndpoint, {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({ signatureData: message, isHex }),
              });

              if (!res.ok) {
                throw new Error(`Signing failed: ${res.statusText}`);
              }

              const { signature } = await res.json();
              return signature;
            },
            sendTransaction: async () => {
              throw new Error("Not implemented");
            },
            provider: {
              // Fetch public key from server
              getPublicKey: async () => {
                const res = await fetch(signEndpoint);
                const { publicKey } = await res.json();
                return publicKey;
              },
              getSigner: () => ({
                // Return address (same as public key for this implementation)
                getAddress: async () => {
                  const res = await fetch(signEndpoint);
                  const { publicKey } = await res.json();
                  return publicKey;
                },
                // Sign typed data (used for certain Turbo operations)
                _signTypedData: async (
                  _domain: never,
                  _types: never,
                  message: {
                    address: string;
                    "Transaction hash": Uint8Array;
                  }
                ) => {
                  const convertedMsg = Buffer.from(
                    message["Transaction hash"]
                  ).toString("hex");

                  const res = await fetch(signEndpoint, {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                      signatureData: convertedMsg,
                      isHex: true
                    }),
                  });

                  if (!res.ok) {
                    throw new Error(`Signing failed: ${res.statusText}`);
                  }

                  const { signature } = await res.json();
                  return signature;
                },
              }),
              _ready: () => {},
            },
          } as unknown as EthereumWalletSigner;
        },
      },
    });
  }, []);

  // Upload string data
  const handleUploadText = async () => {
    setUploading(true);
    try {
      const result = await turbo.upload({
        data: "Hello, ArDrive Turbo!",
      });
      console.log("Upload successful! ID:", result.id);
      alert(`Upload successful! ID: ${result.id}`);
    } catch (error) {
      console.error("Upload failed:", error);
      alert(`Upload failed: ${(error as Error).message}`);
    } finally {
      setUploading(false);
    }
  };

  // Upload file
  const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    setUploading(true);
    try {
      const result = await turbo.uploadFile({
        fileStreamFactory: () => file.stream(),
        fileSizeFactory: () => file.size,
        dataItemOpts: {
          tags: [
            { name: "Content-Type", value: file.type },
            { name: "File-Name", value: file.name },
          ],
        },
      });
      console.log("File upload successful! ID:", result.id);
      alert(`File uploaded! ID: ${result.id}`);
    } catch (error) {
      console.error("Upload failed:", error);
      alert(`Upload failed: ${(error as Error).message}`);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <button onClick={handleUploadText} disabled={uploading}>
        {uploading ? "Uploading..." : "Upload Text"}
      </button>

      <label htmlFor="file-upload">
        <input
          id="file-upload"
          type="file"
          onChange={handleFileUpload}
          disabled={uploading}
        />
      </label>
    </div>
  );
}

Styling: This is a basic example. Add your own styles, labels, and accessibility features as needed for your application.

Key Implementation Details

  1. useMemo for Turbo Instance: Creating the turbo instance inside useMemo prevents it from being recreated on every render and avoids SSR issues. The turbo client is initialized once when the component mounts.

  2. Provider Implementation: The provider object is required for Ethereum wallets and includes:

    • getPublicKey() - Fetches public key from GET /api/sign
    • getSigner().getAddress() - Returns the wallet address (same as public key)
    • getSigner()._signTypedData() - Signs EIP-712 typed data for certain Turbo operations
    • _ready() - Lifecycle method (can be empty)
  3. Buffer Global: Buffer is available globally from the webpack polyfills you configured in Step 3. No import needed - the ProvidePlugin makes it available everywhere.

  4. isHex Flag: The SDK passes both UTF-8 strings (e.g., during key setup) and binary data (file content). Use this flag to differentiate handling on the server.

  5. File Uploads: Use uploadFile() with fileStreamFactory and fileSizeFactory for files. Use upload() with data for strings or Blobs.

  6. Public Key Format: Server returns uncompressed public key with 04 prefix byte. Use .slice(2) to remove only 0x, keeping 04.

  7. Signature Format: ethers.js produces EIP-191 signatures compatible with the SDK's verification.

  8. Token Matching: Ensure the token in your client matches your server's blockchain (e.g., both use "base-usdc").

  9. Security: Private keys stay in .env.local on the server, never exposed to the browser.

Complete Working Example

To use this component, add it to a page:

// app/page.tsx
import { UploadComponent } from "./upload/component";

export default function Home() {
  return (
    <main>
      <h1>Upload to ArDrive Turbo</h1>
      <UploadComponent />
    </main>
  );
}

Now you can:

  • Click "Upload Text" to upload a string
  • Use the file input to upload any file
  • Both use server-side signing for security

Common Issues and Solutions

Build Errors

If you encounter build errors related to missing modules:

  1. "UnhandledSchemeError: Reading from 'node:stream'" ⚠️ CRITICAL

    • The Turbo SDK uses node: protocol imports that webpack doesn't understand
    • Solution: Add NormalModuleReplacementPlugin to your webpack config (see Step 3)
    • This is the most common build error in Next.js 15
  2. "Module not found: Can't resolve 'fs'"

    • Ensure fs: false is set in your webpack fallback configuration
  3. "process is not defined"

    • Make sure you have the ProvidePlugin configuration for process
  4. "Buffer is not defined"

    • Verify the Buffer polyfill is properly configured in ProvidePlugin

Next.js 15 Specific Issues

  1. "Type does not satisfy constraint" in route handlers

    • Next.js 15 requires route files to ONLY export HTTP method functions
    • Solution: Remove export from helper functions
    // ❌ INCORRECT
    export async function helper() { ... }
    export async function POST() { ... }
    
    // ✅ CORRECT
    async function helper() { ... }  // Not exported
    export async function POST() { ... }
  2. TypeScript compilation errors

    • Solution: Set "target": "ES2017" in tsconfig.json
    • The SDK requires ES2017 features

Runtime Errors

  1. "crypto.getRandomValues is not a function"

    • This usually indicates the crypto polyfill isn't working. Double-check your webpack configuration.
  2. "TypeError: e.startsWith is not a function"

    • This indicates incorrect signer usage. For MetaMask integration, use InjectedEthereumSigner from @dha-team/arbundles, not EthereumSigner.
    • EthereumSigner expects a private key string, while InjectedEthereumSigner expects a provider wrapper.
  3. "No accounts available" during wallet operations

    • Ensure the wallet is properly connected before attempting operations
    • Add validation to check account availability after connection
  4. Message signing failures with wallets

    • For InjectedEthereumSigner, ensure your provider wrapper correctly implements the getSigner() method
    • Handle both string and Uint8Array message types in your signMessage implementation
    • Use MetaMask's personal_sign method with proper parameter formatting
  5. Server-side rendering issues

    • Always use 'use client' directive for components that use the Turbo SDK
    • Consider dynamic imports with ssr: false for complex cases:
import dynamic from "next/dynamic";

const TurboUploader = dynamic(() => import("./TurboUploader"), {
  ssr: false,
});

Server-Side Signing Issues

  1. "Invalid Data Item" errors (400 Bad Request)

    • Usually caused by incorrect signature format or public key issues
    • Common causes:
      • Public key missing 04 prefix byte
      • Incorrect handling of string vs binary data
      • Wrong signature encoding
      • Token mismatch between client and server
  2. Turbo Instance SSR/Re-render Issues

    // ❌ INCORRECT - Creates instance at module level
    const turbo = TurboFactory.authenticated({ ... });
    
    export function Component() {
      // Uses turbo - will cause SSR issues
    }
    
    // ✅ CORRECT - Create inside component with useMemo
    export function Component() {
      const turbo = useMemo(() =>
        TurboFactory.authenticated({ ... })
      , []);
    }
    • Creating at module level runs on server during SSR
    • Creates new instance on every render without memoization
    • Solution: Always use useMemo hook inside component
  3. Public Key Format Issues

    // ❌ INCORRECT - Removes both '0x' AND '04'
    wallet.signingKey.publicKey.slice(4)
    
    // ✅ CORRECT - Removes only '0x', keeps '04' prefix
    wallet.signingKey.publicKey.slice(2)
    • The uncompressed Ethereum public key format is 04 + x_coord + y_coord
    • The SDK expects this full format
  4. String vs Binary Data Handling

    • The SDK passes BOTH types to signMessage:
      • UTF-8 strings (e.g., "sign this message to connect to Bundlr.Network") during setup
      • Binary data (Uint8Array) for actual file content
    • Solution: Use isHex flag to differentiate:
    const isHex = typeof message !== "string";
    const messageStr = isHex
      ? Buffer.from(message).toString("hex")
      : message;
    
    // Send to server with isHex flag
    fetch("/api/sign", {
      body: JSON.stringify({ signatureData: messageStr, isHex })
    });
  5. "Buffer is not defined"

    • Buffer should be available globally from webpack polyfills
    • Solution: Verify ProvidePlugin is configured correctly in next.config.ts (Step 3)
    • No import needed - it's provided globally
  6. Using TypedEthereumSigner instead of ethers.js Wallet

    • TypedEthereumSigner from @dha-team/arbundles produces incompatible signature format
    • Solution: Use ethers.js Wallet for server-side signing
    • Ethers produces proper EIP-191 signatures with recovery parameter
  7. Token Mismatch Errors

    • Client and server must use the same token type
    • Solution: Ensure token: "base-usdc" matches between:
      • Client TurboFactory.authenticated()
      • Server environment (private key's blockchain)
      • Your Turbo account funding

Wallet Integration Issues

  1. Incorrect Signer Import

    // ❌ INCORRECT - For Node environments
    import { EthereumSigner } from "@ardrive/turbo-sdk/web";
    
    // ✅ CORRECT - For browser wallets
    import { InjectedEthereumSigner } from "@dha-team/arbundles";
  2. Provider Interface Mismatch

    // ❌ INCORRECT - window.ethereum doesn't have getSigner()
    const signer = new InjectedEthereumSigner(window.ethereum);
    
    // ✅ CORRECT - Use a provider wrapper
    const providerWrapper = {
      getSigner: () => ({
        signMessage: async (message: string | Uint8Array) => {
          // Implementation here
        },
      }),
    };
    const signer = new InjectedEthereumSigner(providerWrapper);
  3. Missing Dependencies

    If you encounter import errors for @dha-team/arbundles, note that it's available as a peer dependency through @ardrive/turbo-sdk. You may need to ensure it's properly resolved in your build process.

Best Practices

  1. Use Client Components: Always mark components using the Turbo SDK with 'use client'

  2. Error Handling: Implement proper error handling for network requests and wallet interactions

  3. Environment Variables: Store sensitive configuration in environment variables:

// next.config.js
const nextConfig = {
  env: {
    TURBO_UPLOAD_URL: process.env.TURBO_UPLOAD_URL,
    TURBO_PAYMENT_URL: process.env.TURBO_PAYMENT_URL,
  },
  // ... webpack config
};
  1. Bundle Size: Consider code splitting for large applications to reduce bundle size

  2. Wallet Security:

    • Never expose private keys in client-side code
    • Always use browser wallet integrations (MetaMask, Wander, etc.)
    • Request only necessary permissions from wallets
    • Validate wallet connections before use
    • Handle wallet disconnection gracefully

Production Deployment Checklist

For production deployments:

  1. Verify polyfills work correctly in your build environment
  2. Test wallet connections with various providers (Wander, MetaMask, etc.)
  3. Monitor bundle sizes to ensure polyfills don't significantly increase your app size
  4. Use environment-specific configurations for different Turbo endpoints
  5. Implement proper error boundaries for wallet connection failures
  6. Add loading states for wallet operations to improve UX
  7. Test across different browsers to ensure wallet compatibility

Implementation Verification

To verify your MetaMask integration is working correctly:

  1. Check Console Logs: After connecting to MetaMask, you should see:

    Connected to chain: 0x1 (or appropriate chain ID)
  2. Test Balance Retrieval: Add this to verify your authenticated client works:

    // After creating authenticated turbo client
    const balance = await turbo.getBalance();
    console.log("Current balance:", balance);
  3. Verify Signer Setup: Your implementation should:

    • Use InjectedEthereumSigner from @dha-team/arbundles
    • Include a proper provider wrapper with getSigner() method
    • Handle both string and Uint8Array message types
    • Use MetaMask's personal_sign method
  4. Common Success Indicators:

    • No TypeError: e.startsWith is not a function errors
    • Successful wallet connection and address display
    • Ability to fetch balance without errors
    • Upload operations work with proper MetaMask transaction prompts

Additional Resources


For more examples and advanced usage patterns, refer to the Turbo SDK examples directory or the main SDK documentation.

How is this guide?