@paykernel/sql-foundation is the shared relational foundation used by PostgreSQL, SQLite, Turso, D1, and Durable Object adapters. It is not a query builder or ORM.
Version 0.1.0 — published on npm as @paykernel/sql-foundation. Install: bun add @paykernel/sql-foundation.
Install
Usually pulled in by an adapter:
bun add @paykernel/store-postgres
# runtime graph includes @paykernel/sql-foundation + @paykernel/store-contractsDirect install is valid if you are writing a relational adapter:
bun add @paykernel/sql-foundationRedis (@paykernel/store-redis) does not depend on this package.
In scope vs out of scope
| In scope | Out of scope |
|---|---|
| Canonical logical tables / columns | Production drivers (pg, bun:sqlite, D1 bindings, …) |
Validated namespace (prefix, PG schema, tenantColumn flag) |
Auto-migrate on import or store construction |
| Row codecs + validation | General SQL product / ORM |
Versioned migrate() / verifySchema() |
Core or webhooks depending on this package |
Pure decide* / evaluateClaim + dialect SQL templates |
Pretending PostgreSQL === SQLite syntax |
| In-process memory-relational reference (NON-PRODUCTION) | Replacing testkit memory stores |
Consumers: @paykernel/store-postgres, store-sqlite, store-turso, store-d1, store-durable-objects.
Schema
Logical names in LOGICAL_TABLES are resolved through createSchemaNamespace / resolveTableName before any SQL is built.
| Logical table | Contract | Role |
|---|---|---|
payment_idempotency |
LeaseAwareIdempotencyStore |
Reserve / complete / indeterminate |
payment_webhook_inbox |
WebhookInboxStore |
Claim / complete / fail / retry |
payment_reconciliation_jobs |
ReconciliationStore |
Schedule / claim due / complete / fail / review |
payment_storage_migrations |
Foundation ledger | Applied versions + checksums |
Timestamps are ISO-8601 TEXT. Lease tokens and keys are opaque strings. payload_hash is TEXT. last_error is sanitized and capped (MAX_SANITIZED_ERROR_LENGTH = 512). Raw provider payloads and signatures are not stored by default.
CHECK statuses:
- Idempotency:
reserved|completed|indeterminate|expired - Webhook inbox:
pending|claimed|completed|failed|dead_letter - Reconciliation:
scheduled|claimed|completed|failed|manual_review
Official adapters do not write every CHECK-legal status. Postgres never writes idempotency expired (reclaim uses lease_expires_at). Webhook fail writes pending / dead_letter, not failed.
Webhook columns gateway, provider_event_id, first_received_at, last_received_at exist for operator/index use. Store claim() does not populate them (ClaimWebhookInput has no gateway).
import {
createSchemaNamespace,
resolveTableName,
} from "@paykernel/sql-foundation";
const ns = createSchemaNamespace({
tablePrefix: "pay_",
sqlSchema: "payments",
tenantColumn: true,
});
const table = resolveTableName("payment_idempotency", ns);
// "payments"."pay_payment_idempotency"tablePrefix is [A-Za-z0-9_]+, max 36 (MAX_SAFE_TABLE_PREFIX_LENGTH). sqlSchema is a strict identifier (max 63). Invalid config throws SchemaNamespaceError.
Migrations are explicit
CURRENT_SCHEMA_VERSION is 2. SCHEMA_FAMILY is "payments-storage" (not an npm version).
| Version | Name | What it does |
|---|---|---|
| 1 | create_payment_storage_foundation |
Four tables + indexes |
| 2 | create_payment_storage_list_indexes |
Composite list/cleanup indexes (CREATE INDEX IF NOT EXISTS) |
import {
migrate,
verifySchema,
createSchemaNamespace,
type SqlExecutor,
} from "@paykernel/sql-foundation";
const executor: SqlExecutor = {
async execute(sql, params) {
/* prepared / bound params */
},
async query(sql, params) {
/* optional; used to read applied versions */
},
};
const ns = createSchemaNamespace({ tablePrefix: "pay_" });
const result = await migrate(executor, {
dialect: "postgres", // | "sqlite" | "generic"
namespace: ns,
});
// result.applied, result.alreadyApplied, result.currentVersion
const check = await verifySchema(executor, { dialect: "postgres", namespace: ns });
if (!check.ok) {
throw new Error("schema invalid");
}Rules:
- Call
migratefrom ops / adapter helpers — never as a silent side effect. - Importing
@paykernel/sql-foundationdoes not touch a database. - Creating a store adapter must not apply DDL.
- When
sqlSchemais set anddialectis"postgres",migrate()issuesCREATE SCHEMA IF NOT EXISTS. SQLite adapters rejectsqlSchema. Operators still needCREATEprivilege. migrate()does not acquire a portable advisory lock. Serialize migrate across hosts (one job). Concurrent runs can race on the version INSERT after multi-statement DDL.
Failures throw MigrationError (code: "migration_error"). Adapter wrappers map driver errors into StoreError.
Atomic claims
Forbidden: get-then-set across connections.
Required: engine-level conditional writes. Pure decisions (decideIdempotencyReserve, decideWebhookClaim, decideReconciliationClaim, evaluateClaim, decideLeaseMutation) perform no I/O. SQL templates are dialect-tagged (postgres | sqlite | generic).
import {
createSchemaNamespace,
idempotencyReserveTemplates,
pickClaimTemplate,
} from "@paykernel/sql-foundation";
const ns = createSchemaNamespace({ tablePrefix: "pay_" });
const frag = pickClaimTemplate(idempotencyReserveTemplates(ns), "postgres");
// frag.sql, frag.params (names), frag.intent| Dialect | Typical execution |
|---|---|
| PostgreSQL | Single-statement INSERT … ON CONFLICT DO UPDATE … WHERE … RETURNING |
| Local SQLite | INSERT OR IGNORE + conditional UPDATE in one sync BEGIN IMMEDIATE transaction |
| Async SQLite-compatible (Turso / D1) | Prefer single-statement UPSERT + RETURNING; multi-statement only in batch() / write txn |
User values are bound parameters. Table names come only from resolveTableName.
createMemoryRelationalStore is NON-PRODUCTION / NON-DISTRIBUTED (MEMORY_RELATIONAL_NON_PRODUCTION). Do not treat its always-ok executor as a production migrate signal.
Related
- Contracts:
@paykernel/store-contracts - Adapter picker: Stores · Adapter selection