Merchant dashboard and subscriber portal for the Payflow protocol on Stellar.
Live demo: https://mmm-pay.github.io/payflow-frontend/ · Docs: https://mmm-pay.github.io/payflow-docs/
The user-facing half of Payflow — pull-based recurring payments on Stellar. Four pages:
| Route | Who it is for | What it does |
|---|---|---|
/ |
everyone | Explains the mandate model, shows protocol counters and live contract IDs |
/subscribe |
subscribers | Browse published plans and open a mandate |
/account |
subscribers | Fund and withdraw from the vault, pause or cancel subscriptions |
/merchant |
merchants | Publish plans, track revenue, settle due mandates manually, discount or end a subscriber's mandate |
writes (signed by wallet)
browser ──────────────────────────────────────► Soroban RPC ──► contracts
│
│ reads (lists, history, revenue)
└──────────────────────────► payflow-backend ──► SQLite projection
Contract writes go straight from the browser to Soroban RPC. The backend is never in the path of a payment and never holds a key.
Contract reads that need sorting or aggregation come from the indexer,
because Soroban cannot answer "list every mandate for this merchant." Every one
of those calls is allowed to fail: src/lib/api.ts returns null instead of
throwing, and the pages fall back to reading contract state directly. The app
still works with the backend switched off — you lose the lists, not the
ability to pay.
npm install
cp .env.example .env.local # already points at the deployed testnet contracts
npm run devOpen http://localhost:3000 and connect Freighter (or any wallet the Stellar Wallets Kit supports) set to Testnet.
To see plans and history, run payflow-backend alongside it on port 8080.
Every variable is NEXT_PUBLIC_* and therefore inlined at build time.
Changing one on your host requires a redeploy, not a restart — this is the usual
reason a deployed frontend keeps calling localhost.
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_SOROBAN_RPC_URL |
RPC endpoint the browser signs against |
NEXT_PUBLIC_NETWORK_PASSPHRASE |
Network id passed to the wallet |
NEXT_PUBLIC_PLAN_REGISTRY_ID |
Deployed registry contract |
NEXT_PUBLIC_VAULT_ID |
Deployed vault contract |
NEXT_PUBLIC_SUBSCRIPTION_ID |
Deployed subscription contract |
NEXT_PUBLIC_TOKEN_ID |
SEP-41 token used for plans (native XLM SAC by default) |
NEXT_PUBLIC_API_URL |
payflow-backend base URL. Empty means read contract state directly |
NEXT_PUBLIC_READ_SOURCE |
Any existing account, used as the source for read-only simulations before a wallet connects |
- Import the repo. Framework preset Next.js; defaults are correct.
- Add every variable above under Settings → Environment Variables.
- Set
NEXT_PUBLIC_API_URLto your deployed backend URL — notlocalhost. - Set
CORS_ORIGINon the backend to this app's URL. - Redeploy after any variable change.
src/
app/ routes: /, /subscribe, /account, /merchant
components/ presentational pieces (ui.tsx, WalletButton.tsx)
lib/
payflow.ts contract SDK — read via simulation, write via wallet
api.ts payflow-backend client (fails soft)
wallet.tsx Stellar Wallets Kit context
format.ts stroop/decimal conversion, period and address formatting
config.ts env access
discount.ts per-mandate discounts: rate, remaining charges, price
src/lib/payflow.ts is the piece worth reading first: it maps every public
contract function to a typed call and translates raw Error(Contract, #N)
codes into sentences a user can act on.
| Repo | Role |
|---|---|
| payflow-contract | Soroban contracts this app calls |
| payflow-backend | Indexer and API this app reads |
| payflow-frontend | This repo |
| payflow-docs | Protocol documentation |
See CONTRIBUTING.md. CI runs lint, typecheck, test and
build. Changes are recorded in CHANGELOG.md.
See SECURITY.md. Unaudited, testnet only.
| Name | Role | Contact |
|---|---|---|
| Victor Adeleke | Lead maintainer | @titilope12 |
Apache-2.0.