SDKs

Deployment

Docker, Fly.io, and production deployment

Local Development (Docker Compose)

The docker-compose.yml provides PostgreSQL and Redis for local development:

docker compose up -d          # start in background
docker compose down           # stop
docker compose down -v        # stop + delete volumes (full reset)

Services:

ServiceImagePortPurpose
postgrespostgres:16-alpine127.0.0.1:5432Spend logs, wallet budgets
redisredis:7-alpine127.0.0.1:6379Response cache, replay protection

Warning

Default credentials (solvela / solvela_dev_password) are for local development only. Never use in production.

Ports are bound to 127.0.0.1 only -- not accessible from the network.

Redis is configured with 256MB LRU eviction and no RDB persistence (the cache is ephemeral).

Migrations in migrations/ are mounted into the PostgreSQL container and run automatically on first start.

Docker Build

The Dockerfile uses a 2-stage build. Dependency caching is achieved by copying workspace manifests first and compiling against dummy source files, then bringing in the real source for the final compile:

# Stage 1: Build
FROM rust:1.88-slim-bookworm AS builder
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
WORKDIR /app

# Copy workspace manifests first for dependency caching
COPY Cargo.toml Cargo.lock ./
COPY crates/protocol/Cargo.toml crates/protocol/Cargo.toml
COPY crates/x402/Cargo.toml crates/x402/Cargo.toml
COPY crates/router/Cargo.toml crates/router/Cargo.toml
COPY crates/gateway/Cargo.toml crates/gateway/Cargo.toml
COPY crates/cli/Cargo.toml crates/cli/Cargo.toml

# Compile against dummy sources so deps land in the cache layer
RUN mkdir -p crates/protocol/src crates/x402/src crates/router/src crates/gateway/src crates/cli/src && \
    echo "pub fn _dummy() {}" > crates/protocol/src/lib.rs && \
    echo "pub fn _dummy() {}" > crates/x402/src/lib.rs && \
    echo "pub fn _dummy() {}" > crates/router/src/lib.rs && \
    echo "pub fn _dummy() {}" > crates/gateway/src/lib.rs && \
    echo "fn main() {}" > crates/gateway/src/main.rs && \
    echo "pub fn _dummy() {}" > crates/cli/src/lib.rs && \
    echo "fn main() {}" > crates/cli/src/main.rs
RUN cargo build --release --bin solvela-gateway 2>/dev/null || true

# Copy real source + config + migrations, then rebuild
COPY crates/ crates/
COPY config/ config/
COPY migrations/ migrations/
RUN touch crates/protocol/src/lib.rs crates/x402/src/lib.rs crates/router/src/lib.rs crates/gateway/src/lib.rs crates/gateway/src/main.rs
RUN cargo build --release --bin solvela-gateway

# Stage 2: Runtime
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*

# Create non-root runtime user (no home, no login shell)
RUN useradd --uid 1001 --no-create-home --shell /usr/sbin/nologin solvela

WORKDIR /app
COPY --from=builder --chown=solvela:solvela /app/target/release/solvela-gateway .
COPY --from=builder --chown=solvela:solvela /app/config/ config/
USER solvela
EXPOSE 8402
CMD ["./solvela-gateway"]

Key points:

  • Dependency caching is layer-based: workspace Cargo.tomls + dummy sources compile first; the real source is copied in a later layer so unchanged dependencies don't rebuild.
  • Migration SQL files in migrations/ are read at compile time by sqlx::migrate!, so they must be present in the build context.
  • Runtime image is debian:bookworm-slim with only ca-certificates installed.
  • Exposes port 8402.
  • Runs as non-root user solvela (UID 1001). Files copied from the build stage use --chown=solvela:solvela so the binary and config are owned by the runtime user.

Build the image:

docker build -t solvela .

Run:

docker run -p 8402:8402 \
  -e OPENAI_API_KEY=sk-... \
  -e SOLVELA_SOLANA__RPC_URL=https://api.devnet.solana.com \
  -e SOLVELA_SOLANA__RECIPIENT_WALLET=your-wallet \
  solvela

The canonical env-var separator is double underscore (Fly.io convention; see crates/gateway/src/main.rs:70). The legacy single-underscore form (SOLVELA_SOLANA_RPC_URL) is also accepted as a fallback so existing deployments keep working.

Fly.io Deployment

The gateway is configured for Fly.io with fly.toml:

app = "solvela-gateway"
primary_region = "ord"

[build]
  dockerfile = "Dockerfile"

[env]
  PORT = "8402"
  RUST_LOG = "gateway=info,tower_http=info"

[http_service]
  internal_port = 8402
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 1

  [[http_service.checks]]
    grace_period = "10s"
    interval = "15s"
    method = "GET"
    path = "/health"
    protocol = "http"
    timeout = "5s"

[vm]
  size = "shared-cpu-1x"
  memory = "512mb"

Deploy

# First time
fly launch

# Subsequent deploys
fly deploy

Set Secrets

fly secrets set OPENAI_API_KEY=sk-...
fly secrets set ANTHROPIC_API_KEY=sk-ant-...
fly secrets set SOLVELA_SOLANA__RPC_URL=https://api.mainnet-beta.solana.com
fly secrets set SOLVELA_SOLANA__RECIPIENT_WALLET=your-wallet-pubkey
fly secrets set SOLVELA_SOLANA__FEE_PAYER_KEY=your-fee-payer-key
fly secrets set SOLVELA_ADMIN_TOKEN=your-admin-token
fly secrets set DATABASE_URL=postgres://...
fly secrets set REDIS_URL=redis://...
fly secrets set SOLVELA_SESSION_SECRET=your-session-secret

Health Checks

Fly.io probes GET /health every 15 seconds with a 5-second timeout and 10-second grace period on deploy.

Production Checklist

Before deploying to production:

  • Set SOLVELA_ENV=production (disables localhost CORS origins)
  • Set SOLVELA_ADMIN_TOKEN (protects /metrics, /v1/escrow/health, and the org-management endpoints)
  • Set SOLVELA_SESSION_SECRET to a stable value (auto-generated secret changes on restart)
  • Set SOLVELA_SOLANA__RPC_URL to a mainnet-beta endpoint
  • Configure DATABASE_URL with production PostgreSQL credentials
  • Configure REDIS_URL with production Redis
  • Set SOLVELA_CORS_ORIGINS to your dashboard domain
  • Verify all provider API keys are set
  • Set SOLVELA_SOLANA__RECIPIENT_WALLET to your production wallet
  • Configure fee payer keys if using escrow (SOLVELA_SOLANA__FEE_PAYER_KEY plus optional _2 through _8 for rotation)
  • Run cargo test and cargo clippy before deploying
  • Review RUST_LOG level (avoid debug in production)