Mesh LogoMesh

Deploy Cardano dApps to Production

Security best practices, deployment checklists, and error handling for production Cardano dApps with Mesh SDK.

Overview

Deploying Cardano dApps to production requires attention to security, reliability, and user experience. This guide covers essential practices for enterprise-grade deployments.

What you will learn

  • Private key management best practices
  • Transaction validation patterns
  • Provider configuration and failover
  • Error handling strategies
  • Monitoring and alerting setup

Prerequisites

  • A working Cardano dApp (see Next.js guide)
  • Basic understanding of deployment environments
  • Access to production infrastructure

Time to complete

90 minutes to review and implement

Security Best Practices

Private Key Management

Never expose private keys in client-side code. For production applications:

Browser wallets for users:

Let users manage their own keys through wallet extensions. The MeshCardanoBrowserWallet class handles CIP-30 wallet connections without your application touching private keys:

import { MeshCardanoBrowserWallet } from "@meshsdk/wallet";

// User controls their own keys
const wallet = await MeshCardanoBrowserWallet.enable("eternl");
const signedTx = await wallet.signTx(unsignedTx, false);

Server-side signing:

When your application must sign transactions (automated systems, treasury):

import { MeshCardanoHeadlessWallet, AddressType } from "@meshsdk/wallet";

// Store mnemonic in environment variables or secret manager
const mnemonic = process.env.TREASURY_MNEMONIC;

// Never log or expose the mnemonic
const wallet = await MeshCardanoHeadlessWallet.fromMnemonic({
  networkId: 1,
  walletAddressType: AddressType.Base,
  fetcher: provider,
  submitter: provider,
  mnemonic: mnemonic.split(" "),
});

Security checklist:

  • Use environment variables for credentials
  • Run signing services in isolated network segments
  • Implement audit logging for all signing operations
  • Consider hardware security modules (HSMs) for high-value operations
  • Rotate keys periodically

Transaction Validation

Validate all parameters before building transactions:

function validateTransaction(params: {
  recipient: string;
  amount: string;
}) {
  // Check amount is positive and within bounds
  const lovelace = BigInt(params.amount);
  if (lovelace <= 0n) {
    throw new Error("Amount must be positive");
  }
  if (lovelace > 45_000_000_000_000_000n) {
    throw new Error("Amount exceeds maximum supply");
  }

  // Validate address format
  if (!params.recipient.startsWith("addr")) {
    throw new Error("Invalid Cardano address");
  }

  // Check minimum UTxO requirement
  if (lovelace < 1_000_000n) {
    throw new Error("Amount below minimum UTxO");
  }
}

API Key Protection

Never expose provider API keys in client-side code:

// BAD: Key exposed in browser bundle
const provider = new BlockfrostProvider("proj_live_abc123");

// GOOD: Proxy through your backend
const response = await fetch("/api/blockchain/utxos", {
  method: "POST",
  body: JSON.stringify({ address }),
});

API route example:

// app/api/blockchain/utxos/route.ts
import { BlockfrostProvider } from "@meshsdk/core";

const provider = new BlockfrostProvider(process.env.BLOCKFROST_KEY!);

export async function POST(request: Request) {
  const { address } = await request.json();
  const utxos = await provider.fetchAddressUTxOs(address);
  return Response.json(utxos);
}

Deployment Checklist

Pre-Launch Infrastructure

TaskStatus
Provider API keys stored securely (not in client bundle)[ ]
Backend proxy configured for blockchain queries[ ]
Rate limiting implemented on API endpoints[ ]
Error monitoring and alerting configured[ ]
Load testing completed for expected traffic[ ]

Smart Contracts

TaskStatus
Contracts audited by independent party[ ]
Testnet deployment validated thoroughly[ ]
Contract addresses verified on mainnet[ ]
Reference scripts deployed if used[ ]
Monitoring configured for contract interactions[ ]

Application

TaskStatus
Console.log statements removed from production build[ ]
Source maps disabled or properly secured[ ]
CSP headers configured appropriately[ ]
HTTPS enforced with valid certificates[ ]
Input validation comprehensive[ ]

Provider Configuration

Provider Selection

For production applications, consider:

ProviderStrengthConsideration
BlockfrostReliability, documentationRate limits on free tier
KoiosFree tier, decentralizedSelf-hosted option available
MaestroRich data, marketplace supportEnterprise pricing
Self-hosted OgmiosFull control, no rate limitsInfrastructure overhead

Provider Failover

Implement automatic failover when providers fail:

class ResilientProvider implements IFetcher {
  private providers: IFetcher[];
  private currentIndex = 0;

  constructor(providers: IFetcher[]) {
    this.providers = providers;
  }

  async fetchAddressUTxOs(address: string, asset?: string) {
    for (let attempt = 0; attempt < this.providers.length; attempt++) {
      try {
        const index = (this.currentIndex + attempt) % this.providers.length;
        return await this.providers[index].fetchAddressUTxOs(address, asset);
      } catch (error) {
        console.warn(`Provider ${attempt} failed, trying next`);
      }
    }
    throw new Error("All providers failed");
  }

  // Implement other IFetcher methods similarly
}

// Usage
const provider = new ResilientProvider([
  new BlockfrostProvider(process.env.BLOCKFROST_KEY!),
  new KoiosProvider("mainnet"),
]);

Network Selection

NetworkUse Case
PreviewTesting new features, unstable
PreprodProduction-like testing, staging
MainnetProduction deployment

Error Handling

Transaction Failures

Handle failures gracefully with user-friendly messages:

async function submitTransaction(unsignedTx: string) {
  try {
    const signedTx = await wallet.signTx(unsignedTx);
    const txHash = await wallet.submitTx(signedTx);
    return { success: true, txHash };
  } catch (error: any) {
    // Parse and categorize errors
    if (error.message.includes("INSUFFICIENT_FUNDS")) {
      return {
        success: false,
        error: "Insufficient funds. Please add more ADA to your wallet.",
      };
    }

    if (error.message.includes("UTXO_ALREADY_SPENT")) {
      return {
        success: false,
        error: "Transaction conflict. Please try again.",
      };
    }

    if (error.message.includes("User rejected")) {
      return {
        success: false,
        error: "Transaction cancelled.",
      };
    }

    // Log technical details for debugging
    console.error("Transaction failed:", error);

    return {
      success: false,
      error: "Transaction failed. Please try again later.",
    };
  }
}

UTxO Contention

In high-traffic applications, UTxOs may be spent between query and submission:

async function buildWithRetry(
  buildFn: () => Promise<string>,
  maxRetries = 3
) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await buildFn();
    } catch (error: any) {
      if (
        error.message.includes("UTXO_ALREADY_SPENT") &&
        attempt < maxRetries - 1
      ) {
        // Wait briefly and retry with fresh UTxOs
        await new Promise((r) => setTimeout(r, 1000 * (attempt + 1)));
        continue;
      }
      throw error;
    }
  }
}

Performance Optimization

Bundle Size

Minimize client bundle size:

// Import specific functions instead of entire package
import { MeshTxBuilder, BlockfrostProvider } from "@meshsdk/core";

// Use dynamic imports for wallet-specific features
const ConnectWallet = dynamic(() => import("./ConnectWallet"), {
  ssr: false,
});

Caching

Reduce redundant blockchain queries:

class CachedProvider implements IFetcher {
  private provider: IFetcher;
  private cache = new Map<string, { data: any; expiry: number }>();

  constructor(provider: IFetcher) {
    this.provider = provider;
  }

  async fetchProtocolParameters(epoch?: number) {
    const cacheKey = `protocol-${epoch || "current"}`;
    const cached = this.cache.get(cacheKey);

    if (cached && cached.expiry > Date.now()) {
      return cached.data;
    }

    const data = await this.provider.fetchProtocolParameters(epoch);

    // Cache for 1 hour (protocol params rarely change)
    this.cache.set(cacheKey, {
      data,
      expiry: Date.now() + 3600000,
    });

    return data;
  }

  // Implement other methods with appropriate cache TTLs
}

Monitoring

Key Metrics

Track these metrics for production dApps:

Transaction metrics:

  • Submission success rate
  • Average confirmation time
  • Failed transaction breakdown by error type
  • Transaction fee distribution

User metrics:

  • Wallet connection success rate
  • Transaction abandonment rate
  • Error frequency by type

Infrastructure metrics:

  • Provider response times
  • API endpoint latency
  • Error rates by endpoint

Alerting

Configure alerts for:

  • Sustained transaction failure rates above 5%
  • Provider connectivity issues lasting > 1 minute
  • Smart contract interaction anomalies
  • Unusual transaction patterns

Next Steps

Troubleshooting

Transactions timing out

Cause: Network congestion or provider issues.

Solution: Implement longer timeouts and retry logic. Consider using multiple providers for redundancy.

Wallet connection failing in production

Cause: CSP headers blocking wallet extension communication.

Solution: Configure Content Security Policy to allow wallet connections:

// next.config.js
const securityHeaders = [
  {
    key: "Content-Security-Policy",
    value: "default-src 'self'; script-src 'self' 'unsafe-eval';",
  },
];

Rate limiting errors

Cause: Exceeding provider API limits.

Solution: Implement request queuing, caching, or upgrade to a higher tier. Consider self-hosted options for high-traffic applications.

On this page