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.
267 lines
8.3 KiB
Markdown
267 lines
8.3 KiB
Markdown
# 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.
|
|
|
|
```bash
|
|
cargo build --release -p op-server
|
|
OP_BIND_ADDR=127.0.0.1:8080 ./target/release/op-server
|
|
```
|
|
|
|
In another shell:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
OP_BIND_ADDR=127.0.0.1:8080 ./target/release/op-server
|
|
```
|
|
|
|
Then Caddy:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```rust
|
|
// 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:
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```rust
|
|
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`:
|
|
|
|
```toml
|
|
# In your binary's Cargo.toml (or op-server's, if you patch it):
|
|
op-webhook = { path = "../op-webhook", features = ["reqwest-transport"] }
|
|
```
|
|
|
|
```rust
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```rust
|
|
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:
|
|
|
|
```bash
|
|
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/` |
|