A production-quality Rust backend for StellarSend — a global money-transfer application built on the Stellar network.
- Tech Stack
- Architecture
- Getting Started
- Environment Variables
- Database Setup
- Stellar Integration Notes
- API Reference
- Differentiator Features
- Error Handling
- Deployment Guide
- Docker
| Concern | Crate |
|---|---|
| HTTP framework | axum 0.7 |
| Async runtime | tokio |
| Database | sqlx 0.7 + PostgreSQL |
| HTTP client | reqwest |
| Serialisation | serde / serde_json |
| Authentication | jsonwebtoken + bcrypt |
| Middleware | tower-http (CORS, tracing, request-id, timeout) |
| Structured logging | tracing + tracing-subscriber |
| Error handling | thiserror + anyhow |
| IDs | uuid v4 |
| Timestamps | chrono |
src/
├── main.rs # App startup: DB pool, router, bind
├── config.rs # Config struct loaded from env
├── error.rs # AppError enum → HTTP responses
├── db.rs # Pool setup + migration runner
├── models/
│ ├── user.rs # User, UserRow, JwtClaims, CreateUserRequest, LoginRequest
│ ├── transaction.rs # Transaction, TransactionStatus, CreateTransaction, pagination
│ └── payment.rs # Asset, QuoteRequest/Response, SendPaymentRequest, PaymentResult
├── routes/
│ ├── mod.rs # Router assembly
│ ├── auth.rs # POST /api/auth/register, /login
│ ├── payments.rs # POST /api/payments/quote, /send — GET /api/payments/:id
│ ├── transactions.rs # GET /api/transactions (paginated), /transactions/:id
│ ├── accounts.rs # GET /api/accounts/:address, /:address/balances
│ └── rates.rs # GET /api/rates?from=&to=
├── services/
│ ├── stellar.rs # Horizon API wrapper
│ ├── payment.rs # Quote + send business logic
│ ├── transaction.rs # DB CRUD for transactions
│ └── rate.rs # Rate fetching with in-memory TTL cache
└── middleware/
├── auth.rs # JWT extractor (AuthUser), issue_jwt, decode_jwt
└── cors.rs # CORS layer builder
migrations/
├── 001_initial.sql # users + transactions tables
└── 002_add_indices.sql # Performance indices
- Rust >= 1.75 (
rustup update stable) - PostgreSQL >= 14
- sqlx-cli for running migrations outside of the app:
cargo install sqlx-cli --no-default-features --features rustls,postgres
git clone https://github.com/your-org/stellarsend
cd stellarsend/backend
cp .env.example .env
# Edit .env with your DATABASE_URL, JWT_SECRET, etc.createdb stellarsend
# Or with psql:
psql -c "CREATE DATABASE stellarsend;"Migrations run automatically on startup, or manually via:
sqlx migrate run --database-url postgres://user:pass@localhost/stellarsendcargo run
# Development with auto-reload:
cargo install cargo-watch
cargo watch -x runThe server binds to http://0.0.0.0:8080 by default.
Copy .env.example to .env and adjust all values before running.
| Variable | Default | Description |
|---|---|---|
PORT |
8080 |
HTTP listen port |
HOST |
0.0.0.0 |
HTTP bind address |
RUST_LOG |
info |
Log level filter (error, warn, info, debug, trace) |
DATABASE_URL |
— | PostgreSQL connection string (required) |
DATABASE_MAX_CONNECTIONS |
20 |
PgPool max connections |
DATABASE_MIN_CONNECTIONS |
2 |
PgPool min connections |
DATABASE_CONNECT_TIMEOUT_SECS |
30 |
Connection acquisition timeout |
JWT_SECRET |
— | HMAC-SHA256 signing secret, min 32 chars (required) |
JWT_EXPIRY_HOURS |
24 |
Token lifetime in hours |
HORIZON_URL |
https://horizon-testnet.stellar.org |
Stellar Horizon base URL |
STELLAR_NETWORK_PASSPHRASE |
testnet passphrase | Used for XDR verification |
RATE_CACHE_TTL_SECS |
60 |
Exchange-rate cache TTL |
ALLOWED_ORIGINS |
* |
Comma-separated CORS origins, or * for all |
APP_ENV |
development |
development or production |
users
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | gen_random_uuid() |
email |
TEXT UNIQUE | Lowercase-normalised on insert |
password_hash |
TEXT | bcrypt |
full_name |
TEXT | |
stellar_address |
TEXT nullable | Stellar G… public key |
is_active |
BOOLEAN | Default true |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ | Auto-set by trigger |
transactions
| Column | Type | Notes |
|---|---|---|
id |
UUID PK | |
user_id |
UUID FK → users | Cascade delete |
stellar_tx_hash |
TEXT nullable UNIQUE | Set after Horizon submission |
from_asset |
TEXT | e.g. XLM or USDC:GA5Z… |
to_asset |
TEXT | |
send_amount |
TEXT | |
receive_amount |
TEXT nullable | Filled after execution |
source_account |
TEXT | Stellar public key |
destination_account |
TEXT | |
status |
ENUM | pending → submitted → completed / failed |
error_message |
TEXT nullable | Horizon error detail on failure |
fee_xlm |
TEXT nullable | |
path |
JSONB nullable | Asset path hops |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
# Apply all pending migrations
sqlx migrate run --database-url "$DATABASE_URL"
# Roll back the last migration
sqlx migrate revert --database-url "$DATABASE_URL"
# Check migration status
sqlx migrate info --database-url "$DATABASE_URL"Switch between networks by changing two env vars:
# Testnet (default)
HORIZON_URL=https://horizon-testnet.stellar.org
STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015
# Mainnet
HORIZON_URL=https://horizon.stellar.org
STELLAR_NETWORK_PASSPHRASE=Public Global Stellar Network ; September 2015-
Quote — the client calls
POST /api/payments/quotewith the send/receive assets and amount. The backend queries Horizon's/paths/strict-sendendpoint to find the best DEX path and returns an estimated receive amount, implied rate, fee, and path array. -
Client-side signing — the client uses any Stellar SDK (JavaScript, Flutter, etc.) to build a
PathPaymentStrictSendoperation, sign it with the user's secret key, and base64-encode theTransactionEnvelopeXDR. -
Submit — the client posts the signed XDR to
POST /api/payments/send. The backend relays it to Horizon and records the outcome (hash, ledger, fee) in the database.
Assets are passed as JSON objects:
{ "code": "XLM", "issuer": null }
{ "code": "USDC", "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" }For query parameters (e.g. /api/rates), pass XLM or CODE:ISSUER.
The rate service queries Horizon's path-payment endpoint with a send amount of 1 and reads the best destination amount. Results are cached in-process with a configurable TTL. For production workloads consider replacing the fallback with a dedicated price oracle (CoinGecko, Stellar Expert, etc.).
All responses share this envelope:
{ "success": true, "data": { … } }
{ "success": false, "error": { "code": "ERROR_CODE", "message": "Human-readable detail" } }Lightweight liveness probe. No authentication required.
curl http://localhost:8080/health{
"success": true,
"data": { "status": "ok", "service": "stellarsend-backend" }
}Create a new user account and receive a JWT.
Request
curl -X POST http://localhost:8080/api/auth/register \
-H 'Content-Type: application/json' \
-d '{
"email": "alice@example.com",
"password": "supersecret123",
"full_name": "Alice Doe",
"stellar_address": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN"
}'Response 201
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"user": {
"id": "01920c1a-0000-7000-8000-000000000001",
"email": "alice@example.com",
"full_name": "Alice Doe",
"stellar_address": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"is_active": true,
"created_at": "2024-01-15T10:00:00Z"
}
}
}Error codes
| Code | Meaning |
|---|---|
VALIDATION_ERROR |
Missing field or invalid format |
CONFLICT |
Email already registered |
Authenticate with email + password and receive a JWT.
Request
curl -X POST http://localhost:8080/api/auth/login \
-H 'Content-Type: application/json' \
-d '{
"email": "alice@example.com",
"password": "supersecret123"
}'Response 200
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
"user": {
"id": "01920c1a-0000-7000-8000-000000000001",
"email": "alice@example.com",
"full_name": "Alice Doe",
"stellar_address": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"is_active": true,
"created_at": "2024-01-15T10:00:00Z"
}
}
}Error codes
| Code | Meaning |
|---|---|
INVALID_CREDENTIALS |
Wrong email or password |
All payment endpoints require Authorization: Bearer <token>.
Get the best path-payment quote from the Stellar DEX without committing anything.
Request
curl -X POST http://localhost:8080/api/payments/quote \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"from_asset": { "code": "USDC", "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" },
"to_asset": { "code": "XLM", "issuer": null },
"amount": "100",
"destination": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
}'Response 200
{
"success": true,
"data": {
"send_amount": "100",
"estimated_receive": "909.0909090",
"rate": "9.0909090",
"fee_xlm": "0.0000100",
"from_asset": { "code": "USDC", "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" },
"to_asset": { "code": "XLM", "issuer": null },
"path": []
}
}Error codes
| Code | Meaning |
|---|---|
NO_PATH_FOUND |
No DEX path exists between the two assets |
BAD_REQUEST |
Invalid amount |
HORIZON_ERROR |
Horizon returned an error |
Submit a client-signed Stellar transaction XDR. The client is responsible for building and signing the transaction using any Stellar SDK before calling this endpoint.
Workflow
- Call
/api/payments/quoteto get the path. - Build a
PathPaymentStrictSendtransaction in your Stellar SDK. - Sign with the user's Stellar secret key.
- Base64-encode the
TransactionEnvelopeXDR. - POST to this endpoint.
Request
curl -X POST http://localhost:8080/api/payments/send \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"source_account": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"signed_xdr": "AAAAAgAAAABiXst2pnqBtsr…AAAAAAAAAAA=",
"from_asset": { "code": "USDC", "issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN" },
"to_asset": { "code": "XLM", "issuer": null },
"send_amount": "100",
"destination_account": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5"
}'Response 200
{
"success": true,
"data": {
"tx_hash": "a1b2c3d4e5f6…",
"success": true,
"ledger": 48293011,
"fee_charged": "100",
"transaction_id": "01920c1a-0000-7000-8000-000000000042"
}
}Error codes
| Code | Meaning |
|---|---|
TRANSACTION_FAILED |
Horizon rejected the XDR |
BAD_REQUEST |
Empty or malformed XDR |
HORIZON_ERROR |
Network-level Horizon error |
Retrieve an internal payment record plus live Stellar data (when a hash is available).
curl http://localhost:8080/api/payments/01920c1a-0000-7000-8000-000000000042 \
-H 'Authorization: Bearer <token>'Response 200
{
"success": true,
"data": {
"payment": {
"id": "01920c1a-0000-7000-8000-000000000042",
"user_id": "01920c1a-0000-7000-8000-000000000001",
"stellar_tx_hash": "a1b2c3d4e5f6…",
"from_asset": "USDC:GA5Z…",
"to_asset": "XLM:native",
"send_amount": "100",
"receive_amount": null,
"source_account": "GAAZI4…",
"destination_account": "GBBD47…",
"status": "completed",
"fee_xlm": "0.0000100",
"path": [],
"created_at": "2024-01-15T10:05:00Z",
"updated_at": "2024-01-15T10:05:03Z"
},
"stellar": {
"id": "a1b2c3d4e5f6…",
"hash": "a1b2c3d4e5f6…",
"ledger": 48293011,
"created_at": "2024-01-15T10:05:01Z",
"source_account": "GAAZI4…",
"fee_charged": "100",
"successful": true
}
}
}All transaction endpoints require Authorization: Bearer <token>.
Return a paginated list of the authenticated user's transactions with optional filters.
Query parameters
| Parameter | Type | Description |
|---|---|---|
status |
string | Filter: pending, submitted, completed, failed, expired |
from_asset |
string | Filter by source asset |
to_asset |
string | Filter by destination asset |
page |
integer | Page number (default: 1) |
per_page |
integer | Results per page (default: 20, max: 100) |
# All transactions, newest first
curl 'http://localhost:8080/api/transactions' \
-H 'Authorization: Bearer <token>'
# Only completed, page 2
curl 'http://localhost:8080/api/transactions?status=completed&page=2&per_page=10' \
-H 'Authorization: Bearer <token>'Response 200
{
"success": true,
"data": {
"items": [ { "id": "…", "status": "completed", "…": "…" } ],
"total": 42,
"page": 1,
"per_page": 20,
"total_pages": 3
}
}Retrieve a single transaction by its internal UUID.
curl http://localhost:8080/api/transactions/01920c1a-0000-7000-8000-000000000042 \
-H 'Authorization: Bearer <token>'Response 200
{
"success": true,
"data": {
"id": "01920c1a-0000-7000-8000-000000000042",
"user_id": "01920c1a-0000-7000-8000-000000000001",
"stellar_tx_hash": "a1b2c3…",
"from_asset": "XLM",
"to_asset": "USDC:GA5Z…",
"send_amount": "50",
"receive_amount": "5.5",
"source_account": "GAAZI4…",
"destination_account": "GBBD47…",
"status": "completed",
"error_message": null,
"fee_xlm": "0.0000100",
"path": [],
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:00:03Z"
}
}Error codes
| Code | Meaning |
|---|---|
NOT_FOUND |
No transaction with that id |
FORBIDDEN |
Transaction belongs to a different user |
All account endpoints require Authorization: Bearer <token>.
Fetch the full Horizon account record for a Stellar public key.
curl http://localhost:8080/api/accounts/GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN \
-H 'Authorization: Bearer <token>'Response 200
{
"success": true,
"data": {
"id": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"account_id": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"sequence": "175420452143",
"subentry_count": 2,
"balances": [
{ "balance": "9999.9999900", "asset_type": "native", "asset_code": "", "asset_issuer": "" },
{ "balance": "100.0000000", "asset_type": "credit_alphanum4", "asset_code": "USDC", "asset_issuer": "GA5Z…", "limit": "922337203685.4775807" }
],
"thresholds": { "low_threshold": 0, "med_threshold": 0, "high_threshold": 0 },
"flags": { "auth_required": false, "auth_revocable": false, "auth_immutable": false },
"last_modified_ledger": 48291000,
"last_modified_time": "2024-01-15T09:50:00Z"
}
}Return only the balance array — useful for quick wallet display.
curl http://localhost:8080/api/accounts/GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN/balances \
-H 'Authorization: Bearer <token>'Response 200
{
"success": true,
"data": {
"address": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"balances": [
{ "balance": "9999.9999900", "asset_type": "native", "asset_code": "", "asset_issuer": "" },
{ "balance": "100.0000000", "asset_type": "credit_alphanum4", "asset_code": "USDC", "asset_issuer": "GA5Z…", "limit": "…" }
]
}
}Error codes
| Code | Meaning |
|---|---|
BAD_REQUEST |
Address is not a valid 56-char G… key |
NOT_FOUND |
Account does not exist on Stellar |
No authentication required.
Returns the current exchange rate between two assets, sourced from the Stellar DEX and cached server-side.
Asset format for query params
- Native XLM:
XLM - Issued asset:
CODE:ISSUER_PUBLIC_KEY
# XLM to USDC
curl 'http://localhost:8080/api/rates?from=XLM&to=USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN'
# USDC to XLM
curl 'http://localhost:8080/api/rates?from=USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN&to=XLM'Response 200
{
"success": true,
"data": {
"from": "XLM",
"to": "USDC:GA5Z…",
"rate": 0.11,
"timestamp": "2024-01-15T10:00:00Z"
}
}These sit on top of the on-chain building blocks in StellarSend/contracts — this layer handles persistence, history, and (where a scheduled action is needed) submitting already-authorized on-chain calls. Every fund-moving path still follows the non-custodial pattern above: the client signs with its own wallet, and this backend either relays the signed XDR or, for subscriptions only, a keeper service account submits a call the payer pre-authorized on-chain.
migrations/006_add_subscriptions.sql — mirrors stellar_send::subscription.
| Method | Path | Notes |
|---|---|---|
POST |
/api/subscriptions |
Create a subscription record |
GET |
/api/subscriptions |
List the caller's subscriptions |
GET |
/api/subscriptions/:id |
Get one |
POST |
/api/subscriptions/:id/cancel |
Cancel |
A background tokio task (toggle with KEEPER_ENABLED) — plus a manual POST /api/keeper/run-subscriptions trigger — finds subscriptions due for execution and submits execute_subscription via the keeper. A subscription is marked failed after 3 consecutive on-chain failures rather than retried forever.
migrations/009_add_batch_payments.sql adds batch_id/batch_index to transactions.
| Method | Path | Notes |
|---|---|---|
POST |
/api/payments/batch |
Relay one client-signed multi-recipient transaction; records one transactions row per leg |
migrations/007_add_payment_requests.sql.
| Method | Path | Notes |
|---|---|---|
POST |
/api/payment-requests |
Create a request, returns a shareable id |
GET |
/api/payment-requests |
List the caller's requests |
GET |
/api/payment-requests/:id |
Public lookup (for the payer's UI) |
POST |
/api/payment-requests/:id/fulfill |
Fulfill (client-signed, same as a regular send) |
POST |
/api/payment-requests/:id/cancel |
Cancel |
migrations/008_add_escrows.sql.
| Method | Path | Notes |
|---|---|---|
POST |
/api/escrows |
Record an escrow the client has funded via its own signed create_escrow call |
GET |
/api/escrows |
List escrows the caller is party to |
GET |
/api/escrows/:id |
Get one (open to depositor/beneficiary/arbiter — they may not all be StellarSend accounts) |
POST |
/api/escrows/:id/release/build |
Build an unsigned release_escrow invocation sourced from the caller's own account |
POST |
/api/escrows/:id/release |
Relay the caller's signed release envelope |
POST |
/api/escrows/:id/refund/build |
Build an unsigned refund_escrow invocation |
POST |
/api/escrows/:id/refund |
Relay the caller's signed refund envelope |
Why release/refund are client-signed, not keeper-executed: the contract's release_escrow/refund_escrow require caller.require_auth() — only the actual beneficiary, depositor, or arbiter can authorize the call. A keeper service account can't sign on their behalf, so unlike subscriptions (which use a pre-granted token allowance) there's no sound way to automate this for a party the keeper isn't itself. The /build step returns an unsigned invocation for whichever party is acting to sign client-side, exactly like a regular payment.
src/services/soroban.rs implements a genuine Soroban RPC client: it builds, simulates, signs (with the keeper's own ed25519 key, configured via KEEPER_SECRET_KEY — never a user's), and submits invoke_host_function calls. The keeper only ever pays its own network fee; it can't move user funds beyond what the contract itself, pre-authorized by the user on-chain, allows. Due subscriptions are selected with FOR UPDATE SKIP LOCKED so it's safe to run more than one backend instance.
Known gap: the contracts, backend, and frontend for these four features were built in parallel against a shared spec rather than a single locked API contract. The escrow release/refund auth model above has been reconciled and fixed, but endpoint payload casing and a few paths (e.g. exact batch/subscription URLs) may still need a pass to line up frontend and backend exactly — check
src/lib/api.tsin the frontend repo againstsrc/routes/mod.rshere before wiring up a new client.
Every error response follows the same envelope:
{
"success": false,
"error": {
"code": "MACHINE_READABLE_CODE",
"message": "Human-readable description"
}
}| HTTP | Code | Trigger |
|---|---|---|
| 400 | BAD_REQUEST |
Malformed request body / params |
| 401 | UNAUTHORIZED |
Missing or invalid Authorization header |
| 401 | INVALID_CREDENTIALS |
Wrong email or password |
| 401 | TOKEN_EXPIRED |
JWT past its exp claim |
| 401 | INVALID_TOKEN |
JWT signature invalid or malformed |
| 403 | FORBIDDEN |
Authenticated but not allowed |
| 404 | NOT_FOUND |
Resource not found |
| 409 | CONFLICT |
Resource already exists |
| 422 | VALIDATION_ERROR |
Field-level validation failure |
| 422 | NO_PATH_FOUND |
No DEX path for the requested asset pair |
| 422 | TRANSACTION_FAILED |
Stellar transaction rejected by network |
| 502 | HORIZON_ERROR |
Horizon API returned an error |
| 500 | INTERNAL_ERROR |
Unexpected server error (detail logged, not exposed) |
- Set
APP_ENV=production - Use a strong, random
JWT_SECRET(at least 64 random bytes, base64-encoded) - Point
HORIZON_URLathttps://horizon.stellar.org - Update
STELLAR_NETWORK_PASSPHRASEto the mainnet passphrase - Set
ALLOWED_ORIGINSto your frontend domain(s) — never*in production - Use a managed PostgreSQL instance (AWS RDS, Supabase, etc.) and set
DATABASE_URL - Run behind a TLS-terminating reverse proxy (nginx, Caddy, AWS ALB)
- Set
RUST_LOG=warnorRUST_LOG=infoto reduce log volume
cargo build --release
./target/release/stellarsendfly launch --name stellarsend-api
fly secrets set DATABASE_URL="postgres://…"
fly secrets set JWT_SECRET="$(openssl rand -hex 64)"
fly secrets set HORIZON_URL="https://horizon.stellar.org"
fly deployPush to GitHub and link the repo in Railway. Set the same env vars via the Railway dashboard. The Cargo.toml is auto-detected.
# ─── Build stage ──────────────────────────────────────────────────────────────
FROM rust:1.75-slim-bookworm AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y pkg-config libssl-dev && rm -rf /var/lib/apt/lists/*
COPY Cargo.toml Cargo.lock ./
COPY src ./src
COPY migrations ./migrations
RUN cargo build --release
# ─── Runtime stage ────────────────────────────────────────────────────────────
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates libssl3 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/stellarsend /app/stellarsend
COPY migrations ./migrations
EXPOSE 8080
CMD ["/app/stellarsend"]version: "3.9"
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: stellarsend
POSTGRES_PASSWORD: password
POSTGRES_DB: stellarsend
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
api:
build: .
ports:
- "8080:8080"
environment:
DATABASE_URL: postgres://stellarsend:password@db:5432/stellarsend
JWT_SECRET: dev-secret-change-in-production-must-be-32-chars-minimum
HORIZON_URL: https://horizon-testnet.stellar.org
STELLAR_NETWORK_PASSPHRASE: "Test SDF Network ; September 2015"
RUST_LOG: debug
APP_ENV: development
depends_on:
- db
volumes:
pgdata:# Start everything
docker-compose up -d
# Tail logs
docker-compose logs -f api
# Stop
docker-compose downMIT