Twin Documentation
  • Overview
  • Consumer
  • Developer
  • Operations
Arquitectura
    Backend ArchitectureDashboard ArchitecturePlatform ArchitectureBridge Contracts Architecture
Flujos Técnicos
Base de Datos
Local SetupConventions
powered by Zudoku
Arquitectura

Backend Architecture

NestJS monorepo running on Fastify. Exposes two APIs (dashboard and platform), a cron service, and shared packages for database access, business logic, and utilities.

Project Structure

Code
twin-backend/ apps/ cron-service/ # Scheduled jobs (12 cron jobs) packages/ database/ # Objection.js models, 33+ Knex migrations, seeds dashboard/ # Fastify routes for the admin dashboard API platform/ # Fastify routes for the user-facing platform API shared/ # 35+ service classes, BitGo client, config utils/ # Pure utility functions service/ # Service layer consumed by frontends

Dependency Graph

All packages compile to dist/. Consumers import from the compiled output, not source.

Build Pipeline

Each package requires two build steps:

  1. npm run build -- tsup generates .js/.cjs + barrel .d.ts (re-exports only)
  2. npm run build:types -- tsc -b . generates individual .d.ts files per service/model

Build in dependency order:

TerminalCode
cd twin-backend npm run build -w packages/utils npm run build:types -w packages/utils npm run build -w packages/database npm run build:types -w packages/database npm run build -w packages/shared npm run build:types -w packages/shared npm run build -w packages/dashboard

{% hint style="warning" %} Skipping build:types leaves stale type declarations in dist/. Symptom: Property X does not exist on type Y in consumers that reference changed signatures. {% endhint %}

Controller Pattern

Routes are registered as Fastify plugins. Each route file exports a function that receives the Fastify instance and registers handlers.

Static routes must be registered before parameterized routes. Fastify matches in registration order -- if /:id is registered before /history, the string "history" is captured as a param.

Code
// Correct order app.get('/resource/history', historyHandler); app.get('/resource/active', activeHandler); app.get('/resource/:id', getByIdHandler);

Service Pattern

Services are classes with static methods that use Objection.js models for database access. No dependency injection -- services are imported directly.

Code
export class MintingService { static async getTransactions(filters: TransactionFilters) { return MintingTransaction.query() .where(filters) .orderBy('created_at', 'desc'); } }

Cron Jobs

The cron service runs 12 scheduled jobs:

KeyIntervalDescription
CHECK_JOBS1 minHealth check -- ensures other jobs are running
OPS_BALANCE_UPDATE5 minUpdates operational wallet balances from on-chain
CONTRACT_DATA_COLLECTION15 minCollects contract state (total supply, etc.)
WALLET_BALANCE_CHECKER10 minChecks wallet balances and flags anomalies
DAILY_BALANCE_SNAPSHOTDailyRecords daily balance snapshots for reporting
WEEKLY_REWARD_PROCESSORWedProcesses staking reward distributions
PAYMENT_RETRY15 minRetries failed payment transactions
ORACLE_PRICE_UPDATE5 minPushes token prices to on-chain oracle contract
MINTING_TX_SYNC2 minSyncs minting transaction states with BitGo
BRIDGE_TX_SYNC2 minSyncs bridge transaction states with LayerZero
UNISWAP_AUTO_REBALANCE5 minAuto-rebalances Uniswap V4 positions when price deviates
CIRCULATION_SNAPSHOTDailyRecords circulating supply snapshots per token

Registering a new cron job requires changes in 3 places:

  1. cron-service/src/jobs/cron-keys.ts -- enum key
  2. cron-service/src/jobs/manager.ts -- import + jobHandlers map
  3. dashboard/src/cron-handlers.ts -- handler for on-demand execution from UI

BitGo Integration

Two patterns for sending transactions through BitGo Express:

Pattern 1: Contract Call

Used for oracle updates, Uniswap liquidity operations, and ERC20 approvals. Sends a zero-value transaction with encoded calldata.

Code
await bitgoExpressClient.sendContractCall({ coin: 'baseeth', // Base coin (no token suffix) walletId: wallet.bitgoWalletId, amount: '0', data: encodedCalldata, // ABI-encoded function call passphrase: decryptedPass, });

Pattern 2: Token Transfer

Used for minting (sending tokens to recipients). Uses the full coin identifier including token suffix.

Code
await bitgoExpressClient.sendMany({ coin: 'baseeth:argt', // Full coin with token suffix walletId: wallet.bitgoWalletId, recipients: [{ amount: amountInBaseUnits, address: recipientAddress, tokenName: 'argt', // Required inside each recipient }], passphrase: decryptedPass, });

{% hint style="info" %} GET endpoints on BitGo use the base coin (e.g., baseeth, polygon) without the token suffix. See getBaseCoin() in bitgo-express.client.ts. {% endhint %}

Last modified on May 12, 2026
ArquitecturaDashboard Architecture
On this page
  • Project Structure
  • Dependency Graph
  • Build Pipeline
  • Controller Pattern
  • Service Pattern
  • Cron Jobs
  • BitGo Integration
    • Pattern 1: Contract Call
    • Pattern 2: Token Transfer
TypeScript
TypeScript
TypeScript
TypeScript