Skip to content

PayKernel

Type-safe payment orchestration for TypeScript. MENA gateways + modern server runtimes. Published on npm as @paykernel/*.

Updated View as Markdown

PayKernel is a TypeScript payment orchestration toolkit. You compose @paykernel/core (create / capture / refund / verify) with optional inbox, reconciliation, routing, stores, and HTTP adapters at the application layer. Core never depends on those packages.

All packages are published to npm as @paykernel/* (core 1.0.0, gateway-tap/gateway-myfatoorah 1.0.2, testkit 1.0.2, gateway-hesabe 0.1.1, store-d1 0.1.2, integration-http 0.1.2, other engines/stores/frameworks 0.1.x, contracts/sql-foundation 0.1.0). Install with bun add @paykernel/core or npm install @paykernel/core. Source: aashahin/paykernel.

Tested with provider sandbox accounts

The deployed payment lab completed Paymob payments and full/partial refunds and a Moyasar card payment with 3-D Secure against real provider sandbox accounts on 2026-09-11. Authenticated provider inquiries confirmed the app’s settlement totals. Stripe and PayPal also passed authentication/configuration checks.

The gateway validation matrix separates completed account flows, configuration checks, and automated simulation results for all seven gateways. It also records the remaining gaps; these results do not claim production-account testing.

Quickstart

Create a first Moyasar payment with createPaymentClient, moyasarGateway, money(), and isPaidOutcome — including declined and indeterminate branches.

Packages

Full map on Packages. Built-in gateways live in core; Tap, MyFatoorah, and Hesabe are extra packages, not BuiltInGatewayName.

Examples

Private hosts under Examples (never published). POST /internal/* test hooks require enableTestHooks and must not be deployed.

Honesty

  • Folder packages/observability publishes as @paykernel/opentelemetry. Do not invent an npm name from the folder.
  • @paykernel/internal-sql-store (internal/sql-store) is private. It is a BC shim over @paykernel/sql-foundation. Do not add it to an app.
  • Redis is optional. Local SQLite is single-host. :memory: is one process. Turso is remote multi-host and has no /sync export. D1 ≠ Durable Objects ≠ Turso ≠ local SQLite. Memory stores are NON-PRODUCTION.
  • No published adapter declares coordinationScope: "multi-region".
  • handleWebhook verifies and normalizes. It does not claim, lease, or set HTTP status.
  • Never fulfill in onWebhookVerified. Fulfill after an inbox claim, and only when the rematched event is payment.succeeded or capture.completed and payment.status === "paid", bound to gatewayPaymentId.
  • 1.0 removed success from GatewayPaymentResult. Use isPaidOutcome / outcome. authorized, approved, and pending are not paid.
  • After outcome === "indeterminate" or reconciliationRequired, do not createPayment again. Lookup + decideReconciliationPolicy only.
  • Never auto-route a second gateway after timeout / indeterminate / uncertain 5xx.
  • HTTP status codes live in @paykernel/integration-http (mapInboxOutcome), not in @paykernel/webhooks.
  • Example POST /internal/* routes are unauthenticated test hooks (enableTestHooks) and must not be deployed.

If markdown and implementation disagree, code wins.

Package map

All names below are the package.json "name" fields. Extra Tap / MyFatoorah / Hesabe adapters depend only on @paykernel/core and are not BuiltInGatewayName ("moyasar" | "paypal" | "paymob" | "stripe").

Core and engines

Package Role
@paykernel/core Unified SDK: client, money, outcomes, hooks, built-in Moyasar / PayPal / Paymob / Stripe
@paykernel/webhooks Portable webhook inbox (claim, lease fencing, modes). No HTTP status codes
@paykernel/reconciliation Safe lookup, drift, decision-only policy, store-backed schedule. No mandatory queue
@paykernel/routing Select-only gateway routing. Select-time fallback is not post-attempt recovery
@paykernel/opentelemetry Metrics, spans, redacting telemetry, optional OTEL bridge (./otel). No hard OTEL in core

Extra gateways

Package Role
@paykernel/gateway-tap Tap Payments adapter. Not a core built-in
@paykernel/gateway-myfatoorah MyFatoorah adapter. Not a core built-in
@paykernel/gateway-hesabe Hesabe adapter (KWD). Not a core built-in

Built-ins: Moyasar, PayPal, Paymob, Stripe, custom plugins.

Stores

Package Role
@paykernel/store-contracts Lease-aware contracts, StoreError, manifests
@paykernel/sql-foundation Relational schemas, migrations, claim SQL templates
@paykernel/store-postgres PostgreSQL durable stores; multi-host claims
@paykernel/store-redis Optional Redis / Valkey / Upstash. Redis is never required
@paykernel/store-sqlite Single-host SQLite (Bun / Node / better-sqlite3). Not multi-host
@paykernel/store-turso Remote multi-host Turso / libSQL. Not local SQLite. No /sync
@paykernel/store-d1 Multi-host Cloudflare D1. Not SQLite, not Turso, not Durable Objects
@paykernel/store-durable-objects Multi-host partitioned SQLite-backed DOs. Not D1. Never one global DO

Importing a store package does not apply DDL. Migrate explicitly. Which store: adapter selection.

Integrations and tests

Package Role
@paykernel/integration-http mapInboxOutcome, processWebhookHttp (raw-body-safe)
@paykernel/integration-hono Thin Hono adapter over HTTP helpers
@paykernel/integration-elysia Thin Elysia adapter (parse: "none")
@paykernel/integration-express Thin Express adapter (expressRawJson)
@paykernel/integration-cloudflare-workers Thin Workers adapter
@paykernel/testkit Mock gateway, conformance, NON-PRODUCTION memory stores

Webhook routes must read the raw body (text()), never json(), before verify. See HTTP integration.

Not published: @paykernel/internal-sql-store (private BC shim), everything under examples/*, the monorepo root, and this docs app (apps/docs).

First payment

import { createPaymentClient, money, isPaidOutcome, moyasarGateway } from "@paykernel/core";

const client = createPaymentClient({
  gateways: {
    moyasar: moyasarGateway({
      secretKey: process.env.MOYASAR_SECRET_KEY!,
      webhookSecret: process.env.MOYASAR_WEBHOOK_SECRET,
    }),
  },
  defaultGateway: "moyasar",
});

const result = await client.createPayment({
  amount: money("100.00", "SAR"),
  currency: "SAR",
  orderId: "order_123",
  callbackUrl: "https://example.com/callback",
  moyasarSource: { type: "token", token: "token_xxx" },
});

if (isPaidOutcome(result)) {
  // Paid-like settlement only (`outcome === "succeeded"` and status `paid`).
} else if (result.outcome === "indeterminate" || result.reconciliationRequired) {
  // Charge may already exist. Schedule reconcile. Do not createPayment again.
} else {
  // declined / pending / authorized — see /guides/outcomes
}

Full branches (declined, requires_action, thrown declines): Quickstart. Production inbox + store + reconcile: Getting started.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close