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
| Pattern | Use Case | Direction |
|---|---|---|
| Sign-In with Solana | Web2 auth via wallet | Web3 → Web2 |
| Oracle | Off-chain data on-chain | Web2 → Web3 |
| Webhook | On-chain events to servers | Web3 → Web2 |
| Payment Gateway | Accept crypto payments | Web3 → Web2 |
| NFT Gating | Token-based access | Web3 → Web2 |
| Bridged Assets | Cross-chain value | Web3 ↔ 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
| Area | Practice |
|---|---|
| Authentication | Never trust client-side wallet ownership claims |
| Data sync | Use idempotent operations for blockchain events |
| Payments | Always verify on-chain before fulfilling orders |
| API design | Provide both polling and webhook options |
| Error handling | Implement retry logic for blockchain operations |
Next: API Design - Building robust APIs for blockchain applications.