Web2-Web3 Integration Overview

Integrating traditional web systems with blockchain requires careful consideration of authentication, data flow, and trust boundaries.

Integration Architecture

Text
┌─────────────────────────────────────────────────────────────────┐
│               Web2-Web3 Integration Architecture                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────┐         ┌─────────────────┐                │
│  │     Web2        │         │     Web3        │                │
│  │   (Traditional) │         │  (Blockchain)   │                │
│  │                 │         │                 │                │
│  │  • Database     │◄───────▶│  • Smart        │                │
│  │  • API Server   │         │    Contracts    │                │
│  │  • Auth System  │         │  • Wallets      │                │
│  │  • Payment      │         │  • Tokens       │                │
│  └────────┬────────┘         └────────┬────────┘                │
│           │                           │                         │
│           │     ┌─────────────────┐   │                         │
│           └────▶│   Integration   │◀──┘                         │
│                 │      Layer      │                             │
│                 │                 │                             │
│                 │  • Webhooks     │                             │
│                 │  • Indexers     │                             │
│                 │  • Oracles      │                             │
│                 │  • Bridge       │                             │
│                 └─────────────────┘                             │
│                         │                                       │
│                         ▼                                       │
│                 ┌─────────────────┐                             │
│                 │    Frontend     │                             │
│                 │                 │                             │
│                 │  • Wallet UI    │                             │
│                 │  • Traditional  │                             │
│                 │    UI           │                             │
│                 └─────────────────┘                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Integration Patterns

PatternUse CaseDirection
Sign-In with SolanaWeb2 auth via walletWeb3 → Web2
OracleOff-chain data on-chainWeb2 → Web3
WebhookOn-chain events to serversWeb3 → Web2
Payment GatewayAccept crypto paymentsWeb3 → Web2
NFT GatingToken-based accessWeb3 → Web2
Bridged AssetsCross-chain valueWeb3 ↔ Web3

Authentication Bridge

Sign-In with Solana (SIWS)

TypeScript
import { Connection, PublicKey } from "@solana/web3.js";
import nacl from "tweetnacl";
import bs58 from "bs58";
import jwt from "jsonwebtoken";

interface SIWSMessage {
  domain: string;
  address: string;
  statement: string;
  uri: string;
  version: string;
  chainId: string;
  nonce: string;
  issuedAt: string;
  expirationTime?: string;
  resources?: string[];
}

export class SolanaAuth {
  private jwtSecret: string;
  private domain: string;
  private connection: Connection;
  private nonceStore: Map<string, { nonce: string; expires: Date }> = new Map();

  constructor(jwtSecret: string, domain: string, rpcUrl: string) {
    this.jwtSecret = jwtSecret;
    this.domain = domain;
    this.connection = new Connection(rpcUrl);
  }

  // Generate nonce for sign-in
  generateNonce(address: string): string {
    const nonce = bs58.encode(nacl.randomBytes(32));
    const expires = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes

    this.nonceStore.set(address, { nonce, expires });
    return nonce;
  }

  // Create SIWS message
  createMessage(address: string, nonce: string): string {
    const message: SIWSMessage = {
      domain: this.domain,
      address,
      statement: "Sign in to access your account",
      uri: `https://${this.domain}`,
      version: "1",
      chainId: "mainnet-beta",
      nonce,
      issuedAt: new Date().toISOString(),
      expirationTime: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
    };

    return this.formatMessage(message);
  }

  private formatMessage(msg: SIWSMessage): string {
    return `${msg.domain} wants you to sign in with your Solana account:
${msg.address}

${msg.statement}

URI: ${msg.uri}
Version: ${msg.version}
Chain ID: ${msg.chainId}
Nonce: ${msg.nonce}
Issued At: ${msg.issuedAt}${msg.expirationTime ? `\nExpiration Time: ${msg.expirationTime}` : ""}`;
  }

  // Verify signature and issue JWT
  async verify(
    address: string,
    message: string,
    signature: string,
  ): Promise<{ token: string; user: any } | null> {
    // Check nonce
    const stored = this.nonceStore.get(address);
    if (!stored || new Date() > stored.expires) {
      return null;
    }

    // Verify message contains correct nonce
    if (!message.includes(stored.nonce)) {
      return null;
    }

    // Verify signature
    const publicKey = new PublicKey(address);
    const signatureBytes = bs58.decode(signature);
    const messageBytes = new TextEncoder().encode(message);

    const isValid = nacl.sign.detached.verify(
      messageBytes,
      signatureBytes,
      publicKey.toBytes(),
    );

    if (!isValid) {
      return null;
    }

    // Clear nonce
    this.nonceStore.delete(address);

    // Get or create user
    const user = await this.getOrCreateUser(address);

    // Issue JWT
    const token = jwt.sign(
      {
        sub: address,
        userId: user.id,
        iat: Math.floor(Date.now() / 1000),
      },
      this.jwtSecret,
      { expiresIn: "7d" },
    );

    return { token, user };
  }

  private async getOrCreateUser(address: string): Promise<any> {
    // In practice, query your database
    return {
      id: `user_${address.slice(0, 8)}`,
      address,
      createdAt: new Date(),
    };
  }

  // Verify JWT middleware
  verifyToken(token: string): { sub: string; userId: string } | null {
    try {
      const decoded = jwt.verify(token, this.jwtSecret) as any;
      return { sub: decoded.sub, userId: decoded.userId };
    } catch {
      return null;
    }
  }
}

// Express middleware
export function authMiddleware(auth: SolanaAuth) {
  return (req: any, res: any, next: any) => {
    const authHeader = req.headers.authorization;
    if (!authHeader?.startsWith("Bearer ")) {
      return res.status(401).json({ error: "No token provided" });
    }

    const token = authHeader.slice(7);
    const payload = auth.verifyToken(token);

    if (!payload) {
      return res.status(401).json({ error: "Invalid token" });
    }

    req.user = payload;
    next();
  };
}

Frontend Integration

TypeScript
import { useWallet } from "@solana/wallet-adapter-react";
import { useState } from "react";

export function useSignIn(apiUrl: string) {
  const { publicKey, signMessage } = useWallet();
  const [loading, setLoading] = useState(false);
  const [user, setUser] = useState<any>(null);

  const signIn = async () => {
    if (!publicKey || !signMessage) {
      throw new Error("Wallet not connected");
    }

    setLoading(true);

    try {
      // Get nonce from server
      const nonceRes = await fetch(`${apiUrl}/auth/nonce`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ address: publicKey.toBase58() }),
      });
      const { nonce, message } = await nonceRes.json();

      // Sign message
      const encodedMessage = new TextEncoder().encode(message);
      const signature = await signMessage(encodedMessage);

      // Verify with server
      const verifyRes = await fetch(`${apiUrl}/auth/verify`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          address: publicKey.toBase58(),
          message,
          signature: bs58.encode(signature),
        }),
      });

      const { token, user } = await verifyRes.json();

      // Store token
      localStorage.setItem("auth_token", token);
      setUser(user);

      return { token, user };
    } finally {
      setLoading(false);
    }
  };

  const signOut = () => {
    localStorage.removeItem("auth_token");
    setUser(null);
  };

  return { signIn, signOut, loading, user };
}

Data Synchronization

Off-chain to On-chain (Oracle Pattern)

TypeScript
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
import * as anchor from "@coral-xyz/anchor";

interface PriceData {
  symbol: string;
  price: number;
  timestamp: number;
  source: string;
}

export class PriceOracle {
  private connection: Connection;
  private program: anchor.Program;
  private authority: Keypair;

  constructor(
    connection: Connection,
    program: anchor.Program,
    authority: Keypair,
  ) {
    this.connection = connection;
    this.program = program;
    this.authority = authority;
  }

  // Fetch price from external API
  async fetchPrice(symbol: string): Promise<PriceData> {
    // Aggregate from multiple sources
    const sources = [
      this.fetchFromBinance(symbol),
      this.fetchFromCoinbase(symbol),
      this.fetchFromKraken(symbol),
    ];

    const prices = await Promise.allSettled(sources);
    const validPrices = prices
      .filter(
        (p): p is PromiseFulfilledResult<number> => p.status === "fulfilled",
      )
      .map((p) => p.value);

    if (validPrices.length === 0) {
      throw new Error("No valid prices available");
    }

    // Calculate median price
    validPrices.sort((a, b) => a - b);
    const median = validPrices[Math.floor(validPrices.length / 2)];

    return {
      symbol,
      price: median,
      timestamp: Date.now(),
      source: "aggregated",
    };
  }

  private async fetchFromBinance(symbol: string): Promise<number> {
    const response = await fetch(
      `https://api.binance.com/api/v3/ticker/price?symbol=${symbol}USDT`,
    );
    const data = await response.json();
    return parseFloat(data.price);
  }

  private async fetchFromCoinbase(symbol: string): Promise<number> {
    const response = await fetch(
      `https://api.coinbase.com/v2/prices/${symbol}-USD/spot`,
    );
    const data = await response.json();
    return parseFloat(data.data.amount);
  }

  private async fetchFromKraken(symbol: string): Promise<number> {
    const response = await fetch(
      `https://api.kraken.com/0/public/Ticker?pair=${symbol}USD`,
    );
    const data = await response.json();
    const pair = Object.keys(data.result)[0];
    return parseFloat(data.result[pair].c[0]);
  }

  // Push price to on-chain program
  async updateOnChainPrice(symbol: string): Promise<string> {
    const priceData = await this.fetchPrice(symbol);

    // Scale price for on-chain storage (8 decimals)
    const scaledPrice = Math.round(priceData.price * 1e8);

    const [priceFeed] = PublicKey.findProgramAddressSync(
      [Buffer.from("price"), Buffer.from(symbol)],
      this.program.programId,
    );

    const tx = await this.program.methods
      .updatePrice(
        new anchor.BN(scaledPrice),
        new anchor.BN(priceData.timestamp),
      )
      .accounts({
        priceFeed,
        authority: this.authority.publicKey,
      })
      .signers([this.authority])
      .rpc();

    return tx;
  }

  // Start periodic updates
  startUpdates(symbols: string[], intervalMs: number): () => void {
    const intervals = symbols.map((symbol) =>
      setInterval(async () => {
        try {
          const tx = await this.updateOnChainPrice(symbol);
          console.log(`Updated ${symbol}: ${tx}`);
        } catch (error) {
          console.error(`Failed to update ${symbol}:`, error);
        }
      }, intervalMs),
    );

    return () => intervals.forEach(clearInterval);
  }
}

On-chain to Off-chain (Webhook Pattern)

TypeScript
import express from "express";
import { Pool } from "pg";

interface BlockchainEvent {
  signature: string;
  slot: number;
  timestamp: number;
  programId: string;
  type: string;
  data: any;
}

export class EventBridge {
  private app: express.Application;
  private db: Pool;
  private handlers: Map<string, (event: BlockchainEvent) => Promise<void>>;

  constructor(databaseUrl: string) {
    this.app = express();
    this.app.use(express.json());
    this.db = new Pool({ connectionString: databaseUrl });
    this.handlers = new Map();
    this.setupRoutes();
  }

  private setupRoutes() {
    // Webhook endpoint for blockchain events
    this.app.post("/webhook/events", async (req, res) => {
      const events: BlockchainEvent[] = req.body;

      for (const event of events) {
        try {
          // Store event
          await this.storeEvent(event);

          // Process with registered handler
          const handler = this.handlers.get(event.type);
          if (handler) {
            await handler(event);
          }
        } catch (error) {
          console.error("Event processing error:", error);
        }
      }

      res.json({ processed: events.length });
    });
  }

  private async storeEvent(event: BlockchainEvent): Promise<void> {
    await this.db.query(
      `INSERT INTO blockchain_events (signature, slot, timestamp, program_id, event_type, data)
       VALUES ($1, $2, $3, $4, $5, $6)
       ON CONFLICT (signature, event_type) DO NOTHING`,
      [
        event.signature,
        event.slot,
        new Date(event.timestamp * 1000),
        event.programId,
        event.type,
        JSON.stringify(event.data),
      ],
    );
  }

  // Register event handler
  on(
    eventType: string,
    handler: (event: BlockchainEvent) => Promise<void>,
  ): void {
    this.handlers.set(eventType, handler);
  }

  start(port: number): void {
    this.app.listen(port, () => {
      console.log(`Event bridge listening on port ${port}`);
    });
  }
}

// Example handlers
const bridge = new EventBridge(process.env.DATABASE_URL!);

// Handle NFT purchases
bridge.on("NFT_SALE", async (event) => {
  const { mint, buyer, seller, price } = event.data;

  // Update ownership in database
  await db.query(`UPDATE nft_ownership SET owner = $1 WHERE mint = $2`, [
    buyer,
    mint,
  ]);

  // Send notification
  await sendEmail(seller, "Your NFT was sold!", {
    mint,
    price: price / 1e9,
    buyer,
  });
});

// Handle token transfers
bridge.on("TOKEN_TRANSFER", async (event) => {
  const { mint, from, to, amount } = event.data;

  // Update balances
  await db.query(
    `UPDATE user_balances 
     SET balance = balance - $1 
     WHERE address = $2 AND mint = $3`,
    [amount, from, mint],
  );

  await db.query(
    `INSERT INTO user_balances (address, mint, balance)
     VALUES ($1, $2, $3)
     ON CONFLICT (address, mint) 
     DO UPDATE SET balance = user_balances.balance + $3`,
    [to, mint, amount],
  );
});

Payment Processing

TypeScript
import {
  Connection,
  PublicKey,
  Transaction,
  SystemProgram,
  LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import * as token from "@solana/spl-token";

interface PaymentRequest {
  id: string;
  amount: number;
  currency: "SOL" | "USDC";
  recipient: string;
  memo?: string;
  expiresAt: Date;
  status: "pending" | "completed" | "expired" | "failed";
}

export class PaymentGateway {
  private connection: Connection;
  private treasury: PublicKey;
  private usdcMint: PublicKey;
  private payments: Map<string, PaymentRequest> = new Map();

  constructor(
    connection: Connection,
    treasury: PublicKey,
    usdcMint: PublicKey,
  ) {
    this.connection = connection;
    this.treasury = treasury;
    this.usdcMint = usdcMint;
  }

  // Create payment request
  createPaymentRequest(
    amount: number,
    currency: "SOL" | "USDC",
    options?: { memo?: string; expiresIn?: number },
  ): PaymentRequest {
    const id = crypto.randomUUID();
    const expiresAt = new Date(
      Date.now() + (options?.expiresIn ?? 30 * 60 * 1000),
    );

    const payment: PaymentRequest = {
      id,
      amount,
      currency,
      recipient: this.treasury.toBase58(),
      memo: options?.memo,
      expiresAt,
      status: "pending",
    };

    this.payments.set(id, payment);
    return payment;
  }

  // Generate payment transaction
  async createPaymentTransaction(
    paymentId: string,
    payer: PublicKey,
  ): Promise<Transaction> {
    const payment = this.payments.get(paymentId);
    if (!payment) {
      throw new Error("Payment not found");
    }

    if (payment.status !== "pending") {
      throw new Error(`Payment is ${payment.status}`);
    }

    if (new Date() > payment.expiresAt) {
      payment.status = "expired";
      throw new Error("Payment expired");
    }

    const transaction = new Transaction();

    if (payment.currency === "SOL") {
      transaction.add(
        SystemProgram.transfer({
          fromPubkey: payer,
          toPubkey: this.treasury,
          lamports: payment.amount * LAMPORTS_PER_SOL,
        }),
      );
    } else {
      // USDC transfer
      const payerAta = await token.getAssociatedTokenAddress(
        this.usdcMint,
        payer,
      );
      const treasuryAta = await token.getAssociatedTokenAddress(
        this.usdcMint,
        this.treasury,
      );

      transaction.add(
        token.createTransferInstruction(
          payerAta,
          treasuryAta,
          payer,
          payment.amount * 1e6, // USDC has 6 decimals
        ),
      );
    }

    // Add memo if provided
    if (payment.memo) {
      const memoProgram = new PublicKey(
        "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr",
      );
      transaction.add({
        keys: [],
        programId: memoProgram,
        data: Buffer.from(payment.memo),
      });
    }

    const { blockhash } = await this.connection.getLatestBlockhash();
    transaction.recentBlockhash = blockhash;
    transaction.feePayer = payer;

    return transaction;
  }

  // Verify payment
  async verifyPayment(paymentId: string, signature: string): Promise<boolean> {
    const payment = this.payments.get(paymentId);
    if (!payment || payment.status !== "pending") {
      return false;
    }

    try {
      const tx = await this.connection.getParsedTransaction(signature, {
        commitment: "confirmed",
      });

      if (!tx || tx.meta?.err) {
        return false;
      }

      // Verify amount and recipient
      let verified = false;

      if (payment.currency === "SOL") {
        // Check SOL transfer
        const preBalance = tx.meta?.preBalances?.[1] ?? 0;
        const postBalance = tx.meta?.postBalances?.[1] ?? 0;
        const transferred = postBalance - preBalance;

        verified = transferred >= payment.amount * LAMPORTS_PER_SOL;
      } else {
        // Check USDC transfer
        const tokenBalances = tx.meta?.postTokenBalances?.find(
          (b) =>
            b.mint === this.usdcMint.toBase58() &&
            b.owner === this.treasury.toBase58(),
        );

        if (tokenBalances) {
          // Verify amount matches
          verified = true;
        }
      }

      if (verified) {
        payment.status = "completed";
        return true;
      }

      return false;
    } catch (error) {
      console.error("Payment verification error:", error);
      return false;
    }
  }

  // Get payment status
  getPayment(paymentId: string): PaymentRequest | undefined {
    return this.payments.get(paymentId);
  }
}

Best Practices

AreaPractice
AuthenticationNever trust client-side wallet ownership claims
Data syncUse idempotent operations for blockchain events
PaymentsAlways verify on-chain before fulfilling orders
API designProvide both polling and webhook options
Error handlingImplement retry logic for blockchain operations

Next: API Design - Building robust APIs for blockchain applications.