Okay, picture this: you're building a little NFT dropper or a token swapper for your side hustle, and suddenly you realize, "Shit, I need a backend that talks to Solana without babysitting servers." Happened to me last week. Friend hits me up, "How do I set this up quick?" So, I walked him through it. Took like 15 minutes. That's what we're doing here. Your backend - the serverless kind that handles wallet checks, transaction relays, all that jazz - live on Solana devnet. No fluff. Let's go.
Look, if you don't have these, nothing works. I usually start here every time.
Why these? Backend means Node scripts talking to Solana RPC. Can't do that without 'em. Sound familiar? Probably.
sh -c "$(curl -sSfL https://release.solana.com/stable/install)"export PATH="$HOME/.local/share/solana/install/active_release/bin:$PATH"source ~/.zshrc (or whatever your file is).solana --version. See something like 1.18.x? Good.The thing is, this CLI lets your backend send txns, check balances. Fees? Like 0.000005 SOL per signature. Pennies.
One gotcha: If you're on Windows without WSL, it might bitch. Switch to WSL. Fixed it for me instantly.
Don't mainnet yet. You'll burn real SOL testing dumb shit.
solana config set --url https://api.devnet.solana.comsolana config get. RPC should say devnet.solana airdrop 2. Might fail sometimes - Solana's anti spam. Try again or use a faucet site.In my experience, devnet's perfect for backend prototyping. Transactions confirm in seconds. Gas? Negligible, under 0.00001 SOL usually.
Now, the fun. Create a folder, mkdir solana backend && cd solana backend. Init npm: npm init -y.
Install the magic: npm install @solana/web3.js. That's your JS SDK. Talks to Solana like it's your buddy.
Make index.js. Paste this:
const { Connection, clusterApiUrl, Keypair, SystemProgram, Transaction } = require('@solana/web3.js'); async function main() { const connection = new Connection(clusterApiUrl('devnet'), 'confirmed'); const fromKeypair = Keypair.generate(); // Dummy payer for demo console.log('Public:', fromKeypair.publicKey.toBase58()); // Check balance const balance = await connection.getBalance(fromKeypair.publicKey); console.log('Balance:', balance / 1e9, 'SOL'); // Lamports to SOL
} main().catch(console.error);
Run node index.js. Boom. Public and balance (probably 0). Why does this matter? This is your backend querying Solana. Scale it to check user wallets later.
Pro tip: Always use 'confirmed' commitment. Faster than 'finalized' for dev.
Backend often calls smart programs. Solana's are in Rust. Install Rust first: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh. Then rustup update.
Make a program folder: cargo init counter program --lib && cd counter program.
Edit Cargo.toml, add:
[dependencies]
solana program = "1.18"
borsh = "0.10"
borsh derive = "0.10"
src/lib.rs - simple counter:
use borsh::{BorshDeserialize, BorshSerialize};
use solanaprogram::{ accountinfo::{nextaccountinfo, AccountInfo}, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey,
}; #[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct Counter { pub count: u32,
} entrypoint!(processinstruction); fn processinstruction( programid: &Pubkey, accounts: &[AccountInfo], instructiondata: &[u8],
) -> ProgramResult { let accountsiter = &mut accounts.iter(); let account = nextaccountinfo(accountsiter)?; let mut counterdata = Counter::tryfromslice(&account.data.borrow())?; counterdata.count += 1; counterdata.serialize(&mut &mut account.data.borrowmut()[.])?; msg!("Counter now: {}", counter_data.count); Ok(())
}
Build: cargo build bpf. target/deploy/counter_program.so is your bytecode.
Back in your backend folder. Generate keypair: solana keygen new --outfile keypair.json.
Now, script to deploy. Add to a deploy.js:
const { Connection, clusterApiUrl } = require('@solana/web3.js');
const fs = require('fs'); async function deploy() { const connection = new Connection(clusterApiUrl('devnet')); const keypair = JSON.parse(fs.readFileSync('keypair.json', 'utf8')); // Load from secret bytes actually, but simplify const programId = await connection.requestAirdrop(/ payer pubkey /, 1e9); // Fund first console.log('Deploying..'); // Full deploy command better via CLI, but here's JS way
} deploy();
Honestly? Use CLI for deploy: solana program deploy target/deploy/counter_program.so --keypair keypair.json. Grabs the program ID. Note it down - that's your backend's target.
Issue: Build fails on linking? Install llvm: rustup component add llvm tools preview. Fixed every time.
| Option | Setup Time | Cost | Best For |
|---|---|---|---|
| AWS Lambda | 5 min | Free tier | Heavy txns |
| Vercel Functions | 3 min | Free hobby | Next.js fans |
| Node Express Local | 1 min | $0 | Testing |
I usually go Vercel. Dead simple. mkdir vercel backend, npm init -y, npm i @solana/web3.js.
api/relay.js (Vercel auto detects):
import { Connection, clusterApiUrl, PublicKey } from '@solana/web3.js'; export default async function handler(req, res) { if (req.method !== 'POST') return res.status(405).end(); const { wallet } = req.body; const connection = new Connection(clusterApiUrl('devnet')); try { const balance = await connection.getBalance(new PublicKey(wallet)); res.json({ balance: balance / 1e9 }); } catch (e) { res.status(500).json({ error: e.message }); }
}
Deploy: npm i -g vercel, vercel deploy. Hit your endpoint: POST {wallet: 'YourPubkeyHere'}. Backend queries Solana. Done.
Potential fuckup: CORS. Vercel handles it. AWS? Add headers manually.
Your backend's useless alone. Quick React app.
SolanaContext.js - like this:
import React, { createContext, useContext, useState, useEffect } from 'react';
import { Connection, clusterApiUrl } from '@solana/web3.js'; const SolanaContext = createContext(); export const useSolana = () => useContext(SolanaContext); export const SolanaProvider = ({ children }) => { const [connection] = useState(new Connection(clusterApiUrl('devnet'), 'confirmed')); return ( <SolanaContext.Provider value={{ connection }}> {children} </SolanaContext.Provider> );
};
App.js:
import { SolanaProvider } from './SolanaContext';
import { WalletAdapterNetwork } from '@solana/wallet adapter base';
import { PhantomWalletAdapter } from '@solana/wallet adapter phantom'; function App() { return ( <SolanaProvider> <div> <h1>Solana Backend Tester</h1> <p>Check your balance via backend!</p> </div> </SolanaProvider> );
} export default App;
Now, button to call backend. Fetch('/api/relay', {method: 'POST', body: JSON.stringify({wallet: publicKey})}). Boom. Full stack.
Backend relaying user txns? Secure way: User signs frontend, sends unsigned to backend? Nah. Backend can't sign user stuff. Use sessions.
Here's a transfer endpoint. Assume you have a service keypair funded.
// In your lambda/handler.js
const { Connection, clusterApiUrl, Keypair, Transaction, SystemProgram, PublicKey } = require('@solana/web3.js'); module.exports.relayTransfer = async (event) => { const { to, amountLamports } = JSON.parse(event.body); const connection = new Connection(clusterApiUrl('devnet')); const payer = Keypair.fromSecretKey(/ your funded secret /); // Secure this! const tx = new Transaction().add( SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: new PublicKey(to), lamports: amountLamports, // e.g. 1000000 = 0.001 SOL }) ); const signature = await connection.sendTransaction(tx, [payer]); return { statusCode: 200, body: JSON.stringify({ sig: signature }) };
};
Deploy. Call from frontend. Fees deducted from payer - about 5000 lamports (0.000005 SOL).
Security? Never hardcode secrets. Use env vars, AWS Secrets Manager. Or better, make users pay their own gas.
Users logging in? Backend verifies.
npm i firebase admin in backend.
Quick setup: Get Firebase project, download service account JSON.
const admin = require('firebase admin');
admin.initializeApp({ credential: admin.credential.cert('serviceAccount.json') }); module.exports.checkUser = async (event) => { const { idToken } = JSON.parse(event.body); try { const decoded = await admin.auth().verifyIdToken(idToken); // Now query Solana for this user return { statusCode: 200, body: JSON.stringify({ user: decoded.uid }) }; } catch (e) { return { statusCode: 401, body: 'Unauthorized' }; }
};
Frontend signs in, sends token to backend. Backend trusts Firebase, queries Solana. Clean.
Devnet lagging? Run localnet.
solana test validatorsolana config set --url http://localhost:8899solana airdrop 10 - unlimited locally.Your backend points to localhost:8899. Tests fly. Deploy to devnet when ready.
That's it. Backend live. In minutes, like promised. I built a token claimer this way last month - relayed 500 txns, zero issues. Tweak for your use case. Hit snags? Common stuff above. Go build.