Architecture overview
SolBid is built on a modern 3-tier architecture that combines blockchain permanence with real-time web performance. This page provides a comprehensive overview of how the components work together.
High-level architecture
┌─────────────────────────────────────────────────────────────┐
│ Next.js Frontend │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ React UI │ │ API Routes │ │ Solana Client │ │
│ └──────────────┘ └──────────────┘ └─────────────────┘ │
└────────────┬────────────────┬────────────────┬─────────────┘
│ │ │
│ WebSocket │ REST API │ RPC Calls
│ │ │
┌────────────▼────────┐ ┌────▼────────┐ ┌───▼──────────────┐
│ WebSocket Server │ │ Database │ │ Solana Program │
│ (Node.js + WS) │ │ (Prisma) │ │ (Rust) │
└─────────────────────┘ └─────────────┘ └──────────────────┘
│ │
│ │
└────── Real-time Game Updates ──────────┘
Solana Program On-chain smart contract handling game logic, bid validation, and prize distribution
Next.js Application Full-stack web app with React UI, API routes, and Solana integration
WebSocket Server Real-time game state synchronization across all connected clients
Tier 1: Solana smart contract
The core game logic runs entirely on-chain via a Rust-based Solana program located in programs/src/.
Program structure
// programs/src/lib.rs
use solana_program :: entrypoint;
use processor :: process_instruction;
pub mod instructions ;
pub mod processor ;
pub mod state ;
pub mod error ;
pub mod utils ;
entrypoint! ( process_instruction );
The program follows the standard Solana program architecture with:
Entry point : process_instruction handles all incoming instructions
Instructions : Define the available operations (CreateGame, PlaceBid)
State : Define account data structures (GameState, PlayerState, Bid)
Processor : Routes instructions to handler functions
Utils : Helper functions for PDAs and data deserialization
Account structure
SolBid uses Program Derived Addresses (PDAs) to store game data:
Game PDA
// programs/src/state.rs:4
pub struct GameState {
pub game_id : u64 ,
pub initial_bid_amount : u64 ,
pub highest_bid : u64 ,
pub last_bid_time : u64 ,
pub total_bids : u64 ,
pub last_bidder : Pubkey ,
pub prize_pool : u64 ,
pub platform_fee_percentage : u64 ,
pub game_ended : bool ,
}
Derived using: [b"game", game_id]
Player PDA
// programs/src/state.rs:17
pub struct PlayerState {
pub total_bid_amount : u64 ,
pub safe : bool ,
pub royalty_earned : u64 ,
pub bid_count : u64 ,
}
Derived using: [b"player", game_id, player_pubkey, bid_count]
Each bid creates a new Player PDA because the bid count changes. This allows the program to track all player interactions throughout the game.
Bid PDA
// programs/src/state.rs:25
pub struct Bid {
pub bidder : Pubkey ,
pub amount : u64 ,
pub timestamp : u64 ,
}
Derived using: [b"bid", game_id, bid_number]
Core instructions
CreateGame
Creates a new game with an initial bid:
// programs/src/instructions/create_game.rs:22
pub fn create_game (
program_id : & Pubkey ,
accounts : & [ AccountInfo ],
game_id : u64 ,
initial_bid_amount : u64 ,
) -> ProgramResult
Process flow:
Validate initial bid
Ensures the initial bid is at least 14,000,000 lamports (0.014 USDC).
Derive PDAs
Calculates Game, Player, and Bid PDA addresses using the game ID and player public key.
Create accounts
Uses invoke_signed to create the three PDA accounts with appropriate rent and space.
Initialize state
Serializes GameState, PlayerState, and Bid data into the account data.
Transfer initial bid
Transfers the bid amount from the player to the Game PDA.
PlaceBid
Adds a new bid to an existing game:
// programs/src/instructions/place_bid.rs:16
pub fn place_bid (
program_id : & Pubkey ,
accounts : & [ AccountInfo ],
bid_amount : u64 ,
bid_count : u64 ,
) -> ProgramResult
Process flow:
Deserialize game state
Loads current game data from the Game PDA.
Check if game ended
Verifies the game hasn’t already ended and checks if the timer expired.
Validate bid amount
Ensures bid is at least 2x the current highest bid.
Validate bid count
Prevents race conditions by verifying bid_count matches total_bids + 1.
Create new accounts
Creates new Player and Bid PDAs for this bid entry.
Update game state
Updates highest_bid, last_bidder, total_bids, prize_pool, and last_bid_time.
Transfer bid to pool
Transfers bid amount to the Game PDA.
Prize distribution
When the timer expires, the end_game function handles prize distribution:
// programs/src/instructions/place_bid.rs:153
pub fn end_game <' a , ' b : ' a >(
program_id : & Pubkey ,
game_id : u64 ,
accounts : & ' a [ AccountInfo <' b >],
game_state : & mut GameState ,
) -> ProgramResult
Distribution logic:
Fetch bid history
Retrieves all Bid PDAs from the accounts array using fetch_bid_history.
Determine distribution model
Games with fewer than 5 bids use simplified distribution (winner gets 90%, platform 10%).
Calculate royalties
For 5+ bid games, calculates weighted royalties for early bidders using the cal_royalties function.
Transfer platform fee
Sends 10% of the last 5 bids to the platform account.
Transfer winner prize
Sends remaining balance to the last bidder’s account.
Update player states
Marks players as “safe” and updates their royalty_earned fields.
End the game
Sets game_ended to true, preventing further bids.
The prize distribution happens atomically in a single transaction. If any transfer fails, the entire transaction reverts.
Tier 2: Next.js application
The frontend is a full-stack Next.js 14 application located in next-app/.
Technology stack
Framework : Next.js 14 with App Router
UI : React 18 with Tailwind CSS and Radix UI components
Blockchain : Solana Web3.js + Wallet Adapter
Authentication : NextAuth.js with Prisma adapter
Database : PostgreSQL via Prisma ORM
State management : React Context API
Real-time : WebSocket client
Solana integration
The app integrates with Solana through several key modules:
Transaction building
// next-app/solana/game.ts:13
export const createGame = async (
publicKey : PublicKey ,
gameId : number ,
bidAmount : number
) => {
// Serialize instruction data
const instructionData = Buffer . concat ([
Buffer . from ([ 0 ]), // CreateGame discriminator
gameIdBuffer ,
initialBidAmountBuffer ,
])
// Build transaction instruction
const createGameIx = new TransactionInstruction ({
keys: [
{ pubkey: gamePda , isSigner: false , isWritable: true },
{ pubkey: publicKey , isSigner: true , isWritable: true },
{ pubkey: SystemProgram . programId , isSigner: false , isWritable: false },
{ pubkey: playerPda , isSigner: false , isWritable: true },
{ pubkey: bidPda , isSigner: false , isWritable: true },
],
programId: PROGRAM_ID ,
data: instructionData ,
})
return { instructions: [ createGameIx ], totalCost: initialBidLamports , gamePda , playerPda , bidPda }
}
PDA derivation
// next-app/solana/pda.ts:5
export function getGamePda ( gameId : BN ) : PublicKey {
const gameIdBuffer = Buffer . from ( gameId . toArray ( 'le' , 8 ));
const [ gamePda ] = PublicKey . findProgramAddressSync (
[ Buffer . from ( 'game' ), gameIdBuffer ],
PROGRAM_ID
);
return gamePda ;
}
PDA derivation must exactly match the on-chain program’s logic to ensure address consistency.
Transaction execution
The useTransaction hook manages transaction lifecycle:
// From next-app/components/game/CreateGame.tsx:63
const txId = await execute ( async () => {
const { instructions , totalCost , gamePda , playerPda , bidPda } =
await createGame ( publicKey , gameId , bidAmount );
return { instructions , totalCost };
});
The hook handles:
Transaction construction
Wallet signing
Network submission
Confirmation polling
Error handling
API routes
Next.js API routes provide backend functionality:
Route Purpose /api/gameCreate game records in database /api/bidRecord bids and retrieve player/bid PDAs /api/pdaFetch all PDAs for a game /api/auth/*Authentication and user management /api/dashboardAggregate game and transaction data
API routes serve as the bridge between on-chain data and the off-chain database, ensuring data consistency and enabling fast queries.
Component architecture
Key React components:
CreateGame component
// next-app/components/game/CreateGame.tsx:28
export default function CreateGame ({ onCreateGame } : CreateGameProps ) {
const { sendMessage } = useSocket ();
const { publicKey } = useWallet ();
const { status , execute , resetStatus } = useTransaction ( CONNECTION );
// 1. Build and execute transaction
// 2. Store game in database via API
// 3. Broadcast to WebSocket server
// 4. Update local UI state
}
PlaceBid component
// next-app/components/game/PlaceBid.tsx:33
export default function PlaceBid ({
gameId ,
bidCount ,
currBid ,
onPlaceBid ,
} : PlaceBidModalProps ) {
// Validates bid is 2x current bid
// Checks if game ended during transaction
// Handles race conditions with bid count verification
}
Tier 3: WebSocket server
The WebSocket server (ws/src/) provides real-time game state synchronization.
Server implementation
// ws/src/app.ts:5
function main () {
const server = createHttpServer ();
const wss = new WebSocketServer ({ server });
wss . on ( "connection" , handleConnection );
const PORT = process . env . PORT ?? 8080 ;
server . listen ( PORT , () => {
console . log ( `WS server is running on port ${ PORT } ` );
});
}
Connection handling
// ws/src/connectionHandler.ts:7
export function handleConnection ( ws : WebSocket ) {
const gameManager = GameManager . getInstance ();
gameManager . addClient ( ws );
ws . on ( "message" , async ( raw : Buffer ) => {
const { data } = JSON . parse ( raw . toString ());
const token = data ?. token ;
// Verify JWT token
const authorized = await verifyToken ( ws , token );
if ( ! authorized ) {
sendError ( ws , "Unauthorized" );
ws . close ();
return ;
}
handleMessage ( ws , raw . toString ());
});
}
All WebSocket connections are authenticated using JWT tokens generated by NextAuth. Unauthenticated clients are immediately disconnected.
Game state management
The GameManager singleton maintains in-memory game state:
// ws/src/gameManager.ts:7
class GameManager {
private static instance : GameManager ;
private games : Map < number , Game > = new Map ();
private clients : Set < WebSocket > = new Set ();
addGame ( game : Game ) {
if ( ! this . games . has ( gameId )) {
this . games . set ( gameId , game );
this . broadcastNewGame ( game );
}
}
updateGame ( gameId : number , data : any ) {
const game = this . games . get ( gameId );
if ( game ) {
this . updateGameData ( game , data );
this . broadcastBidUpdate ( game );
}
}
}
Message types
Message Type Direction Purpose create-gameClient → Server Notify server of new game creation place-bidClient → Server Notify server of new bid new-gameServer → Clients Broadcast new game to all clients game-updateServer → Clients Broadcast bid update to all clients
Data flow
Creating a game
1. User clicks "Create Game" in UI
└─> next-app/components/game/CreateGame.tsx:37
2. Frontend builds transaction instruction
└─> next-app/solana/game.ts:13
3. User approves transaction in wallet
└─> @solana/wallet-adapter-react
4. Transaction submitted to Solana
└─> programs/src/instructions/create_game.rs:22
5. Smart contract creates PDAs and initializes state
└─> Solana blockchain
6. Frontend stores game in database
└─> next-app/app/api/game/route.ts
7. Frontend broadcasts to WebSocket server
└─> ws/src/gameManager.ts:31
8. WebSocket server broadcasts to all clients
└─> All connected browsers receive update
Placing a bid
1. User enters bid amount in PlaceBid modal
└─> next-app/components/game/PlaceBid.tsx:49
2. Frontend validates bid is 2x current bid
└─> Client-side validation
3. Frontend fetches existing player/bid PDAs
└─> next-app/app/api/bid/route.ts (GET)
4. Frontend builds transaction with all required accounts
└─> next-app/solana/bid.ts:14
5. Transaction submitted to Solana
└─> programs/src/instructions/place_bid.rs:16
6. Smart contract validates and updates state
└─> Validates bid amount, creates new PDAs, transfers SOL
7. Frontend checks if game ended
└─> next-app/solana/game.ts:50
8. Frontend stores bid in database
└─> next-app/app/api/bid/route.ts (POST)
9. WebSocket broadcast to all clients
└─> Real-time update across all browsers
Security considerations
On-chain security
PDA validation
Every instruction validates that provided PDAs match expected derivations: if * game_account . key != game_pda {
return Err ( BiddingError :: InvalidGameAccount . into ());
}
Bid validation
The smart contract enforces the 2x minimum bid rule and timer expiration on-chain, preventing client-side manipulation.
Atomic distributions
Prize distributions happen in a single transaction, ensuring either complete success or complete failure.
Rent exemption
All accounts maintain rent-exempt balances to prevent account closure.
Off-chain security
JWT authentication
WebSocket connections require valid JWT tokens issued by NextAuth.
Input validation
All user inputs are validated both client-side and server-side before blockchain submission.
Database integrity
Prisma ORM prevents SQL injection and enforces data type constraints.
Transaction optimization
Account chunking : For games with many bids, the placeBidScale function chunks account arrays to stay within Solana’s transaction size limits
Parallel account creation : Game, Player, and Bid accounts are created in a single transaction using invoke_signed
Frontend optimization
Optimistic UI updates : UI updates immediately while waiting for transaction confirmation
WebSocket debouncing : Game state updates are debounced to prevent excessive re-renders
Connection pooling : Shared Solana RPC connection across components
Backend optimization
In-memory game state : WebSocket server caches active games in memory for instant broadcasts
Database indexing : Game ID, user ID, and timestamp fields are indexed for fast queries
The combination of on-chain state storage and off-chain caching provides both data permanence and real-time performance.
Deployment architecture
For production deployments:
┌─────────────────┐
│ Vercel/CDN │ Next.js frontend (edge deployed)
└────────┬────────┘
│
┌────────▼────────┐
│ WebSocket VPS │ Node.js WebSocket server
└────────┬────────┘
│
┌────────▼────────┐
│ PostgreSQL │ Managed database (e.g., Supabase)
└─────────────────┘
┌─────────────────┐
│ Solana Devnet │ Smart contract deployment
└─────────────────┘
Configuration
Key configuration values:
// next-app/lib/constant.ts:3
export const PROGRAM_ID = new PublicKey ( '71Q2euxEEAzcqFBNNgAgUaTTE63dJaVioLmKx7AKMpSB' );
export const CONNECTION = new Connection ( "https://api.devnet.solana.com" , "confirmed" );
export const PLATFROM_ACCOUNT = new PublicKey ( '7UxgfmMiNMbjHxEayn51uRjkeyrMiR4pPXWbo8sFUrsG' );
For mainnet deployment, update PROGRAM_ID, CONNECTION, and PLATFROM_ACCOUNT to use mainnet addresses and RPC endpoints.
Next steps
Now that you understand the architecture:
Explore the Quickstart guide to interact with the system
Review the smart contract code in programs/src/ to understand the game logic
Check the API documentation to integrate with external applications
Study the WebSocket protocol for building custom clients