Local setup
Three repositories. Start with the contracts — the other two need deployed contract IDs.
Prerequisites
| Tool | Version | Why |
|---|---|---|
| Rust | 1.96+ | contracts |
wasm32v1-none target | — | rustup target add wasm32v1-none |
| Stellar CLI | 27+ | build, deploy, invoke |
| Node.js | 22.5+ | backend uses node:sqlite and native TS execution |
Contracts
git clone https://github.com/MMM-pay/payflow-contract
cd payflow-contract
cargo test --all # 63 tests
stellar contract build # -> target/wasm32v1-none/release/*.wasm
Deploy your own suite:
stellar keys generate payflow-deployer --network testnet --fund
./scripts/deploy.sh testnet payflow-deployer
The script deploys in dependency order — registry, then vault, then subscription — passing each its configuration as constructor arguments, grants the subscription contract debit rights on the vault, and writes the ids and the deployment ledger to deployments/<network>.env.
Smoke-test it end to end:
./scripts/demo.sh testnet # reads deployments/testnet.env
Backend
git clone https://github.com/MMM-pay/payflow-backend
cd payflow-backend
npm install
cp .env.example .env # paste your contract IDs
npm test # 20 tests, no network needed
npm run dev
Backfill once without running the server:
npm run indexer
The keeper is off by default. To turn it on, fund an account with testnet XLM and set KEEPER_SECRET plus KEEPER_ENABLED=true. It pays transaction fees only; it holds no user funds and has no on-chain privilege.
Frontend
git clone https://github.com/MMM-pay/payflow-frontend
cd payflow-frontend
npm install
cp .env.example .env.local # paste the same contract IDs
npm run dev
Open http://localhost:3000 and connect a wallet on Testnet.
Environment variables
Contract IDs must match across all three. Get them from deploy.sh.
Backend
| Variable | Default | Purpose |
|---|---|---|
SOROBAN_RPC_URL | https://soroban-testnet.stellar.org | RPC endpoint |
NETWORK_PASSPHRASE | Test SDF Network ; September 2015 | Network id |
PLAN_REGISTRY_ID | — | Registry contract |
VAULT_ID | — | Vault contract |
SUBSCRIPTION_ID | — | Subscription contract |
KEEPER_ENABLED | false | Automatic settlement |
KEEPER_SECRET | — | S... key paying settlement fees |
INDEXER_INTERVAL_MS | 10000 | Indexing poll |
DATABASE_PATH | ./payflow.db | SQLite file |
PORT | 8080 | HTTP port |
CORS_ORIGIN | * | Allowed origins |
Frontend
Every variable is NEXT_PUBLIC_* and therefore inlined at build time. Changing one requires a rebuild, not a restart. This is the usual reason a deployed frontend keeps calling localhost.
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SOROBAN_RPC_URL | RPC the browser signs against |
NEXT_PUBLIC_NETWORK_PASSPHRASE | Network id given to the wallet |
NEXT_PUBLIC_PLAN_REGISTRY_ID | Registry contract |
NEXT_PUBLIC_VAULT_ID | Vault contract |
NEXT_PUBLIC_SUBSCRIPTION_ID | Subscription contract |
NEXT_PUBLIC_TOKEN_ID | SEP-41 token for plans |
NEXT_PUBLIC_API_URL | Backend base URL |
Deployment topology
browser ──── writes (wallet-signed) ────► Soroban RPC ──► contracts
│ │
│ │ events
└──── reads (lists, history) ──► backend ◄─── indexer ─────┘
│
SQLite
Frontend on Vercel, backend on Render with a mounted disk for SQLite. The backend is never in the path of a payment.