Transaction Science open standards monorepo. Each subdirectory is a self-contained reference protocol with its own README and licence. - openpay/ Apache-2.0 payment-acceptance stack (Rust, 26 crates) - smart-byte/ CC-BY-4.0 content-addressed value carrier (spec) - eoc/ CC-BY-4.0 energy-optimised compute (spec) The wire format and the right to fork are public. The optional hosted services are the offer; the protocols are not.
8.3 KiB
Deploying OpenPay
Operator-side guide for running op-server in production. Read top
to bottom — each section assumes the previous one. The shipped
binary (crates/op-server/src/main.rs) is intentionally minimal: it
reads two env vars (OP_BIND_ADDR, RUST_LOG) and serves the
default in-memory AppState. Everything else — persistence, auth,
webhooks, FX, crypto rails — is wired by either editing
crates/op-server/src/main.rs or building your own binary around
the exported op_server::router / op_server::router_with_middleware
functions. The library APIs ship and are stable; the binary is a
reference entry point you are expected to extend.
Companion files in this directory:
| File | Purpose |
|---|---|
quickstart.md |
Five-minute clone → build → smoke-test path. |
Caddyfile.sample |
TLS-terminating reverse proxy in front of op-server. |
openpay.service |
systemd unit. |
openpay.env.sample |
Env-file template; covers every var the binary reads (and the ones your custom binary may add). |
backup.md |
Single-file .graph backup + restore. |
monitoring.md |
tracing spans, log format, and what to dashboard. |
1. Hello, world
The shipped binary listens on 127.0.0.1:8080 and holds state in
memory. No persistence, no auth, no TLS. Suitable for a smoke test.
cargo build --release -p op-server
OP_BIND_ADDR=127.0.0.1:8080 ./target/release/op-server
In another shell:
curl http://127.0.0.1:8080/health # → 200 {"status":"ok"}
curl http://127.0.0.1:8080/readiness # → 200 if stores answer
That's it for the default path. Everything below is operator configuration on top.
2. Put it behind Caddy with TLS
op-server does not terminate TLS. Run Caddy (or nginx, or a load
balancer) in front. See Caddyfile.sample in this directory — one
reverse_proxy 127.0.0.1:8080 block, ACME-issued certs for free.
Bind op-server to loopback so it's only reachable through the reverse proxy:
OP_BIND_ADDR=127.0.0.1:8080 ./target/release/op-server
Then Caddy:
sudo caddy run --config /etc/caddy/Caddyfile
3. Persist to a real .graph file
The default main.rs calls AppState::new_in_memory(). To persist
the entire deployment — refunds, disputes, settlement batches,
ledger, reconciliation, webhooks, subscriptions, idempotency cache
— to a single Minigraf-backed file, swap that line:
// crates/op-server/src/main.rs
let state = AppState::with_graph_path("/var/lib/openpay/data.graph")?;
with_graph_path opens (or creates) the file and points every
store at it. Reopening the same path on restart recovers every
stored fact. See crates/op-server/src/state.rs for the full
docstring.
Or read the path from env in your binary:
let state = match std::env::var("OP_GRAPH_PATH").ok() {
Some(path) => AppState::with_graph_path(path)?,
None => AppState::new_in_memory(),
};
OP_GRAPH_PATH is not read by the shipped main.rs. The env
name is a convention this guide adopts; the systemd unit and env
sample assume you've patched main.rs accordingly (or shipped your
own binary).
4. Wire auth keys
op-server exports ApiKeyAuthLayer (in crates/op-server/src/auth.rs)
and RateLimitLayer (in crates/op-server/src/rate_limit.rs).
Neither is mounted by the default main.rs. To turn them on, build
your binary using router_with_middleware:
use std::collections::HashSet;
use op_server::{router_with_middleware, AppState, ApiKeyAuthLayer, RateLimitLayer};
let keys: HashSet<String> = std::env::var("OP_API_KEYS")
.unwrap_or_default()
.split(',')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect();
let auth = ApiKeyAuthLayer::new(keys)
.with_bypass_paths(vec!["/health".into(), "/readiness".into()]);
let limit = RateLimitLayer::per_minute(600);
let app = router_with_middleware(state, auth, limit);
Treat OP_API_KEYS as a deployment convention — same shape as
OP_GRAPH_PATH above. The library does not read it; your binary
does.
5. Wire a real HTTP transport for webhooks
The default emitter (NoOpEmitter) drops events. To deliver them,
turn on the reqwest-transport feature on op-webhook and install
the transport on AppState:
# In your binary's Cargo.toml (or op-server's, if you patch it):
op-webhook = { path = "../op-webhook", features = ["reqwest-transport"] }
use std::sync::Arc;
use op_webhook::ReqwestTransport;
let transport = Arc::new(ReqwestTransport::new()?);
let state = AppState::with_graph_path("/var/lib/openpay/data.graph")?
.with_webhook_transport(transport);
with_webhook_transport wires a WebhookDispatcher over the
state's webhooks store with a Stripe-like exponential backoff
(ExponentialBackoffPolicy::stripe_like).
6. Register a webhook endpoint
Once the dispatcher is wired, register an endpoint to receive events. Either POST directly, or use the CLI:
op webhooks register \
--url https://yourapp.example.com/openpay/events \
--secret "$(openssl rand -hex 32)" \
--event-types payment.succeeded,refund.settled
(Equivalent: POST /v1/webhooks/endpoints — see
crates/op-server/src/handlers/webhook.rs.)
The secret is used to sign outbound payloads with HMAC-SHA256. Your
receiver verifies; if it can't, drop the event. See docs/28-webhooks-progress.md
for the full delivery / retry / dead-letter story.
7. Register an FX provider
The default StaticQuoteProvider::new() has no rates and rejects
every quote request. For a deployment that calls /v1/fx/quote,
either populate the static provider at build time or install a live
one. Static is fine for stable corridors:
use op_core::Currency;
use op_fx::StaticQuoteProvider;
let fx = std::sync::Arc::new(
StaticQuoteProvider::new()
.with_rate(Currency::EUR, Currency::USD, 1_082_500) // 1.0825
.with_rate(Currency::USD, Currency::EUR, 923_900) // 0.9239
);
let state = state.with_fx_provider(fx);
rate_ppm is parts-per-million: 1_082_500 = 1.0825. See
docs/29-fx-progress.md for live-feed integration patterns (Wise,
Open Exchange Rates, an internal hedged feed) — implement
QuoteProvider and pass it to with_fx_provider.
8. Register a CryptoGateway
For the stablecoin rail, implement (or pull in) a CryptoGateway
for the chain you settle on, wrap it in a CryptoAdapter, and
register it on your orchestrator:
use std::sync::Arc;
use op_orchestrator::{CryptoAdapter, Orchestrator};
use op_rails_crypto::CryptoGateway;
let gateway: Arc<dyn CryptoGateway> = Arc::new(YourSolanaGateway::new(rpc_url));
let adapter = CryptoAdapter::new("solana-usdc", gateway);
let mut orchestrator = Orchestrator::new();
orchestrator.register_adapter(Arc::new(adapter));
let state = AppState::with_graph_path("/var/lib/openpay/data.graph")?
.with_orchestrator(orchestrator);
The driver name ("solana-usdc") is what your PolicyRouter
references when choosing a driver. See docs/25-crypto-rail-progress.md
for the full crypto-rail design.
Final: smoke test the deployment
With auth on, set the key in your shell:
export OP_API_KEY="<one of your OP_API_KEYS>"
op health
op readiness
# Create + fetch a refund as a round-trip:
op refund create --payment-id pay_demo --amount-minor 1000 --currency USD --external-id "smoke-$(date +%s)"
op refund get <id-from-previous-output>
If health returns 200 and the refund round-trips, the deployment
is wired correctly: HTTP → auth → state → graph → response.
Where each knob lives
| Concern | Code path |
|---|---|
| Bind address | OP_BIND_ADDR in crates/op-server/src/main.rs |
| Log level | RUST_LOG env (filtered by tracing-subscriber::EnvFilter) |
| Persistence | AppState::with_graph_path in crates/op-server/src/state.rs |
| Auth | ApiKeyAuthLayer in crates/op-server/src/auth.rs |
| Rate limit | RateLimitLayer in crates/op-server/src/rate_limit.rs |
| Webhook transport | AppState::with_webhook_transport + op-webhook reqwest-transport feature |
| FX rates | AppState::with_fx_provider |
| Crypto rail | CryptoAdapter::new(name, gateway) + Orchestrator::register_adapter |
| HTTP routes | crates/op-server/src/routes.rs |
| Tracing spans | grep tracing::instrument across crates/op-*/src/ |