X-Change Guide

Architecture overview

Three pieces, two languages, one idea. This page connects everything before you read the deep-dives.

The big picture

Next.js frontend
React 19 · Tailwind
keeps keys · signs · shows data
JSON over HTTPS
Rust node
Axum server on :8080
API · block producer · matching
SQL
PostgreSQL
the source of truth
12 tables · migrations
Frontend (apps/web)Backend (apps/node)Database

The frontend never talks to the database. It talks only to the Rust node's HTTP API. The node talks to Postgres. The interesting part: the blockchain itself is stored in Postgres — blocks and transactions are just rows, and the in-memory order book is rebuilt from the orders table at boot.

What happens when the node starts

1 · Load config
Config::from_env()
PORT, DATABASE_URL, JWT_SECRET, chain params…
dotenv
2 · Connect to Postgres
PgPool (max 10 conns)
sqlx
3 · Run migrations
sqlx::migrate!("./src/db/migrations")
creates all 12 tables
4 · Bootstrap genesis block
if the chain is empty
block 0 + seeds faucet balance
5 · Rebuild order books
reload open orders from DB
6 · Start block producer
tokio::spawn — a 3s timer
produce_block runs forever in the background
7 · Serve HTTP
axum on 0.0.0.0:8080
all /auth /tx /chain /trade /staking… routes

So the same process is both the API server and the block producer. They share the same AppState. That is the single most important architectural decision to be able to explain.

apps/node/src/state.rs — the shared state
1#[derive(Clone)]
2pub struct AppState {
3 pub db: PgPool, // every handler can query
4 pub cfg: Config, // chain params from env
5 pub node_address: String, // this node's identity
6 pub orderbooks: Arc<Mutex<HashMap<String, OrderBook>>>, // live order books
7 pub rate: Arc<RwLock<HashMap<String, (u32, i64)>>>, // rate limiter
8 pub faucet: Option<(String, Vec<u8>)>, // faucet key for minting
9}

Every HTTP handler receives a clone of AppState (cheap — it's mostly Arcs), so the handler can query the DB and read the order books directly.

Inside the Rust node

Think of the node as four layers:

Routes + handlers + middleware. Each route is a small function that extracts JSON, does a thing, returns JSON. Middleware wraps everything: CORS, rate limiting, JWT auth.

2 · Chain layer Backend blockchain

The "blockchain" — but as pure functions: Block, compute_hash, validate_block, apply_block, plus consensus (select_proposer) and the mempool.

Balances (per address × asset), staking (validators, positions, epoch rewards) and the order book. Money is always integer minor units — no floats.

4 · Storage layer Database database

One db.rs with every SQL query and row struct. The only place SQL lives.

Module map of the backend

apps/node/src — where each concern lives (post-refactor)
1src/
2├── main.rs # boot sequence (startup steps 1-7)
3├── bin/seed.rs # demo data seeder
4├── config.rs # every env var -> Config
5├── state.rs # AppState (shared by all handlers)
6├── db.rs # row structs + ALL SQL queries
7├── wallet.rs # ed25519 keygen, sign, verify, derive_address
8├── exchange.rs # OrderBook, Side, match_order, depth
9├── block_producer.rs # genesis bootstrap + 3s produce_block loop
10├── chain/
11 ├── blockchain.rs # Block, compute_hash, merkle, validate/apply
12 ├── consensus.rs # select_proposer, epoch math
13 ├── mempool.rs # submit/pending/confirm (stored in Postgres)
14 └── transaction.rs # Transaction, message_to_sign, verify
15├── ledger/
16 ├── balances.rs # get/upsert balance, load/persist state view
17 └── staking.rs # stake/unstake/process_epoch, Validator
18└── api/
19 ├── routes.rs # build_router() every route registered here
20 ├── middleware.rs # AppError, JWT, require_auth, CORS, rate_limit
21 └── handlers/ # one file per resource (auth, tx, chain, …)

The two money flows

There are two different ways value moves. Being able to distinguish them is a strong defense point:

On-chain transfers

Wallet → wallet, faucet, staking escrow. These are signed transactions, go through the mempool, and are packed into blocks. They live in the transactions and blocks tables. Backend see transactions

Off-chain exchange

Trades on the order book. Order matching happens in memory, settlement is a direct balances update. No blocks involved — trade records live in orders and trades. Backend see exchange

Security model in one paragraph

The browser owns the keys and signs every mutating action. The node never sees a secret key. JWTs (issued after proving wallet ownership via a signed challenge) gate the exchange/staking/multisig endpoints. Nonces stop replay attacks. Amounts are integers (no float rounding). The CORS allowlist and rate limiter protect the API. Postgres transactions keep balance updates atomic.

Try saying this at the defense
"The architecture keeps the wallet's private key in the browser. The browser signs a canonical message, the node verifies the signature, then treats the signed transaction as the user's intent. The node itself produces a block every three seconds, choosing a proposer deterministically by stake. All state — blocks, balances, orders — is stored in Postgres, which is the source of truth."

What to read next