---
title: "Bun + Hono + Postgres"
description: "Thin Hono fetch adapter over the checkout kernel with explicit Postgres migrate and storeFactory."
---

> Documentation Index
> Fetch the complete documentation index at: https://paykernel-docs.abshahin.workers.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Bun + Hono + Postgres

`@paykernel/example-bun-hono-postgres` is a **private** workspace host (`private: true`). It is **not** published. HTTP mapping is the same thin Hono adapter as [Bun + Hono + SQLite](/examples/bun-hono-sqlite). The difference is how tests and the listen script obtain stores.

Source: [`examples/bun-hono-postgres`](https://github.com/aashahin/paykernel/tree/main/examples/bun-hono-postgres) ([README](https://github.com/aashahin/paykernel/blob/main/examples/bun-hono-postgres/README.md)).

Postgres here means **durable inbox / recon / idempotency tables** on a shared cluster ([`@paykernel/store-postgres/pg`](/stores/postgres)). The charge gateway is still the kernel **mock**. No published adapter declares `coordinationScope: "multi-region"`. This host is still one Hono process.

## HTTP mapping

[`src/app.ts`](https://github.com/aashahin/paykernel/blob/main/examples/bun-hono-postgres/src/app.ts) matches the sqlite Hono host: `createCheckoutHandlers` + `honoWebhook` from [`@paykernel/integration-hono`](/integrations/hono). Stripe HMAC is verified on `Request.text()`. Never `c.req.json()` on `/webhooks/stripe`. Status codes come from `mapInboxOutcome`, not [`@paykernel/webhooks`](/packages/webhooks). Never fulfill in `onWebhookVerified`.

Routes (same as other hosts):

| Method | Path | Notes |
| --- | --- | --- |
| `POST` | `/payments` | Create order + mock charge |
| `POST` | `/webhooks/stripe` | Raw body + `stripe-signature` |
| `GET` | `/orders/:orderId` | Order book |
| `POST` | `/internal/reconcile` | **Test hook.** Do not deploy. |
| `POST` | `/internal/provider-paid` | **Test hook.** Do not deploy. |
| `GET` | `/internal/create-count` | **Test hook.** Do not deploy. |

Tests pass `{ enableTestHooks: true }`. Listen does not. Without the flag, internal routes return `404`.

## Listen (Postgres required)

[`src/index.ts`](https://github.com/aashahin/paykernel/blob/main/examples/bun-hono-postgres/src/index.ts) uses the **typed** `/pg` API (`{ client: pool }`), matching [Getting started](/guides/getting-started) and the store-postgres README:

```ts
import { createCheckoutKernel } from "@paykernel/example-checkout-kernel";
import {
  createPgPostgresExecutor,
  createPostgresStoresFromPg,
  migratePostgresAdapter,
} from "@paykernel/store-postgres/pg";
import { Pool } from "pg";

const pgUrl = process.env.PAYMENTS_SDK_PG_URL ?? process.env.DATABASE_URL;
const pool = new Pool({ connectionString: pgUrl });
const executor = createPgPostgresExecutor(pool);
// Ops/CI only — migrate explicitly before kernel, never on import/request
await migratePostgresAdapter(executor);
const stores = createPostgresStoresFromPg({ client: pool });
const kernel = await createCheckoutKernel({
  storeFactory: async () => ({
...stores,
close: () => {
  void pool?.end();
},
  }),
});
```

`createPostgresStoresFromPg` is `{ client, clock?, namespace? }` — not `{ executor }` ([`packages/store-postgres/src/drivers/pg.ts`](https://github.com/aashahin/paykernel/blob/main/packages/store-postgres/src/drivers/pg.ts)).

Without `PAYMENTS_SDK_PG_URL` / `DATABASE_URL` the process **exits**. Set `ALLOW_MEMORY_FALLBACK=1` to fall back to the kernel’s in-memory SQLite (dev only, not a Postgres RC):

```bash
PAYMENTS_SDK_PG_URL=postgres://payments:payments@127.0.0.1:54329/payments_sdk bun src/index.ts
# or
ALLOW_MEMORY_FALLBACK=1 bun src/index.ts
```

Listen does **not** enable test hooks. Do not deploy this example as-is.

:::caution[Kernel sqlite-migrate vs Postgres executor]
`createCheckoutKernel` still calls `migrateSqliteAdapter(stores.executor)` when `stores.executor` is present. A Postgres bundle’s executor is `PostgresExecutor` (`query`/`execute`, `$1` placeholders), not `SqliteExecutor` (`query`/`run`, `?`). The listen path injects that bundle anyway. Verify this composition before copying it; do not assume sqlite migrate is a no-op on Postgres.
:::

## Tests

Live Postgres is required for the checkout suite. When `PAYMENTS_SDK_PG_URL` (or `DATABASE_URL`) is unset, that suite is `describe.skipIf` — same honesty as [`packages/store-postgres/docs/testing.md`](https://github.com/aashahin/paykernel/blob/main/packages/store-postgres/docs/testing.md).

```bash
docker compose -f ../../packages/store-postgres/docker-compose.yml up -d
PAYMENTS_SDK_PG_URL=postgres://payments:payments@127.0.0.1:54329/payments_sdk bun test
```

From the monorepo root with no PG env, `bun test examples/bun-hono-postgres` still runs the test-hook honesty tests and **skips** the live Postgres case.

Tests call `createHonoCheckoutApp(kernel)` and `app.fetch`. They do not start a listener.

:::note[Test file vs typed `/pg` factory]
[`src/app.test.ts`](https://github.com/aashahin/paykernel/blob/main/examples/bun-hono-postgres/src/app.test.ts) currently calls `createPostgresStoresFromPg({ executor, namespace: { tablePrefix: prefix } })` and imports from `packages/store-postgres/src/pg`. The public function requires `client: PgPoolLike`. The host README describes that test call; [Getting started](/guides/getting-started) and the listen script use `{ client: pool }`. **Code of `createPostgresStoresFromPg` wins** — copy the listen / getting-started shape.
:::

The honesty tests still construct `createCheckoutKernel()` with **no** Postgres factory (in-memory sqlite) to assert `/internal/*` is `404` without `enableTestHooks`.

## Related

- [Checkout kernel](/examples/checkout-kernel) · [Postgres store](/stores/postgres) · [Hono](/integrations/hono) · [Getting started](/guides/getting-started) · [Composition](/guides/composition)

Source: https://paykernel-docs.abshahin.workers.dev/examples/bun-hono-postgres/index.mdx
