# 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 or Pages Router)
- Node.js 18+
- Basic familiarity with Next.js configuration
# Installation
First, install the required dependencies:
npm install @ardrive/turbo-sdk
For client-side usage, you'll also need polyfill packages:
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.
# Configuration
# Step 1: Configure Webpack Polyfills
Create or update your next.config.js
file to include the necessary polyfills:
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config, { isServer }) => {
// 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 config.webpack.ProvidePlugin({
process: "process/browser",
Buffer: ["buffer", "Buffer"],
})
);
}
return config;
},
};
module.exports = nextConfig;
# Step 2: TypeScript Configuration (Optional)
If you're using TypeScript, update your tsconfig.json
to include proper module resolution:
{
"compilerOptions": {
"moduleResolution": "bundler",
"lib": ["es2015", "dom", "dom.iterable"]
// ... 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>;
};
}
# Usage Examples
# Wallet Integration Examples
WARNING
Never expose private keys in browser applications! Always use browser wallet integrations for security.
# Uploading with Metamask
WARNING
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>
);
}
# Uploading with Wander
"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>
);
}
# Common Issues and Solutions
# Build Errors
If you encounter build errors related to missing modules:
"Module not found: Can't resolve 'fs'"
- Ensure
fs: false
is set in your webpack fallback configuration
- Ensure
"process is not defined"
- Make sure you have the
ProvidePlugin
configuration for process
- Make sure you have the
"Buffer is not defined"
- Verify the Buffer polyfill is properly configured in
ProvidePlugin
- Verify the Buffer polyfill is properly configured in
# Runtime Errors
"crypto.getRandomValues is not a function"
- This usually indicates the crypto polyfill isn't working. Double-check your webpack configuration.
"TypeError: e.startsWith is not a function"
- This indicates incorrect signer usage. For MetaMask integration, use
InjectedEthereumSigner
from@dha-team/arbundles
, notEthereumSigner
. EthereumSigner
expects a private key string, whileInjectedEthereumSigner
expects a provider wrapper.
- This indicates incorrect signer usage. For MetaMask integration, use
"No accounts available" during wallet operations
- Ensure the wallet is properly connected before attempting operations
- Add validation to check account availability after connection
Message signing failures with wallets
- For
InjectedEthereumSigner
, ensure your provider wrapper correctly implements thegetSigner()
method - Handle both string and Uint8Array message types in your
signMessage
implementation - Use MetaMask's
personal_sign
method with proper parameter formatting
- For
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:
- Always use
import dynamic from "next/dynamic";
const TurboUploader = dynamic(() => import("./TurboUploader"), {
ssr: false,
});
# Wallet Integration Issues
Incorrect Signer Import
// ❌ INCORRECT - For Node environments import { EthereumSigner } from "@ardrive/turbo-sdk/web"; // ✅ CORRECT - For browser wallets import { InjectedEthereumSigner } from "@dha-team/arbundles";
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);
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
Use Client Components: Always mark components using the Turbo SDK with
'use client'
Error Handling: Implement proper error handling for network requests and wallet interactions
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
};
Bundle Size: Consider code splitting for large applications to reduce bundle size
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:
- Verify polyfills work correctly in your build environment
- Test wallet connections with various providers (Wander, MetaMask, etc.)
- Monitor bundle sizes to ensure polyfills don't significantly increase your app size
- Use environment-specific configurations for different Turbo endpoints
- Implement proper error boundaries for wallet connection failures
- Add loading states for wallet operations to improve UX
- Test across different browsers to ensure wallet compatibility
# Implementation Verification
To verify your MetaMask integration is working correctly:
Check Console Logs: After connecting to MetaMask, you should see:
Connected to chain: 0x1 (or appropriate chain ID)
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);
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
- Use
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
- No
# Additional Resources
- Turbo SDK Documentation
- Web Usage Examples
- Next.js Webpack Configuration (opens new window)
- ArDrive Examples Repository (opens new window)
For more examples and advanced usage patterns, refer to the Turbo SDK examples directory (opens new window) or the main SDK documentation.