No description
  • TypeScript 62.5%
  • JavaScript 32.2%
  • CSS 3%
  • HTML 2%
  • Dockerfile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-28 11:35:51 +02:00
n8n initial commit 2026-08-28 11:35:51 +02:00
node_modules initial commit 2026-08-28 11:35:51 +02:00
public initial commit 2026-08-28 11:35:51 +02:00
scripts initial commit 2026-08-28 11:35:51 +02:00
server initial commit 2026-08-28 11:35:51 +02:00
src initial commit 2026-08-28 11:35:51 +02:00
.dockerignore initial commit 2026-08-28 11:35:51 +02:00
docker-compose.yml initial commit 2026-08-28 11:35:51 +02:00
Dockerfile initial commit 2026-08-28 11:35:51 +02:00
eslint.config.mjs initial commit 2026-08-28 11:35:51 +02:00
package-lock.json initial commit 2026-08-28 11:35:51 +02:00
package.json initial commit 2026-08-28 11:35:51 +02:00
README.md initial commit 2026-08-28 11:35:51 +02:00
startup.sh initial commit 2026-08-28 11:35:51 +02:00
tsconfig.json initial commit 2026-08-28 11:35:51 +02:00
vite.config.ts initial commit 2026-08-28 11:35:51 +02:00

Beliot Ventures — Rent Payment

Secure M-Pesa rent payment page. Tenants open a link with a token, the app loads their balance from n8n, and they pay from their phone.

Production-style URL:

https://pay.beliot.co.ke?token=TOKEN_CODE

Requirements

  • Node.js 22.12 or later (Node 18 will crash Vite with styleText is not exported)
  • npm 10 or later
  • Network access to https://n8n.beliot.co.ke

If node -v shows v18.x, upgrade first. On Ubuntu/WSL with nvm:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# close and reopen the terminal, or:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

nvm install 22
nvm use 22
node -v    # should print v22.x

Then reinstall (Vite binaries were built under Node 18):

rm -rf node_modules package-lock.json
npm install
npm run dev

Setup

unzip beliot-rent-payment.zip
cd beliot-rent-payment   # or the folder you unzipped into
npm install

The HMAC signing secret is already set in src/lib/payment/n8n.server.ts. To override it without editing code, set:

export PAYMENT_HMAC_SECRET="your-hmac-secret"

Ports

How you run it Where to change the port
npm run dev Env PORT (default 8080), or server.port in vite.config.ts
Docker / npm start Env PORT (default 8080). Nitro also reads NITRO_PORT. Bind HOST=0.0.0.0

Examples:

PORT=3000 npm run dev
docker run --rm -p 80:8080 -e PORT=8080 -e PAYMENT_HMAC_SECRET=your-secret beliot-pay

The container always listens on PORT inside the image (compose maps host PORT → container 8080). To change the published host port only:

PORT=80 docker compose up --build

Docker

docker compose up --build
# or
docker build -t beliot-pay .
docker run --rm -p 8080:8080 \
  -e PAYMENT_HMAC_SECRET=your-hmac-secret \
  beliot-pay

Production build uses Nitro node-server (NITRO_PRESET=node-server). HMAC signing still happens only on the server.

npm run dev

Open http://localhost:8080. With no token, the page loads a sample request so you can check the layout.

Open a real payment request:

http://localhost:8080/?token=YOUR_TOKEN

Sample token used for testing:

test_pay_7f3c9e2a1b8d4f06

How a payment works

  1. The page reads token from the query string.

  2. The server POSTs {"token":"..."} to
    https://n8n.beliot.co.ke/webhook/payment-details
    with header X-Signature (HMAC-SHA256 hex of the exact JSON body).

  3. Tenant name, house/unit, outstanding balance, suggested amount, phone, and expiry are shown. Add house_unit to the payment-details payload (used on the page and as the M-Pesa AccountReference).

  4. Amount is typed by the tenant. If they leave it blank, suggested_amount is sent.

  5. Pay with M-Pesa POSTs this JSON to
    https://n8n.beliot.co.ke/webhook/pay
    with the same X-Signature HMAC header:

    {
      "token": "test_pay_7f3c9e2a1b8d4f06",
      "phoneNumber": "254721271252",
      "amount": 10000,
      "account": "B2"
    }
    

    account is the house/unit (max 12 characters for Daraja). HMAC is SHA-256 hex of that exact JSON string (key order as above).

  6. n8n should wait for the STK Push HTTP response and reply with JSON, not a redirect:

    {
      "success": true,
      "checkout_request_id": "ws_CO_...",
      "message": "Prompt sent"
    }
    
  7. The page then polls https://n8n.beliot.co.ke/webhook/payment-status every 3s:

    { "token": "...", "checkout_request_id": "ws_CO_..." }
    

    Reply { "success": true, "status": "pending" | "paid" | "failed" | "cancelled", "message": "..." }
    using the result stored from /webhook/mpesa_cb.

Copy-paste n8n Code nodes: n8n/hmac-verify.js, n8n/check-token-cache.js, n8n/generate-stk-password.js, n8n/stk-respond.js, n8n/mpesa-callback-store.js, n8n/payment-status.js.

Kenyan numbers are accepted as 07…, 01…, 2547…, or +2547….

Build

npm run build
npm run typecheck

The production build is a Vercel / Nitro output under .vercel/output.

Deploy (Vercel)

  1. Push this folder to GitHub (or upload it in the Vercel dashboard).
  2. Import the project in Vercel. Framework preset can stay Vite; the Nitro plugin already emits a Vercel function.
  3. Set environment variable PAYMENT_HMAC_SECRET to your webhook secret.
  4. Point pay.beliot.co.ke at the Vercel deployment.

The HMAC secret must never be exposed in client-side code. Signing happens only in src/lib/payment/n8n.server.ts.

Useful scripts

Command What it does
npm run dev Development server on port 8080
npm run build Production build
npm run typecheck TypeScript check
npm run preview Serve the production build locally

Project layout

src/routes/index.tsx              Payment page (reads ?token=)
src/components/payment/           Page UI
src/lib/payment/api.ts            Server functions
src/lib/payment/n8n.server.ts     HMAC signing + n8n calls
public/                           Logo, favicon, share card