A modern Solana backend framework for shipping web2 backends faster
Build production-ready Solana indexers with auto-generated APIs in minutes, not days.
Website β’ Documentation β’ Examples β’ Telegram
This project is currently in active development. APIs, features, and documentation may change without notice. Use at your own risk.
Solder is a comprehensive Solana backend framework that abstracts away the complexity of building blockchain indexers and APIs. It provides:
- π Indexer Abstraction - Monitor Solana programs and events with minimal configuration
- ποΈ Built-in Database Support - Integrated Drizzle ORM with PostgreSQL
- π Auto-generated APIs - RESTful CRUD endpoints created automatically from your schema
- π Cloud Wallet Integration - Optional GCP KMS integration for secure message signing
- π Type-safe - Full TypeScript support with IDL-based type inference
- π Real-time Progress UI - Live terminal interface with performance metrics and health monitoring
- β‘ Fast Development - Go from zero to production-ready backend in minutes
- π₯ Hot Schema Reloading - Automatic database schema synchronization during development
- Node.js >= 18
- npm, pnpm, or yarn
- PostgreSQL database
- Solana RPC endpoint (optional: defaults to public mainnet RPC)
- Realtime streaming endpoint (choose one):
- Yellowstone gRPC endpoint (recommended): Provides low-latency block streaming with better reliability
- WebSocket endpoint: Must support
blockSubscribeRPC method (many providers don't support this)
- Google Cloud Project with KMS (optional: only if using cloud wallets)
The fastest way to get started with Solder is using the create-solder CLI:
npx create-solderThe CLI will:
- Prompt you for a project name
- Ask where you want to create the project
- Ask if you want to install dependencies automatically
- Ask if you want to use GCP Cloud Wallets (optional)
- Set up a complete Solder project with example code
After creation:
cd your-project-name
# Copy and configure environment variables
cp .env.example .env
# Update .env with your RPC URL and database connection string
# Install dependencies (if you skipped during setup)
npm install
# Generate database schema
npm run generate
# Push schema to database
npm run push
# Start the indexer and API server
npm run devSolder now supports optional GCP Cloud Wallets integration for secure message signing using Google Cloud KMS. This feature is perfect for applications that need to sign transactions or messages without exposing private keys.
When you enable GCP Cloud Wallets during project creation, you get:
- Cloud Wallet Dependencies - All necessary packages for GCP KMS integration
- Message Signing API - Ready-to-use HonoJS endpoints for signing messages
- Environment Configuration - Pre-configured environment variables for GCP setup
- Example Implementation - Working code that demonstrates cloud wallet functionality
If you enable cloud wallets, your Solder app will include these endpoints:
# Sign a message with GCP KMS
POST /api/sign-message
{
"message": "Hello, Solana!"
}
# Check cloud wallet status
GET /api/cloud-wallet/statusTo use GCP Cloud Wallets, you'll need:
- Google Cloud Project with KMS enabled
- Service Account with KMS permissions
- Crypto Key created in Google Cloud KMS
- Environment Variables configured in your
.envfile
# Test the message signing endpoint
curl -X POST http://localhost:4000/api/sign-message \
-H "Content-Type: application/json" \
-d '{"message": "Test message for signing"}'
# Check if cloud wallets are enabled
curl http://localhost:4000/api/cloud-wallet/statusWhen cloud wallets are enabled, you'll need to configure these environment variables in your .env file:
# GCP Configuration
GCP_PROJECT_ID=your-project-id
GCP_LOCATION=global
GCP_KEY_RING=your-key-ring
GCP_KEY_NAME=your-key-name
GCP_KEY_VERSION=1
# Authentication (choose one method)
# Method 1: Service Account JSON file
GCP_KEY_FILENAME=./path/to/service-account-key.json
# Method 2: Service Account credentials
# GCP_CLIENT_EMAIL=your-service-account@project.iam.gserviceaccount.com
# GCP_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"Solder includes automatic schema synchronization during development, making it incredibly fast to iterate on your database schema.
When you run pnpm run dev, Solder automatically:
- Watches your
solder.schema.tsfile for changes - Syncs schema changes to your database instantly (using
drizzle-kit push) - Skips migration file generation entirely in development mode
- No manual pushes - Schema changes sync automatically as you save files
- Fast iteration - See your changes reflected immediately
- No migration files - Keep your repo clean during development
- Production-ready - Generate migrations when you're ready to deploy
Just edit your solder.schema.ts file:
// Add a new field to your table
const trades = solderTable(
"trades",
{
id: serial("id").primaryKey(),
mint: varchar("mint", { length: 44 }).notNull(),
// Add this new field - it syncs automatically!
fee: integer("fee").default(0),
// ... other fields
},
// ... options
);Save the file and watch the console - your database updates automatically! π
If you need to disable automatic schema syncing:
You can set enableHotReload: false in your solder.config.ts:
export const solderConfig: SolderConfig = {
db: {
connectionString: process.env.DATABASE_URL ?? "",
},
dev: {
enableHotReload: false,
},
};For production, generate proper migration files:
# Generate migration files
pnpm run generate
# Review and commit the migration files
git add drizzle/
# Apply migrations in production
pnpm run pushImportant: Migration files in the drizzle/ folder are only needed for production deployments. In development, schema changes sync automatically without creating migration files.
The solderTable function is the core building block for defining your database schema with Solder. It extends Drizzle ORM tables with additional metadata for automatic API generation.
import { solderTable } from "@solder-build/core";
import { serial, varchar, timestamp, boolean, text } from "drizzle-orm/pg-core";
const trades = solderTable(
"trades", // Table name
{
// Column definitions (standard Drizzle ORM)
id: serial("id").primaryKey(),
mint: varchar("mint", { length: 44 }).notNull(),
user: varchar("user", { length: 44 }).notNull(),
isBuy: boolean("is_buy").notNull(),
timestamp: timestamp("timestamp", { mode: "date" }).notNull(),
createdAt: timestamp("created_at", { mode: "date" }).defaultNow().notNull(),
},
{
// Solder-specific options
primaryKey: "id",
api: {
basePath: "/trades",
enabled: true,
operations: {
list: true, // GET /trades
read: true, // GET /trades/:id
create: true, // POST /trades
update: false, // PUT /trades/:id (disabled)
delete: false, // DELETE /trades/:id (disabled)
},
},
description: "Trade events from pump.fun",
},
);| Option | Type | Required | Description |
|---|---|---|---|
primaryKey |
string |
Yes | Name of the primary key column |
api |
object |
No | API generation configuration |
description |
string |
No | Table description for documentation |
| Option | Type | Default | Description |
|---|---|---|---|
enabled |
boolean |
true |
Enable/disable API generation for this table |
basePath |
string |
"/{tableName}" |
Base path for API endpoints |
operations.list |
boolean |
true |
Enable GET /basePath (list with query support) |
operations.read |
boolean |
true |
Enable GET /basePath/:id (read one) |
operations.create |
boolean |
true |
Enable POST /basePath (create) |
operations.update |
boolean |
true |
Enable PUT /basePath/:id (update) |
operations.delete |
boolean |
true |
Enable DELETE /basePath/:id (delete) |
The LIST operation supports powerful PostgREST-style query parameters for filtering, sorting, pagination, and field selection.
Important: All query parameters use database column names (snake_case like is_buy, sol_amount), not schema property names (camelCase like isBuy, solAmount).
Filter results by adding query parameters with operator syntax: field=operator.value
# Get trades where is_buy equals true
GET /trades?is_buy=eq.true
# Get trades where sol_amount is greater than 1000
GET /trades?sol_amount=gt.1000
# Multiple filters (AND condition by default)
GET /trades?is_buy=eq.true&sol_amount=gt.1000Solder supports the following comparison operators:
| Operator | Description | Example |
|---|---|---|
eq |
Equal | ?amount=eq.100 |
neq |
Not equal | ?amount=neq.0 |
gt |
Greater than | ?amount=gt.1000 |
gte |
Greater than or equal | ?amount=gte.1000 |
lt |
Less than | ?amount=lt.500 |
lte |
Less than or equal | ?amount=lte.500 |
Type Handling: The API automatically converts values to the appropriate type (booleans, numbers, strings, null).
OR Conditions:
# Get trades where is_buy is true OR sol_amount is greater than 5000
GET /trades?or=(is_buy.eq.true,sol_amount.gt.5000)AND Conditions (Default):
# Get trades where is_buy is true AND sol_amount is greater than 1000
GET /trades?is_buy=eq.true&sol_amount=gt.1000Sort results by specifying a field and direction:
# Sort by timestamp descending (newest first)
GET /trades?order=timestamp.desc
# Sort by amount ascending
GET /trades?order=sol_amount.ascControl the number of results and starting position:
# Get first 10 results
GET /trades?limit=10
# Skip first 20 results, then get 10
GET /trades?limit=10&offset=20
# Page 3 with 25 items per page
GET /trades?limit=25&offset=50Return only specific fields instead of all columns:
# Return only mint, user, and timestamp fields
GET /trades?select=mint,user,timestamp
# Return only the amount fields
GET /trades?select=sol_amount,token_amountNote: Field names must match the database column names (snake_case), not the schema property names.
All query parameters can be combined for powerful queries:
# Complex query: Filter, sort, paginate, and select fields
GET /trades?is_buy=eq.true&sol_amount=gt.1000&order=timestamp.desc&limit=10&select=mint,user,sol_amount,timestamp
# OR condition with sorting and pagination
GET /trades?or=(is_buy.eq.true,sol_amount.gt.5000)&order=sol_amount.desc&limit=20
# Filter by date range with field selection
GET /trades?timestamp=gte.2024-01-01×tamp=lt.2024-02-01&select=mint,timestamp,sol_amount# Get recent buys over a certain amount
GET /trades?is_buy=eq.true&sol_amount=gt.10&order=timestamp.desc&limit=50
# Find all trades for a specific mint
GET /trades?mint=eq.abc123...&order=timestamp.desc
# Get large trades (buy or sell)
GET /trades?or=(sol_amount.gt.100,token_amount.gt.1000000)&order=sol_amount.desc
# Monitor recent activity
GET /trades?order=timestamp.desc&limit=100&select=mint,user,is_buy,sol_amount,timestampIf a query is malformed or references invalid fields, the API will return a 400 error with details:
{
"error": "Unknown field: invalid_field_name"
}If no query parameters are provided, the API behaves as before and returns all records:
# Returns all trades (no filtering)
GET /tradesAfter defining your tables, use solderSchema to build the final schema:
import { solderSchema } from "@solder-build/core";
const built = solderSchema(trades, users, tokens);
// Export for Drizzle Kit
export const tradesTable = trades.table;
export const usersTable = users.table;
// Export for application use
export const schema = built.schema;
export const tables = built.tables;
export type AppSchema = typeof schema;The Indexer class is the core component for monitoring Solana blockchain events and processing them into your database.
import { Indexer } from "@solder-build/core";
// Example with gRPC streaming (recommended)
const indexer = new Indexer({
startBlock: 300000000, // Starting slot number
rpcUrl: process.env.RPC_URL, // Solana RPC endpoint
databaseUrl: process.env.DATABASE_URL, // PostgreSQL connection string
cursorKey: "my-indexer", // Unique identifier for this indexer
enableUIProgress: true, // Enable real-time progress UI
grpcUrl: process.env.GRPC_URL, // Yellowstone gRPC endpoint
grpcToken: process.env.GRPC_TOKEN, // Optional: gRPC authentication token
});
// Example with WebSocket streaming (requires blockSubscribe support)
const indexerWS = new Indexer({
startBlock: 300000000,
rpcUrl: process.env.RPC_URL,
databaseUrl: process.env.DATABASE_URL,
cursorKey: "my-indexer",
enableUIProgress: true,
wsUrl: process.env.WS_URL, // WebSocket endpoint (must support blockSubscribe)
});| Option | Type | Required | Description |
|---|---|---|---|
startBlock |
number |
Yes | Slot number to start indexing from |
rpcUrl |
string |
Yes | Solana RPC endpoint URL |
databaseUrl |
string |
No | PostgreSQL connection string (required for persistence) |
cursorKey |
string |
No | Unique key for cursor storage (defaults to "default") |
enableUIProgress |
boolean |
No | Enable real-time progress UI (defaults to false) |
grpcUrl |
string |
Yes* | Yellowstone gRPC endpoint for streaming (required if wsUrl not provided) |
grpcToken |
string |
No | Optional authentication token for gRPC endpoint |
wsUrl |
string |
Yes* | WebSocket endpoint for streaming (required if grpcUrl not provided) |
logger |
Logger |
No | Custom logger instance (defaults to console logger) |
* Either grpcUrl or wsUrl must be provided, but not both.
Important: The wsUrl option requires a WebSocket endpoint that supports the blockSubscribe RPC method. Many RPC providers (including Helius) do not support this method, which will cause the indexer to fail with a "Method not found" error.
If WebSocket initialization fails, the indexer will stop immediately with an error message. There is no automatic fallback to HTTP polling.
Recommendation: Use grpcUrl with a Yellowstone gRPC endpoint for more reliable streaming. Yellowstone gRPC provides:
- Lower latency than WebSocket
- More efficient data handling
- Better reliability and reconnection handling
- Direct block data without additional RPC calls
Example error when WebSocket doesn't support blockSubscribe:
[ERROR] Failed to initialize websocket channel: Error: Method not found
If you encounter this error, switch to using grpcUrl instead of wsUrl.
Use the onEvent method to register handlers for specific program events:
import { type Idl } from "@coral-xyz/anchor";
import pumpFunIdl from "./idls/pump-fun.json";
await indexer.onEvent({
programId: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
idl: pumpFunIdl as Idl,
eventName: "TradeEvent",
handler: async (event, db) => {
// Process the event
await db.insert(tradesTable).values({
mint: event.parsed.mint.toBase58(),
user: event.parsed.user.toBase58(),
isBuy: event.parsed.isBuy,
timestamp: new Date(Number(event.parsed.timestamp) * 1000),
});
},
});type EventHandler<TIdl, TEventName> = (
event: IndexerEvent<TIdl, TEventName>,
db: NodePgDatabase,
) => Promise<void> | void;
interface IndexerEvent {
name: string; // Event name
contract: string; // Program ID
type: string; // Event type
parsed: any; // Parsed event data (typed based on IDL)
timestamp: string; // Event timestamp
transaction: {
hash: string; // Transaction signature
slot: number; // Slot number
blockTime: number; // Block timestamp
};
programId: string; // Program ID
eventName: string; // Event name
}// Start the indexer
await indexer.start();
// Stop the indexer
indexer.stop();const status = indexer.getStatus();
console.log(status);
// {
// isRunning: true,
// currentSlot: 300000042,
// registeredPrograms: 1,
// eventHandlers: 1
// }When enableUIProgress: true is set, Solder provides a real-time terminal UI that displays:
- Chain Status: Current blockchain status and sync progress
- Indexing Stats: Real-time event processing statistics with RPS (requests per second)
- Event Table: Live view of processed events with counts and performance metrics
- Progress Bar: Visual progress indicator with ETA calculations
- Health Monitoring: Database, realtime stream (gRPC/WebSocket), and RPC connection status
The progress UI automatically updates in place, providing a clean development experience without cluttering your terminal output.
const indexer = new Indexer({
// ... other options
enableUIProgress: true, // Enables the real-time progress UI
});Features:
- Live terminal updates without scrolling
- Performance metrics (RPS, average processing time)
- Health status indicators
- Progress tracking with ETA
- Event processing statistics
- Responsive design that adapts to terminal width
import { Indexer } from "@solder-build/core";
import { tradesTable } from "./schema";
import pumpFunIdl from "./idls/pump-fun.json";
export const initializeIndexer = async () => {
const indexer = new Indexer({
startBlock: 300000000,
rpcUrl: process.env.RPC_URL || "https://api.mainnet-beta.solana.com",
databaseUrl: process.env.DATABASE_URL,
cursorKey: "pump-fun-indexer",
enableUIProgress: true,
// Use gRPC for reliable streaming (recommended)
grpcUrl: process.env.GRPC_URL,
grpcToken: process.env.GRPC_TOKEN, // Optional
// Or use WebSocket (only if your provider supports blockSubscribe)
// wsUrl: process.env.WS_URL,
});
await indexer.onEvent({
programId: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
idl: pumpFunIdl,
eventName: "TradeEvent",
handler: async (event, db) => {
await db.insert(tradesTable).values({
mint: event.parsed.mint.toBase58(),
solAmount: event.parsed.solAmount.toString(),
tokenAmount: event.parsed.tokenAmount.toString(),
isBuy: event.parsed.isBuy,
user: event.parsed.user.toBase58(),
virtualSolReserves: event.parsed.virtualSolReserves.toString(),
virtualTokenReserves: event.parsed.virtualTokenReserves.toString(),
timestamp: new Date(Number(event.parsed.timestamp) * 1000),
});
},
});
await indexer.start();
return indexer;
};Solder is built on a modern, modular architecture that combines several best-in-class technologies:
- Indexer Layer - Monitors Solana blockchain for program events
- Schema Layer - Type-safe database schema with Drizzle ORM
- API Layer - Auto-generated RESTful APIs with Hono
- RPC Layer - Efficient Solana RPC client for block and transaction data
For a detailed explanation of Solder's architecture, features, and design philosophy, please visit our comprehensive documentation:
π Solder Documentation
This Turborepo includes the following packages and apps:
apps/docs- Documentation website (Next.js)apps/web- Marketing website (Next.js)apps/example-app- Example Solder application indexing pump.fun trades
packages/core- Core Solder framework (solder)packages/cli- CLI for scaffolding projects (create-solder)@repo/ui- Shared React component library@repo/eslint-config- Shared ESLint configurations@repo/typescript-config- Shared TypeScript configurations
# Build all packages
pnpm build
# Run all apps in development mode
pnpm dev
# Lint all packages
pnpm lint
# Format code
pnpm format
# Create a new Solder app
pnpm create-appStart building production-ready Solana backends today
Follow us on X/Twitter β’ Check out legends.fun
This repo includes a high-performance gRPC indexer implemented with a Rust native addon and Yellowstone Vixen streaming.
- Node.js 18+ and pnpm
- Rust toolchain + Cargo
- PostgreSQL (local or Docker)
- Yellowstone Fumarole gRPC endpoint and X-Token
pnpm installFrom the repo root:
pnpm --filter @solder-build/core build:native
pnpm --filter @solder-build/core build:ts
# Or one shot:
pnpm --filter @solder-build/core builddocker run --name solder-pg \
-e POSTGRES_PASSWORD=password123 \
-e POSTGRES_DB=app \
-p 6500:5432 \
-d postgres:16Set your connection string:
export DATABASE_URL="postgresql://postgres:password123@127.0.0.1:6500/app"export GRPC_ENDPOINT="https://your-fumarole-endpoint.com"
export GRPC_TOKEN="your-x-token"Note: The example at packages/core/src/examples/grpc-indexer.ts currently hardcodes values for grpcEndpoint, xToken, and databaseUrl. For production usage, change those to read from process.env.* or supply your values before building.
node packages/core/dist/examples/grpc-indexer.jsYou should see:
- Event handler registration
- Monitored programs and handler counts
- Streaming logs as events arrive
Stop with Ctrl+C; graceful shutdown is handled.
- Missing
index.node: rerun the native build step (build:native). - DB connection errors: verify
DATABASE_URLmatches your Postgres host/port. - Auth errors: confirm
GRPC_TOKENis valid and paired with the same endpoint.
