Solana Playground

A browser-based IDE for writing, compiling, and deploying Solana Anchor programs — no local toolchain required.

By Suruj Kalita · Open source · GitHub

Why I Built It

Learning Solana development has a steep barrier to entry. Before you can write your first program, you need to install Rust, the Solana CLI, Anchor, set up a local validator, configure your IDE, and figure out devnet faucets. That's five steps before writing a single line of code.

Solana Playground removes all of it. You write Rust in a Monaco editor, click "Build", and get a compiled SBF program — all from your browser.

Solana Playground screenshot 1 Solana Playground screenshot 2

Tech Stack

Frontend

LayerTechnologyVersion
UI FrameworkReact18.3
Build ToolVite5.4
LanguageTypeScript5.5
EditorMonaco Editor (@monaco-editor/react)4.6
Web3 Client@solana/web3.js2.0
HostingVercel

Backend (Compiler Service)

LayerTechnologyVersion
RuntimeNode.js20 (Docker)
FrameworkExpress4.19
Solana CLIsolana-cli1.18.18
Anchoravm + anchor-cli0.30.1
Rust (build)rustc1.85.1
Rust (runtime)rustc1.75.0
platform-toolsSBF toolchainv1.41
Web3 Client@solana/web3.js1.0
HostingRailway (Docker)

Shared Packages (npm Workspaces)

PackageRole
@solshift/coreTypes, constants, cluster config, wallet encryption utilities
@solshift/engineCompilerClient — HTTP client for all build service endpoints
@solshift/shellTerminalEmulator — interprets solana + anchor CLI commands in-browser
@solshift/plugin-managerProjectManager — scaffold, CRUD, localStorage persistence
@solshift/integrationsIDL client generator, PDA derivation, Borsh serialization

System Design — Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│ Browser (Vercel CDN)                                        │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ React SPA (Vite)                                      │  │
│  │  ┌──────────┐ ┌──────────┐ ┌────────┐ ┌───────────┐ │  │
│  │  │  Monaco  │ │ Terminal │ │ Wallet │ │ Build/    │ │  │
│  │  │  Editor  │ │ Emulator │ │ Panel  │ │ Deploy UI │ │  │
│  │  └────┬─────┘ └────┬─────┘ └────────┘ └─────┬─────┘ │  │
│  │       └────────────┴──────────┬──────────────┘       │  │
│  │                        ┌──────┴──────┐               │  │
│  │                        │ Compiler    │               │  │
│  │                        │ Client      │               │  │
│  │                        └──────┬──────┘               │  │
│  └───────────────────────────────┼───────────────────────┘  │
│                                  │ HTTP (fetch)              │
└──────────────────────────────────┼───────────────────────────┘
                                   ▼
┌─────────────────────────────────────────────────────────────┐
│ Railway (Docker Container)                                  │
│  ┌───────────────────────────────────────────────────────┐  │
│  │ Express Server                                         │  │
│  │  POST /api/build    → buildService.ts → cargo build-sbf│  │
│  │  POST /api/deploy   → solana program deploy            │  │
│  │  POST /api/simulate → balance + rent + conflict check  │  │
│  │  POST /api/debug-cpi→ CPI log parser / local validator │  │
│  │  POST /api/profile  → local validator + simulate + CU  │  │
│  │  POST /api/airdrop  → faucet → RPC pool → fallback    │  │
│  │  GET  /api/balance  → solana balance                   │  │
│  └──────────────────────┬────────────────────────────────┘  │
│                         │                                    │
│  ┌──────────────────────▼────────────────────────────────┐  │
│  │ Docker Image (rust:1.75-slim-bookworm)                 │  │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ │  │
│  │  │ Solana   │ │ Anchor   │ │ Rust     │ │platform-│ │  │
│  │  │ CLI 1.18 │ │ 0.30.1   │ │ 1.85     │ │tools    │ │  │
│  │  └──────────┘ └──────────┘ └──────────┘ └─────────┘ │  │
│  └──────────────────────────────────────────────────────┘  │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
              ┌────────────────────────┐
              │ Solana Devnet / Testnet│
              │ (RPC API)              │
              └────────────────────────┘

Frontend — Component Tree & State Management

The frontend manages all state in a single App component using React hooks (useState, useCallback, useRef, useEffect). No external state library — the application's state surface is small enough that prop drilling and callback passing suffice.

App
├── LandingPage (marketing site, toggled via showLanding boolean)
└── Playground
    ├── Toolbar
    │   ├── Cluster selector (devnet/testnet/mainnet)
    │   ├── Build button
    │   ├── Deploy button
    │   └── API status dot
    ├── Icon Sidebar (files | search | docs | settings)
    ├── Side Panel
    │   ├── FileExplorer (file tree + project files)
    │   ├── WalletPanel (generate, import, connect browser wallet, airdrop)
    │   └── DocsPanel (faucet docs, funding instructions)
    ├── EditorPanel (Monaco editor, language-aware)
    ├── BuildResult (build output + simulate + deploy + CPI + Profile CU)
    └── TerminalPanel (command input + scrollable output)

Key State

interface AppState {
  project: SolpgProject | null         // Current project + files
  activeFile: SolpgFile | null         // Currently open in editor
  wallet: WalletState | null           // publicKey + secretKey + connected
  balance: number | null               // Devnet SOL balance
  builtBytecode: string | null         // Compiled .so (base64)
  builtKeypair: string | undefined     // Program keypair (base64)
  buildResult: BuildResultData | null  // Build output info
  terminalLines: TerminalLine[]        // Terminal history
  isBuilding / isAirdropping / isSimulating / isProfiling
  apiConnected: boolean | undefined
  activeSidebar: 'files' | 'search' | 'docs' | 'settings' | ''
  cluster: 'devnet' | 'testnet' | 'mainnet-beta'
  profileData / cpiData / simulation   // Analysis results
}

Backend — Express Request Lifecycle

Each API request follows the same pattern:

  1. Validation — Check required fields, keypair byte lengths, file constraints
  2. Scaffolding — Create a UUID-temp directory under /tmp
  3. Execution — Run the relevant CLI command (cargo build-sbf, solana program deploy, etc.) via execSync with timeouts
  4. Cleanup — Remove temp directory in finally block
  5. Response — Return JSON with success/error and relevant data

The server has a concurrency limit (activeBuilds counter) — max 2 concurrent builds. Requests exceeding this get a 429 "build queue full" response.

Storage & Data Persistence (No Traditional Database)

There is no database — no PostgreSQL, no Redis, no MongoDB. The architecture deliberately avoids persistent storage for simplicity and cost. Data lives in three places:

1. Browser localStorage

KeyContentEncrypted?
solpg_walletJSON-serialized WalletState (publicKey + secretKey + connected flag)✅ XOR cipher with session-derived key
solpg_projectsJSON array of SolpgProject (files + metadata)No (no sensitive data)
solpg_clusterCluster name string ('devnet' | 'testnet' | 'mainnet-beta')No

2. Browser sessionStorage

KeyContentWhy sessionStorage?
solpg_enc_key32-byte random XOR key (hex-encoded)Cleared on tab close — wallet cipher key never persists

3. /tmp on the Docker Container

Each API request creates a UUID-named directory under /tmp for temporary files (keypairs, program binaries, Anchor project scaffolding). These are cleaned up in the finally block of every request handler.

Wallet Encryption Scheme

// On tab open: generate 32 random bytes → store in sessionStorage
// On wallet save:
//   1. JSON.stringify({ publicKey, secretKey, connected })
//   2. XOR each byte of the JSON string with key[i % 32]
//   3. Hex-encode the XOR'd bytes
//   4. Store in localStorage as 'solpg_wallet'
//
// On wallet load: reverse the process
// On tab close: sessionStorage is wiped → key is lost → ciphertext is useless
//
// This ensures secret keys never persist in localStorage as plaintext
// after the browser tab is closed.

API Design — Endpoint Details

POST /api/build — The Core Pipeline

Request:  { programName: string, files: { path: string, content: string }[] }
Response: { success: boolean, program?: string (base64 .so), programId?: string,
            programKeypair?: string (base64), error?: string, logs?: string }

Flow:
  1. Validate programName (lowercase snake_case, 2-64 chars) and files (max 40, max 200KB each)
  2. Scaffold project skeleton under /tmp/{uuid}:
     - programs/{name}/src/  ← user's .rs files
     - programs/{name}/Cargo.toml  ← auto-generated with anchor-lang dep
     - Anchor.toml
     - Cargo.toml (workspace)
  3. Copy pre-built Cargo.lock (cached from Docker build) for faster dependency resolution
  4. Run `cargo generate-lockfile --offline`
  5. Downgrade lockfile v4 → v3 (compatibility)
  6. Run `CARGO_TARGET_DIR=... cargo build-sbf --offline`
  7. Read artifacts from shared CARGO_TARGET_DIR:
     - {name}.so → base64 encode
     - {name}-keypair.json → base64 encode + derive programId via solana-keygen
  8. Clean up temp dir
  9. Return result

Validation:
  - programName must match /^[a-z][a-z0-9_-]{1,63}$/
  - File paths must be relative, no '..' traversal
  - Max 40 files, max 200KB per file
  - Timeout: 300 seconds (cargo build can be slow on cold starts)

POST /api/deploy

Request:  { bytecodeBase64: string, authoritySecretKey: string (hex 64 bytes),
            programKeypair?: string (base64), cluster?: string }
Response: { signature?: string, programId?: string, error?: string }

Flow:
  1. Decode + validate authoritySecretKey (must be exactly 64 bytes)
  2. Write authority keypair JSON + program binary to /tmp
  3. If programKeypair provided: write it → deterministic program ID
  4. Run: solana program deploy --chunk-size 65536 --url {cluster}
  5. Parse "Program Id: <address>" from output
  6. Return transaction output + program ID

Security:
  - The authority's secret key is transmitted to the backend (hex-encoded over HTTPS)
  - Browser wallets (Phantom) now supported via temp keypair generation + airdrop funding

POST /api/airdrop — Multi-Tier Fallback

Request:  { address: string, amount?: number, cluster?: string }
Response: { signature?: string, error?: string }

Tier 1 — Faucet Transfer (instant, no rate limits):
  IF FAUCET_SECRET_KEY is set and has SOL:
    solana transfer --keypair faucet.json {address} {amount}
  ELSE fall through

Tier 2 — RPC Airdrop Pool (25s deadline):
  For each RPC in shuffled pool [DEVNET_RPC_URL, api.devnet.solana.com, helius]:
    Approach A: solana airdrop {amount} {address} --url {rpc} (20s timeout)
    Approach B: Connection.requestAirdrop(pubkey, lamports) (15s timeout)
    Break on first success

Tier 3 — Client-Side Fallback (user's browser IP):
  Frontend retries requestAirdrop from user's browser
  (separate rate-limit pool from Railway's IP)

Error handling:
  Returns actionable message with faucet funding instructions on total failure

POST /api/profile — CU Profiler

Request:  { bytecodeBase64: string, authoritySecretKey: string (hex 64 bytes),
            programKeypair?: string, instructionData?: string (base64) }
Response: { success: boolean, totalCuConsumed: number, cuCap: number,
            cuUtilization: number, programId: string,
            instructions: CpuProfileNode[], error?: string, logs: string[] }

CpuProfileNode:
  { programId: string, cuConsumed: number, ownCu: number,
    depth: number, success: boolean, error?: string,
    percentage: number, isHotspot: boolean (>20% of total),
    children: CpuProfileNode[] }

Flow:
  1. Write program + keypairs to /tmp
  2. Start solana-test-validator (local, ephemeral)
  3. Deploy program to local validator
  4. Construct Transaction with program instruction
  5. simulateTransaction({ innerInstructions: true, sigVerify: false })
  6. Parse logs with regex stack-based parser:
     - /Program (\w+) invoke \[(\d+)\]/     → push node
     - /Program (\w+) consumed (\d+) of (\d+)/ → assign CU
     - /Program (\w+) success/               → pop, mark success
     - /Program (\w+) failed: (.+)/          → pop, mark error
  7. Compute own CU (cuConsumed - sum of children's cuConsumed)
  8. Identify hotspots (>20% = isHotspot: true)
  9. Return tree + total + utilization %

Fallback:
  If solana-test-validator unavailable → return helpful message + empty tree

POST /api/simulate — Pre-Deploy Simulation

Request:  { bytecodeBase64, authoritySecretKey, programKeypair?, cluster? }
Response: { success: boolean, bytecodeSize, estimatedRentSol, authorityBalance,
            hasSufficientBalance, programExists, output }

Logic:
  - Get authority balance from cluster RPC
  - Derive program ID from keypair
  - Check if program already exists on chain (upgrade vs fresh deploy)
  - Estimate rent: ceil((100 + bytecodeSize) / 1024) * 0.0035 SOL
  - Return comparison: balance >= rent + txFee?
  - Shows user what will happen before they spend SOL

POST /api/debug-cpi — CPI Trace Analysis

Two modes:
  Mode 1 — Parse Logs: { rawLogs: string }
    → Parses simulation log output into CPI call tree
    → Returns { cpiTree: CpiNode[], summary, rawLogs }

  Mode 2 — Auto-Trace: { bytecodeBase64, idl? }
    → Starts solana-test-validator, deploys, simulates
    → Falls back gracefully if validator unavailable

CpiNode: { programId, depth, computeUnits, success, error?, accounts, children[] }

The parser handles three log line types:
  - "Program <id> invoke [<depth>]"      → push node onto stack
  - "Program <id> consumed <n> of <m>"   → assign CU by program ID (searches stack backward)
  - "Program <id> success"                → pop, mark success=true
  - "Program <id> failed: <message>"     → pop, mark success=false, extract error text

Data Flow — Build to Deploy

User writes Rust code in Monaco editor
        │
        ▼
onChange → ProjectManager.updateFile() → localStorage (persisted)
        │
User clicks "Build"
        │
        ▼
handleBuild():
  1. CompilerClient.build({ programName, files: .rs sources })
  2. POST /api/build → Express → scaffold → cargo build-sbf
  3. Returns: { program (base64), programId, programKeypair }
  4. setBuiltBytecode / setBuiltKeypair / setBuildResult
  5. Terminal: "Build complete. Program ID: ..."
        │
User clicks "Deploy"
        │
        ▼
handleDeploy():
  1. If wallet has secretKey → use it directly
  2. If browser wallet (no secretKey) → generate temp Keypair,
     fund via airdrop, use temp keypair's hex
  3. POST /api/deploy → solana program deploy --chunk-size 65536
  4. Terminal: "Deploy tx: {signature}"
        │
User clicks "Profile CU"  (post-deploy analysis)
        │
        ▼
handleProfile():
  1. POST /api/profile → local validator → simulate → parse CU logs
  2. Returns: { totalCuConsumed, cuUtilization, instructions: [...] }
  3. CuProfiler renders icicle chart with hotspots

Security Model

ConcernMitigation
Wallet secret keysXOR-encrypted in localStorage, key derived from sessionStorage (wiped on tab close)
API authenticationRailway deployment uses RAILWAY_TOKEN (GitHub secret), no public auth on endpoints
CORSBackend allows all origins (development service)
Payload limits5MB JSON limit on Express body-parser
Build isolationEach build in unique /tmp directory, recursive cleanup after
Docker isolationContainerized build environment — host system is isolated
Faucet keyStored as Railway secret (FAUCET_SECRET_KEY), never in code
Frontend secretsAPI URL is public; no secrets in client bundle
File uploadPaths validated against traversal (../), max file size 200KB, max 40 files
Browser wallet deployTemp keypair generated in-browser, funded for exactly 1 SOL, ephemeral

Build Pipeline — Docker Details

The Docker image is built in three stages:

  1. builder-toolchain (rust:1.75-slim-bookworm):
    • Installs build-essential, Node.js 20, curl, git
    • Installs Solana CLI 1.18.18 via the official install script
    • Installs Anchor 0.30.1 via avm (Anchor Version Manager)
    • Installs Rust 1.85.1 for metadata resolution
    • Downloads and extracts platform-tools v1.41 (SBF compiler)
    • Creates a dummy Anchor project and runs cargo fetch + cargo generate-lockfile to pre-cache all dependencies
    • Patches crates for edition2024 + MSRV compatibility (sed workarounds for hybrid-array, cmov, anchor-syn)
    • Copies cached crates between hash directories for multi-Rust-version support
  2. api-builder (node:20-slim):
    • Installs npm dependencies
    • Runs tsc to compile TypeScript → JavaScript
  3. Production (from builder-toolchain):
    • Copies compiled JS + node_modules from api-builder
    • Exposes port 8080
    • CMD: node dist/index.js

The resulting image is ~2.5GB — it's large because it bundles the full Solana + Anchor toolchain. Cold deploys take 5-15 minutes. Subsequent deploys use cached layers and take ~1-2 minutes.

The Airdrop Challenge — In Depth

The devnet requestAirdrop endpoint has a daily limit of ~1 SOL per IP address. Since Railway's egress IP is shared across all users, hitting this limit was the #1 support issue.

The solution is a 3-tier fallback system with a hard 25-second deadline:

Request ──→ Tier 1: Faucet transfer
              ├── Has FAUCET_SECRET_KEY + balance?
              ├── YES → solana transfer → ✅ (instant, no rate limit)
              └── NO  → fall through to Tier 2

         ──→ Tier 2: RPC airdrop pool (shuffled)
              For each RPC endpoint (max 25s total):
                ├── CLI: solana airdrop (20s timeout)
                └── web3.js: Connection.requestAirdrop (15s timeout)
              └── All fail → fall through to Tier 3

         ──→ Tier 3: Client-side browser fallback
              Frontend calls createSolanaRpc().requestAirdrop()
              Uses user's browser IP (separate rate-limit pool)
              └── All fail → show error + funding instructions

The faucet wallet address is pinned:
  3LymxuUGBT67AXqNJQVkRtbvd7kpywyXoUhpDpob2rgR
Fund it at solfaucet.com for instant transfers (no rate limits).

CU Profiling — How It Works

The most recent feature added to Solphg. It answers the question every Solana developer eventually asks: "Why is my program consuming so many compute units?"

The Log Parser (Pure Function)

function parseCpuProfileLogs(logs: string[]) {
  const roots = [], stack = []
  for (const line of logs) {
    if (line matches "Program X invoke [depth]")   → push node onto stack
    if (line matches "Program X consumed N of M")  → assign CU via stack search
    if (line matches "Program X success")          → pop, mark success
    if (line matches "Program X failed: msg")      → pop, mark error
  }
  // Enhance with: ownCu = cuConsumed - sum(children.cuConsumed)
  //               percentage = cuConsumed / totalCu * 100
  //               isHotspot = percentage > 20
  return { tree: enhancedRoots, totalCu }
}

The Flamechart UI

Rendered as an icicle chart (top-down flamegraph) using pure SVGs — no external charting libraries:

Wallet System

Three Wallet Types

TypeHas Secret KeyCan BuildCan DeployCan Airdrop
Generated✅ Hex (64 bytes)
Imported✅ Hex / JSON / Base58
Browser (Phantom/Solflare/Backpack)❌ Not exposed

Browser Wallet Detection

Uses the wallet-standard API with legacy fallback:

  1. Listens for wallet-standard:register-wallet custom event
  2. Queries navigator.wallets for standard-compliant wallets
  3. Falls back to window.solana for legacy adapters
  4. Polls every 2 seconds for late-detected wallets

Deployment Topology

ComponentHostTrigger
FrontendVercelPush to main → auto-deploy
BackendRailwayGitHub Actions → npx railway up
DNSVercel (frontend) / Railway (backend)

Environment Variables

VariableTypePurpose
PORTPublicServer port (8080)
DEVNET_RPC_URLSecretCustom devnet RPC endpoint for airdrops
FAUCET_SECRET_KEYSecretFaucet wallet private key (hex 64 bytes)
HELIUS_API_KEYSecretHelius RPC key for airdrop fallback
MAX_CONCURRENT_BUILDSPublicBuild queue limit (default 2)
BUILD_TIMEOUT_MSPublicPer-build timeout (default 300000)
VITE_COMPILER_API_URLPublicFrontend → backend API URL

Project Templates

Four built-in templates for one-click project scaffolding:

TemplateFrameworkInstructionsAccounts
Anchor CounterAnchor 0.30initialize1
Native Hello WorldNative (solana-program)process_instructionNone
SPL Token TransferAnchor 0.30create_mint, mint_to, send_with_memo4-5
Coin Flip GameAnchor 0.30initialize_house, play, settle3-4

Each template includes: Rust source, Cargo.toml, Anchor.toml, TypeScript client, and test stubs.

What's Next

Real Job Queue

BullMQ + Redis for persistent build history, retries, and horizontal scaling across multiple workers.

Sandboxed Builds

Firecracker microVMs or gVisor for per-build isolation — prevents cross-build contamination.

Persistent Build Cache

Per-user S3/GCS buckets keyed by project hash + lockfile hash for <5s warm builds.

Auto-Generated TS Client

Generate a typed TypeScript client from the IDL after every build — downloadable as a package.

Shareable Project Links

Encode full project state in a URL (solphg.app/p/{id}) — read-only by default, forkable.

CU Profiler Diff

Side-by-side comparison of two profiler runs (before/after code change).

Tech Tags

React 18.3 TypeScript 5.5 Vite 5.4 Monaco Editor Express 4.19 Node.js 20 Solana CLI 1.18 Anchor 0.30 Rust 1.85 Docker Vercel Railway @solana/web3.js v2 wallet-standard npm workspaces GitHub Actions

GitHub Repository · Live Demo · API Health