A browser-based IDE for writing, compiling, and deploying Solana Anchor programs — no local toolchain required.
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.
| Layer | Technology | Version |
|---|---|---|
| UI Framework | React | 18.3 |
| Build Tool | Vite | 5.4 |
| Language | TypeScript | 5.5 |
| Editor | Monaco Editor (@monaco-editor/react) | 4.6 |
| Web3 Client | @solana/web3.js | 2.0 |
| Hosting | Vercel | — |
| Layer | Technology | Version |
|---|---|---|
| Runtime | Node.js | 20 (Docker) |
| Framework | Express | 4.19 |
| Solana CLI | solana-cli | 1.18.18 |
| Anchor | avm + anchor-cli | 0.30.1 |
| Rust (build) | rustc | 1.85.1 |
| Rust (runtime) | rustc | 1.75.0 |
| platform-tools | SBF toolchain | v1.41 |
| Web3 Client | @solana/web3.js | 1.0 |
| Hosting | Railway (Docker) | — |
| Package | Role |
|---|---|
| @solshift/core | Types, constants, cluster config, wallet encryption utilities |
| @solshift/engine | CompilerClient — HTTP client for all build service endpoints |
| @solshift/shell | TerminalEmulator — interprets solana + anchor CLI commands in-browser |
| @solshift/plugin-manager | ProjectManager — scaffold, CRUD, localStorage persistence |
| @solshift/integrations | IDL client generator, PDA derivation, Borsh serialization |
┌─────────────────────────────────────────────────────────────┐
│ 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) │
└────────────────────────┘
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)
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
}
Each API request follows the same pattern:
/tmpcargo build-sbf, solana program deploy, etc.) via execSync with timeoutsfinally blockThe server has a concurrency limit (activeBuilds counter) — max 2 concurrent builds. Requests exceeding this get a 429 "build queue full" response.
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:
| Key | Content | Encrypted? |
|---|---|---|
| solpg_wallet | JSON-serialized WalletState (publicKey + secretKey + connected flag) | ✅ XOR cipher with session-derived key |
| solpg_projects | JSON array of SolpgProject (files + metadata) | No (no sensitive data) |
| solpg_cluster | Cluster name string ('devnet' | 'testnet' | 'mainnet-beta') | No |
| Key | Content | Why sessionStorage? |
|---|---|---|
| solpg_enc_key | 32-byte random XOR key (hex-encoded) | Cleared on tab close — wallet cipher key never persists |
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.
// 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.
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)
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
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
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
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
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
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
| Concern | Mitigation |
|---|---|
| Wallet secret keys | XOR-encrypted in localStorage, key derived from sessionStorage (wiped on tab close) |
| API authentication | Railway deployment uses RAILWAY_TOKEN (GitHub secret), no public auth on endpoints |
| CORS | Backend allows all origins (development service) |
| Payload limits | 5MB JSON limit on Express body-parser |
| Build isolation | Each build in unique /tmp directory, recursive cleanup after |
| Docker isolation | Containerized build environment — host system is isolated |
| Faucet key | Stored as Railway secret (FAUCET_SECRET_KEY), never in code |
| Frontend secrets | API URL is public; no secrets in client bundle |
| File upload | Paths validated against traversal (../), max file size 200KB, max 40 files |
| Browser wallet deploy | Temp keypair generated in-browser, funded for exactly 1 SOL, ephemeral |
The Docker image is built in three stages:
cargo fetch + cargo generate-lockfile to pre-cache all dependenciesThe 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 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).
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?"
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 }
}
Rendered as an icicle chart (top-down flamegraph) using pure SVGs — no external charting libraries:
| Type | Has Secret Key | Can Build | Can Deploy | Can Airdrop |
|---|---|---|---|---|
| Generated | ✅ Hex (64 bytes) | ✅ | ✅ | ✅ |
| Imported | ✅ Hex / JSON / Base58 | ✅ | ✅ | ✅ |
| Browser (Phantom/Solflare/Backpack) | ❌ Not exposed | ✅ | ✅ | ✅ |
Uses the wallet-standard API with legacy fallback:
wallet-standard:register-wallet custom eventnavigator.wallets for standard-compliant walletswindow.solana for legacy adapters| Component | Host | Trigger |
|---|---|---|
| Frontend | Vercel | Push to main → auto-deploy |
| Backend | Railway | GitHub Actions → npx railway up |
| DNS | Vercel (frontend) / Railway (backend) | — |
| Variable | Type | Purpose |
|---|---|---|
| PORT | Public | Server port (8080) |
| DEVNET_RPC_URL | Secret | Custom devnet RPC endpoint for airdrops |
| FAUCET_SECRET_KEY | Secret | Faucet wallet private key (hex 64 bytes) |
| HELIUS_API_KEY | Secret | Helius RPC key for airdrop fallback |
| MAX_CONCURRENT_BUILDS | Public | Build queue limit (default 2) |
| BUILD_TIMEOUT_MS | Public | Per-build timeout (default 300000) |
| VITE_COMPILER_API_URL | Public | Frontend → backend API URL |
Four built-in templates for one-click project scaffolding:
| Template | Framework | Instructions | Accounts |
|---|---|---|---|
| Anchor Counter | Anchor 0.30 | initialize | 1 |
| Native Hello World | Native (solana-program) | process_instruction | None |
| SPL Token Transfer | Anchor 0.30 | create_mint, mint_to, send_with_memo | 4-5 |
| Coin Flip Game | Anchor 0.30 | initialize_house, play, settle | 3-4 |
Each template includes: Rust source, Cargo.toml, Anchor.toml, TypeScript client, and test stubs.
BullMQ + Redis for persistent build history, retries, and horizontal scaling across multiple workers.
Firecracker microVMs or gVisor for per-build isolation — prevents cross-build contamination.
Per-user S3/GCS buckets keyed by project hash + lockfile hash for <5s warm builds.
Generate a typed TypeScript client from the IDL after every build — downloadable as a package.
Encode full project state in a URL (solphg.app/p/{id}) — read-only by default, forkable.
Side-by-side comparison of two profiler runs (before/after code change).
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