Architecture
Payment flow
1. The buyer completes the standard Shopify Checkout with a manual payment method → order PENDING 2. Thank you page: the Checkout UI Extension shows the buttons → GET /pay/:orderId?c=<confirmationNumber>&p=<provider> 3. Mistok reads the order from the Admin API, applies the prepayment rules, creates an invoice in the gateway, redirects to the hosted page 4. Gateway → POST /webhooks/<provider> (signature) → Mistok re-reads the status from the gateway API → checks amount/currency 5. success → orderCreateManualPayment(method name) → PAID / PARTIALLY_PAID → tags, notes, PRRO, SMS, CRM 6. The buyer returns → GET /return/:id → the same sync (safety net in case the webhook is lost) → Order status page 7. A poller every 10 min finishes off open invoices (gateways do not send expired)
Status model
Every gateway is mapped to a single model: created → processing → (hold →) success | failure | expired | reversed. One record per order; when the amount or method changes, the old invoice is voided (superseded) and a new one is created on the next /pay.
Reliability
- Idempotency: a
paidMarkedflag + a per-order mutex — one transaction in Shopify, however many webhooks arrive. - Webhooks are not trusted: the body is only a signal; the truth is in the gateway API response.
- Shopify webhook deduplication by
X-Shopify-Webhook-Id. - Failover:
PROVIDER_FALLBACK— on a 5xx from the primary gateway the invoice is created in the backup one. - Poller and /return — two independent ways to obtain the status without a webhook.
Data
SQLite (WAL) in DB_FILE: invoices, refunds, seen_webhooks, fiscal_receipts, events (analytics), phone_rules, pay_links. No card data anywhere; the only personal data is the buyer’s email and phone (for receipts, SMS, rules).
Several stores on one server
One Mistok instance = one Shopify store (its own token, database, domain, bot). For a second store on the same droplet run the deploy with an instance name and port: INSTANCE=shop2 DOMAIN=pay.shop2.ua PORT=3001 bash mistok/ops/deploy.sh. The script creates /opt/shop2, /root/shop2.env (that store's secrets), the shop2 service, shop2-* backups and the Caddy site /etc/caddy/sites/shop2.caddy on its own domain — instances never see each other's data. Upgrades: the same zip, the same command with the same INSTANCE.
Components
| File | Role |
|---|---|
src/app.js | HTTP: /pay, /l, /return, /api/order, /webhooks/*, /admin |
src/core.js | business logic: payments, statuses, rules, refunds, fiscal receipts, events |
src/providers/* | gateway adapters behind a single interface (types.js) |
src/analytics.js | funnel, geography, drop-off points, dashboard |
src/fiscal/checkbox.js, src/notify/* | PRRO, SMS, email, Telegram, CRM events |
extension/* | Thank you and Order status blocks (Preact) |