Arquitectura de Base de Datos
Este documento explica la arquitectura de datos de SwapBits, centrada en MongoDB.
Vista General de Datos
1. Estructura de Base de Datos
Bases de Datos por Dominio
// Organización de databases
const Databases = {
// Autenticación y usuarios
'swapbits-auth': {
collections: ['users', 'sessions', 'refresh-tokens', 'password-resets']
},
// Wallets y transacciones
'swapbits-wallets': {
collections: ['wallets', 'transactions', 'addresses']
},
// Trading y exchange
'swapbits-exchange': {
collections: ['swaps', 'orders', 'trades', 'price-history']
},
// Banking
'swapbits-banking': {
collections: ['bank-accounts', 'deposits', 'withdrawals', 'conversions']
},
// KYC
'swapbits-kyc': {
collections: ['kyc-submissions', 'kyc-documents', 'kyc-verifications']
},
// Admin
'swapbits-admin': {
collections: ['admin-users', 'chat-conversations', 'chat-messages', 'analytics-events']
},
// Sistema
'swapbits-system': {
collections: ['audit-logs', 'error-logs', 'notifications', 'webhooks']
}
};
2. Schemas Principales
User Schema
// users collection
{
_id: ObjectId("..."),
// Identidad
email: "user@example.com",
emailVerified: true,
username: "johndoe",
// Seguridad
password: "$2b$10$...", // bcrypt hash
pin: "$2b$10$...", // PIN hash para transacciones
twoFactorEnabled: false,
twoFactorSecret?: string,
// Perfil
firstName: "John",
lastName: "Doe",
dateOfBirth: ISODate("1990-01-15"),
phone?: "+1234567890",
phoneVerified: false,
// KYC
kycStatus: "approved", // not_started, pending, approved, rejected
kycLevel: "advanced", // basic, advanced, premium
kycSubmittedAt?: ISODate("..."),
kycApprovedAt?: ISODate("..."),
// Estado
status: "active", // active, suspended, banned, deleted
role: "USER", // USER, ADMIN, SUPER_ADMIN
// Seguridad
failedLoginAttempts: 0,
lockedUntil?: ISODate("..."),
lastLoginAt?: ISODate("..."),
lastLoginIp?: "192.168.1.1",
// Preferencias
preferences: {
language: "en",
currency: "USD",
notifications: {
email: true,
push: true,
sms: false
},
autoConvert: {
enabled: false,
targetCrypto: "BTC"
}
},
// Trading
vipLevel: 0, // 0, 1, 2, 3
tradingVolume30d: 0,
// Auditoría
createdAt: ISODate("..."),
updatedAt: ISODate("..."),
deletedAt?: ISODate("...")
}
Índices:
db.users.createIndex({ email: 1 }, { unique: true });
db.users.createIndex({ username: 1 }, { unique: true });
db.users.createIndex({ status: 1, kycStatus: 1 });
db.users.createIndex({ createdAt: -1 });
Wallet Schema
// wallets collection
{
_id: ObjectId("..."),
userId: ObjectId("..."),
// Identificación
address: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
coin: "ETH",
network: "ethereum",
type: "EVM", // EVM, BTC, SOL, TRX, TON, etc.
// Claves (ENCRIPTADAS)
privateKeyEncrypted: "U2FsdGVkX1...", // AES-256
publicKey: "0x04a8b...",
mnemonic?: "U2FsdGVkX1...", // Solo si fue generada con mnemonic
// Balance
balance: 1.5,
balanceUSD: 3000.00,
lockedBalance: 0.1, // En transacciones pendientes
// Metadata
label?: "Mi wallet principal",
isActive: true,
isImported: false, // true si fue importada
isPrimary: false, // Wallet principal del usuario
// Monitoreo
lastBalanceUpdate: ISODate("..."),
lastTransactionAt?: ISODate("..."),
// Auditoría
createdAt: ISODate("..."),
updatedAt: ISODate("..."),
deletedAt?: ISODate("...")
}
Índices:
db.wallets.createIndex({ userId: 1, coin: 1 });
db.wallets.createIndex({ address: 1 }, { unique: true });
db.wallets.createIndex({ userId: 1, isActive: 1 });
db.wallets.createIndex({ type: 1, network: 1 });
Transaction Schema
// transactions collection
{
_id: ObjectId("..."),
// Relaciones
userId: ObjectId("..."),
walletId: ObjectId("..."),
// Blockchain
txHash: "0x123abc...",
blockNumber: 18234567,
blockHash: "0xdef...",
// Tipo y dirección
type: "received", // sent, received
from: "0xabc...",
to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
// Montos
amount: 0.5,
coin: "ETH",
network: "ethereum",
// Fee
gasFee: 0.00021,
gasFeeUSD: 0.42,
gasPrice: 20, // gwei
gasUsed: 21000,
// Valor en fiat
usdValue: 1000.00, // Valor USD al momento de la tx
pricePerCoin: 2000.00,
// Estado
status: "confirmed", // pending, confirmed, failed
confirmations: 12,
// Metadata
memo?: "Payment for services",
tags?: ["business", "income"],
// Timestamps
timestamp: ISODate("..."), // Cuando se creó en blockchain
detectedAt: ISODate("..."), // Cuando nuestro monitor la detectó
confirmedAt?: ISODate("..."),
// Auditoría
createdAt: ISODate("..."),
updatedAt: ISODate("...")
}
Índices:
db.transactions.createIndex({ userId: 1, timestamp: -1 });
db.transactions.createIndex({ walletId: 1, status: 1 });
db.transactions.createIndex({ txHash: 1 }, { unique: true });
db.transactions.createIndex({ type: 1, coin: 1 });
db.transactions.createIndex({ status: 1, createdAt: -1 });
Swap Schema
// swaps collection (Exchange Service)
{
_id: ObjectId("..."),
userId: ObjectId("..."),
// Quote
quoteId: "quote_abc123",
quoteExpiresAt: ISODate("..."),
// Monedas
fromCoin: "ETH",
toCoin: "BTC",
// Montos
fromAmount: 1.0,
toAmount: 0.05234,
rate: 0.05234,
marketRate: 0.05250,
// Fees
fee: 0.003, // 0.3%
feeAmount: 0.00003,
feeUSD: 0.06,
// Wallets
fromWalletId: ObjectId("..."),
toWalletId: ObjectId("..."),
// Transacciones
depositTxHash?: "0x123...",
depositConfirmations: 12,
withdrawalTxHash?: "0xdef...",
withdrawalConfirmations: 6,
// Estado
status: "completed", // pending, awaiting_deposit, deposit_confirmed,
// processing, completed, failed, refunded
// Timestamps
createdAt: ISODate("..."),
depositConfirmedAt?: ISODate("..."),
completedAt?: ISODate("..."),
// Auditoría
ip: "192.168.1.1",
userAgent: "Mozilla/5.0..."
}
Índices:
db.swaps.createIndex({ userId: 1, createdAt: -1 });
db.swaps.createIndex({ status: 1 });
db.swaps.createIndex({ quoteId: 1 });
Bank Account Schema
// bank-accounts collection
{
_id: ObjectId("..."),
userId: ObjectId("..."),
// Datos bancarios
bankName: "Chase Bank",
accountType: "checking", // checking, savings
accountNumberLast4: "1234", // Solo últimos 4 dígitos
routingNumber: "021000021",
accountHolderName: "John Doe",
// Plaid (proveedor de verificación)
plaidAccountId: "plaid_acc_123",
plaidAccessTokenEncrypted: "U2FsdGVkX1...",
plaidItemId: "plaid_item_456",
// Verificación
isVerified: true,
verifiedAt: ISODate("..."),
verificationMethod: "micro_deposits", // micro_deposits, instant
// Estado
isActive: true,
isPrimary: true,
// Auditoría
createdAt: ISODate("..."),
lastUsedAt: ISODate("..."),
updatedAt: ISODate("...")
}
Deposit/Withdrawal Schema
// deposits collection
{
_id: ObjectId("..."),
userId: ObjectId("..."),
type: "deposit", // deposit o withdrawal
method: "bank_transfer", // bank_transfer, wire, debit_card, credit_card
// Montos
fiatAmount: 500.00,
fiatCurrency: "USD",
cryptoAmount: 500.00,
cryptoCurrency: "USDT",
exchangeRate: 1.0,
// Fee
providerFee: 2.50,
swapbitsFee: 0,
totalFee: 2.50,
netAmount: 497.50,
// Tracking
reference: "SB-USER-123456", // Referencia única
externalTxId: "TRX-ABC123", // ID del proveedor de pago
// Bank account (solo para bank_transfer)
bankAccountId?: ObjectId("..."),
// Estado
status: "completed", // pending, awaiting_funds, received, processing,
// completed, failed, refunded, pending_review
// Compliance
kycLevel: "advanced",
amlChecked: true,
amlRiskScore: 95, // 0-100 (100 = sin riesgo)
requiresManualReview: false,
reviewedBy?: ObjectId("admin_id"),
reviewedAt?: ISODate("..."),
// Timestamps
createdAt: ISODate("..."),
receivedAt?: ISODate("..."),
confirmedAt?: ISODate("..."),
completedAt?: ISODate("..."),
// Auditoría
ip: "192.168.1.1",
userAgent: "Mozilla/5.0..."
}
Índices:
db.deposits.createIndex({ userId: 1, createdAt: -1 });
db.deposits.createIndex({ status: 1 });
db.deposits.createIndex({ reference: 1 }, { unique: true });
db.deposits.createIndex({ type: 1, createdAt: -1 });
3. Estrategias de Indexación
Índices Compuestos
// Queries comunes optimizados
// 1. Buscar transacciones de un usuario por fecha
db.transactions.createIndex({ userId: 1, timestamp: -1 });
// Query optimizado:
db.transactions.find({
userId: ObjectId("user_123")
}).sort({ timestamp: -1 });
// 2. Buscar wallets activas de un usuario
db.wallets.createIndex({ userId: 1, isActive: 1, coin: 1 });
// Query optimizado:
db.wallets.find({
userId: ObjectId("user_123"),
isActive: true
});
// 3. Buscar swaps por estado y fecha
db.swaps.createIndex({ status: 1, createdAt: -1 });
// Query optimizado:
db.swaps.find({
status: "pending"
}).sort({ createdAt: -1 });
Índices de Texto (Full-Text Search)
// Buscar usuarios por nombre o email
db.users.createIndex({
email: "text",
firstName: "text",
lastName: "text",
username: "text"
});
// Query de búsqueda:
db.users.find({
$text: { $search: "john doe" }
});
// Buscar transacciones por hash o address
db.transactions.createIndex({
txHash: "text",
from: "text",
to: "text"
});
Índices TTL (Time To Live)
// Auto-eliminar sesiones expiradas
db.sessions.createIndex(
{ expiresAt: 1 },
{ expireAfterSeconds: 0 }
);
// Auto-eliminar tokens de reset de password después de 1 hora
db.passwordResets.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 }
);
// Auto-eliminar notificaciones viejas (30 días)
db.notifications.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 2592000 } // 30 días
);
4. Particionamiento (Sharding)
Estrategia de Sharding
Configuración:
// Shard key por userId (hashed)
sh.shardCollection("swapbits-wallets.wallets", { userId: "hashed" });
sh.shardCollection("swapbits-wallets.transactions", { userId: "hashed" });
// Shard key por createdAt (ranged) para datos históricos
sh.shardCollection("swapbits-system.audit-logs", { createdAt: 1 });
// Compound shard key
sh.shardCollection("swapbits-exchange.swaps", {
userId: "hashed",
createdAt: 1
});
5. Agregaciones Comunes
Balance Total de Usuarios
// Calcular balance total en USD de todos los usuarios
db.wallets.aggregate([
{ $match: { isActive: true } },
{ $group: {
_id: "$userId",
totalBalanceUSD: { $sum: "$balanceUSD" }
}},
{ $sort: { totalBalanceUSD: -1 } },
{ $limit: 100 }
]);
Volumen de Trading por Usuario
// Volumen de trading últimos 30 días
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
db.swaps.aggregate([
{ $match: {
createdAt: { $gte: thirtyDaysAgo },
status: "completed"
}},
{ $group: {
_id: "$userId",
totalVolume: { $sum: { $multiply: ["$fromAmount", "$marketRate"] } },
swapCount: { $sum: 1 }
}},
{ $sort: { totalVolume: -1 } }
]);
Transacciones por Día
// Número de transacciones por día (últimos 30 días)
db.transactions.aggregate([
{ $match: {
timestamp: { $gte: thirtyDaysAgo }
}},
{ $group: {
_id: {
year: { $year: "$timestamp" },
month: { $month: "$timestamp" },
day: { $dayOfMonth: "$timestamp" }
},
count: { $sum: 1 },
volume: { $sum: "$usdValue" }
}},
{ $sort: { "_id.year": 1, "_id.month": 1, "_id.day": 1 } }
]);
6. Optimización de Queries
Query Performance
// MAL: Query lento sin índice
db.transactions.find({
amount: { $gt: 100 },
status: "confirmed"
}).sort({ timestamp: -1 });
// BIEN: Con índice compuesto
db.transactions.createIndex({
status: 1,
amount: 1,
timestamp: -1
});
db.transactions.find({
status: "confirmed",
amount: { $gt: 100 }
}).sort({ timestamp: -1 });
Projection (Reducir Datos Retornados)
// MAL: Trae todos los campos
db.users.find({ _id: userId });
// BIEN: Solo trae campos necesarios
db.users.find(
{ _id: userId },
{ email: 1, firstName: 1, lastName: 1, kycStatus: 1 }
);
Límites y Paginación
// Paginación eficiente con skip/limit
const page = 1;
const limit = 20;
const skip = (page - 1) * limit;
db.transactions.find({ userId })
.sort({ timestamp: -1 })
.skip(skip)
.limit(limit);
// MEJOR: Paginación por cursor (más eficiente)
const lastTimestamp = lastTransaction.timestamp;
db.transactions.find({
userId,
timestamp: { $lt: lastTimestamp }
})
.sort({ timestamp: -1 })
.limit(20);
7. Transacciones ACID
Transacciones Multi-Documento
import { ClientSession } from 'mongodb';
async function executeSwap(
userId: ObjectId,
fromWalletId: ObjectId,
toWalletId: ObjectId,
fromAmount: number,
toAmount: number
) {
const session: ClientSession = mongoClient.startSession();
try {
await session.withTransaction(async () => {
// 1. Reducir balance de wallet origen
await walletsCollection.updateOne(
{ _id: fromWalletId },
{ $inc: { balance: -fromAmount } },
{ session }
);
// 2. Incrementar balance de wallet destino
await walletsCollection.updateOne(
{ _id: toWalletId },
{ $inc: { balance: toAmount } },
{ session }
);
// 3. Crear registro de swap
await swapsCollection.insertOne(
{
userId,
fromWalletId,
toWalletId,
fromAmount,
toAmount,
status: 'completed',
createdAt: new Date()
},
{ session }
);
// Si cualquier operación falla, todo se revierte
});
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
await session.endSession();
}
}
8. Backup y Recuperación
Estrategia de Backup
# Backups automáticos diarios
Backup Schedule:
Full Backup:
Frequency: Daily at 2:00 AM UTC
Retention: 30 days
Storage: AWS S3
Incremental Backup:
Frequency: Every 6 hours
Retention: 7 days
Storage: AWS S3
Point-in-Time Recovery:
Enabled: true
Window: Last 7 days
Comando de backup:
# Backup de base de datos específica
mongodump \
--uri="mongodb://username:password@host:27017/swapbits-wallets" \
--out=/backup/$(date +%Y%m%d)
# Comprimir y subir a S3
tar -czf backup-$(date +%Y%m%d).tar.gz /backup/$(date +%Y%m%d)
aws s3 cp backup-$(date +%Y%m%d).tar.gz s3://swapbits-backups/mongodb/
Restauración
# Restaurar desde backup
mongorestore \
--uri="mongodb://username:password@host:27017" \
--archive=backup-20251020.tar.gz \
--gzip
9. Monitoreo de Base de Datos
Métricas Clave
interface DatabaseMetrics {
// Performance
avgQueryTime: number; // 45ms
slowQueries: number; // Queries > 100ms
queriesPerSecond: number; // 500 QPS
// Tamaño
totalDatabaseSize: string; // "50 GB"
totalCollections: number; // 25
totalDocuments: number; // 10,000,000
// Índices
totalIndexSize: string; // "5 GB"
indexHitRate: number; // 98% (bueno)
// Conexiones
activeConnections: number; // 50
availableConnections: number; // 950
// Replicación
replicationLag: number; // 100ms
replicaSetStatus: string; // "healthy"
// Operaciones
insertsPerSecond: number; // 100
updatesPerSecond: number; // 200
deletesPerSecond: number; // 10
}
Queries Lentas
// Habilitar profiling para capturar queries lentas
db.setProfilingLevel(1, { slowms: 100 });
// Ver queries lentas
db.system.profile.find({ millis: { $gt: 100 } }).sort({ ts: -1 }).limit(10);
10. Redis como Cache
Estrategia de Cache
// Cache de balance de wallet (TTL: 30s)
async function getWalletBalance(walletId: string): Promise<number> {
const cacheKey = `wallet:balance:${walletId}`;
// 1. Intentar obtener de cache
const cached = await redisClient.get(cacheKey);
if (cached) {
return parseFloat(cached);
}
// 2. Si no está en cache, obtener de MongoDB
const wallet = await walletsCollection.findOne({ _id: new ObjectId(walletId) });
// 3. Guardar en cache con TTL
await redisClient.setex(cacheKey, 30, wallet.balance.toString());
return wallet.balance;
}
// Invalidar cache cuando balance cambia
async function updateWalletBalance(walletId: string, newBalance: number) {
// 1. Actualizar en MongoDB
await walletsCollection.updateOne(
{ _id: new ObjectId(walletId) },
{ $set: { balance: newBalance } }
);
// 2. Invalidar cache
await redisClient.del(`wallet:balance:${walletId}`);
}
Datos en Redis
// Tipos de datos en Redis
const RedisCacheKeys = {
// Balances (TTL: 30s)
WALLET_BALANCE: (walletId) => `wallet:balance:${walletId}`,
// Precios (TTL: 10s)
PRICE: (coin) => `price:${coin}`,
// Cotizaciones swap (TTL: 30s)
SWAP_QUOTE: (quoteId) => `swap:quote:${quoteId}`,
// Rate limiting
RATE_LIMIT: (userId, action) => `rl:${userId}:${action}`,
// Sesiones (TTL: 7 días)
SESSION: (sessionId) => `session:${sessionId}`,
// Usuarios online
ONLINE_USERS: 'online:users', // Set
USER_SOCKET: (userId) => `user:socket:${userId}`
};
Best Practices de Base de Datos
Hacer:
- ✅ Usar índices para queries frecuentes
- ✅ Implementar paginación en listas largas
- ✅ Usar projection para reducir datos transferidos
- ✅ Implementar TTL para datos temporales
- ✅ Hacer backups diarios automatizados
- ✅ Monitorear queries lentas (> 100ms)
- ✅ Usar transacciones para operaciones críticas
- ✅ Cache datos que no cambian frecuentemente
NO hacer:
- ❌ Queries sin límite (sin .limit())
- ❌ Guardar passwords sin hash
- ❌ Guardar claves privadas sin encriptar
- ❌ Usar .find() sin índices
- ❌ Hacer joins complejos (MongoDB no es relacional)
- ❌ Guardar archivos grandes en MongoDB (usar S3)
- ❌ Ignorar warnings de queries lentas
Seguridad:
- 🔒 Encriptar datos sensibles (claves privadas, tokens)
- 🔒 Usar SSL/TLS para conexiones
- 🔒 Limitar acceso por IP
- 🔒 Usar roles y permisos en MongoDB
- 🔒 Auditar accesos a datos sensibles