Skip to content
This repository was archived by the owner on Jul 21, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/tidy-bees-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@paymesh/memory": minor
"@paymesh/cli": minor
"paymesh": patch
---

Add a first-party in-memory database adapter with strict validation, seeding, CLI awareness, and documentation updates.
105 changes: 105 additions & 0 deletions apps/web/content/docs/database/memory.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: Memory
description: Use the memory adapter for tests, CI, local demos, and ephemeral examples where you want the Paymesh database contract without durable SQL storage.
---

## Overview

`@paymesh/memory` is a first-party in-memory adapter for Paymesh.

It is designed for:

- tests
- CI
- local demos
- examples that should stay dependency-light

It implements the same repository contract as the SQL-backed adapters, but keeps everything process-local and ephemeral.

## Installation

<InstallTabs packages="@paymesh/memory" />

## Usage

```ts title="src/lib/paymesh.ts"
import { createClient } from "paymesh";
import { memory } from "@paymesh/memory";
import { stripe } from "@paymesh/stripe";

export const paymesh = createClient({
provider: stripe({
secret: "sk_test_123",
webhookSecret: "whsec_123",
}),
database: memory({
seed: {
customers: [
{
id: "cus_seed",
provider: "stripe",
sandbox: true,
email: "ada@example.com",
},
],
},
}),
});
```

## Options

| Option | Default | Description |
| --- | --- | --- |
| `persistRaw` | `false` | Stores raw provider payloads in memory alongside normalized records. |
| `strict` | `true` | Enforces relational-style validation such as required fields, related entity existence, and duplicate unique ids. |
| `seed` | `undefined` | Preloads built-in Paymesh tables when the adapter is created. |

## Seeded startup data

Use `seed` when your tests or examples need initial local state:

```ts
memory({
seed: {
customers: [
{
id: "cus_123",
provider: "stripe",
sandbox: true,
email: "ada@example.com",
},
],
products: [
{
id: "prod_123",
provider: "stripe",
sandbox: true,
name: "Pro",
},
],
},
})
```

When `strict` is enabled, seed data is validated during initialization so invalid fixtures fail early.

## CLI behavior

The memory adapter is not a SQL backend.

That means:

- `paymesh generate` is skipped
- `paymesh migrate` is skipped
- `paymesh status` still works, but reports the adapter as ephemeral instead of trying to run SQL-backed migration and count queries

## When to choose this adapter

Use `@paymesh/memory` when:

- you want a first-party test adapter
- CI should not depend on a database container
- you need lightweight examples or prototypes

Use `@paymesh/postgres`, `@paymesh/drizzle`, or `@paymesh/prisma` when you need durable storage.
6 changes: 4 additions & 2 deletions apps/web/content/docs/database/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,20 @@ That means database shape is not just a static package concern. It depends on th

First-party adapters in this repository are:

- `@paymesh/memory`
- `@paymesh/postgres`
- `@paymesh/drizzle`
- `@paymesh/prisma`

All three expose the same Paymesh database driver contract:
All four expose the same Paymesh database driver contract:

- `query`
- `execute`
- `transaction`
- `repositories`
- optional `close`

The difference is where the SQL ultimately runs.
The difference is where persistence ultimately runs. `@paymesh/memory` is repository-backed and ephemeral. The others execute SQL through their respective runtimes.

## `persistRaw`

Expand Down Expand Up @@ -112,6 +113,7 @@ Use `schema.customTables` when you need additional app- or plugin-owned tables.

## Which adapter should you pick

- use `@paymesh/memory` for tests, CI, demos, and local examples
- use `@paymesh/postgres` when you want the simplest direct path with `pg`
- use `@paymesh/drizzle` when your application already standardizes on Drizzle
- use `@paymesh/prisma` when Prisma is your application’s database boundary
Expand Down
1 change: 1 addition & 0 deletions apps/web/content/docs/guides/database.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The schema layer knows about these built-in table keys:

## Adapter choice

- Use `@paymesh/memory` for tests, CI, and non-production examples.
- Use `@paymesh/postgres` for the shortest path to direct Postgres persistence.
- Use `@paymesh/drizzle` if your app already owns a Drizzle instance.
- Use `@paymesh/prisma` if your stack already standardizes on Prisma.
35 changes: 34 additions & 1 deletion apps/web/content/docs/installation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,36 @@ description: Install Paymesh, wire a provider, optionally add persistence, and m

For anything beyond a toy integration, add one database adapter.

<Tabs items={["postgres", "drizzle", "prisma"]}>
<Tabs items={["memory", "postgres", "drizzle", "prisma"]}>
<Tab value="memory">
<InstallTabs packages="@paymesh/memory" />

```ts title="src/lib/paymesh.ts"
import { createClient } from "paymesh";
import { memory } from "@paymesh/memory";
import { stripe } from "@paymesh/stripe";

export const paymesh = createClient({
provider: stripe({
secret: "sk_test_123",
webhookSecret: "whsec_123",
}),
database: memory({
seed: {
customers: [
{
id: "cus_seed",
provider: "stripe",
sandbox: true,
email: "ada@example.com",
},
],
},
}),
});
```
</Tab>

<Tab value="postgres">
<InstallTabs packages="@paymesh/postgres pg" />

Expand Down Expand Up @@ -148,6 +177,10 @@ description: Install Paymesh, wire a provider, optionally add persistence, and m
<Callout>
`persistRaw: true` stores provider payloads alongside normalized data. Use it when you need reconciliation, replay analysis, or provider-specific debugging. Leave it off when normalized records are enough.
</Callout>

<Callout type="info">
Use `@paymesh/memory` only for tests, CI, demos, and local examples. For production persistence, choose Postgres, Drizzle, or Prisma.
</Callout>
</Step>

<Step>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/content/docs/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ At runtime, Paymesh is built from four layers:

1. A `client` created with `createClient`.
2. A `provider` such as `@paymesh/stripe` or `@paymesh/polar`.
3. An optional `database` adapter such as `@paymesh/postgres`, `@paymesh/drizzle`, or `@paymesh/prisma`.
3. An optional `database` adapter such as `@paymesh/memory`, `@paymesh/postgres`, `@paymesh/drizzle`, or `@paymesh/prisma`.
4. Optional `plugins` such as `@paymesh/dash` and `@paymesh/audit-logs`.

```ts title="src/lib/paymesh.ts"
Expand Down Expand Up @@ -76,7 +76,7 @@ The current repository exposes these first-party modules:
| --- | --- |
| Core client | `paymesh` |
| Providers | `@paymesh/stripe`, `@paymesh/polar`, `@paymesh/abacatepay` |
| Database adapters | `@paymesh/postgres`, `@paymesh/drizzle`, `@paymesh/prisma` |
| Database adapters | `@paymesh/memory`, `@paymesh/postgres`, `@paymesh/drizzle`, `@paymesh/prisma` |
| Framework adapters | `@paymesh/next`, `@paymesh/express`, `@paymesh/fastify`, `@paymesh/hono`, `@paymesh/elysia` |
| Plugins | `@paymesh/dash`, `@paymesh/audit-logs` |
| Tooling | `@paymesh/cli` |
Expand Down
12 changes: 12 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 31 additions & 1 deletion lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,34 @@ pre-commit:
test:
run: bun run test
types:
run: bun run types
run: |
FILES=$(git diff --name-only --cached --diff-filter=ACMR)

if [ -z "$FILES" ]; then
exit 0
fi

if echo "$FILES" | grep -q '^tsconfig.base.json$'; then
bun run types
exit $?
fi

FILTERS=$(echo "$FILES" | awk -F/ '
/^packages\/[^/]+\// {
if ($2 == "paymesh") print "paymesh";
else print "@paymesh/" $2;
}
/^apps\/[^/]+\// {
print "./apps/" $2;
}
' | sort -u)

if [ -z "$FILTERS" ]; then
exit 0
fi

echo "$FILTERS" | while IFS= read -r filter; do
if [ -n "$filter" ]; then
bun run --filter "$filter" typecheck || exit $?
fi
done
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"check": "biome check .",
"check:write": "biome check --write .",
"typecheck": "bun run typecheck:packages",
"typecheck:packages": "bun run --filter './packages/{paymesh,audit-logs,postgres,drizzle,prisma,stripe,polar,abacatepay,express,fastify,hono,elysia,next,cli,mcp}' typecheck",
"typecheck:packages": "bun run --filter './packages/{paymesh,audit-logs,memory,postgres,drizzle,prisma,stripe,polar,abacatepay,express,fastify,hono,elysia,next,cli,mcp}' typecheck",
"typecheck:apps": "bun run --filter './apps/*' typecheck",
"types": "bun run typecheck",
"test": "bun test --pass-with-no-tests",
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { confirm, isCancel, log, text } from '@clack/prompts';
import type { Command } from 'commander';
import pc from 'picocolors';
import { loadClient } from '../lib/client';
import { isMemoryDatabase } from '../lib/database';
import {
DEFAULT_MIGRATIONS_DIR,
planGenerateMigrations,
Expand Down Expand Up @@ -31,6 +32,14 @@ export function registerGenerateCommand(program: Command) {
explicitPath: options.client,
});

if (isMemoryDatabase(client.database)) {
logInfo(
'Configured client uses @paymesh/memory. SQL migration generation does not apply.',
);

return;
}

const paymeshDir = path.join(process.cwd(), 'paymesh');
const isFirstRun = !existsSync(paymeshDir);

Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/commands/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Command } from 'commander';
import { PaymeshError } from 'paymesh';
import { loadClient } from '../lib/client';
import { isMemoryDatabase } from '../lib/database';
import {
getAppliedPaymeshMigrations,
getExpectedMigrations,
Expand Down Expand Up @@ -31,6 +32,14 @@ export function registerMigrateCommand(program: Command) {
message: 'The configured client does not define a database',
});

if (isMemoryDatabase(client.database)) {
logInfo(
'Configured client uses @paymesh/memory. SQL migrations do not apply.',
);

return;
}

try {
const migrationsDir = resolveMigrationsDir(process.cwd(), options.dir);
const historyPath = resolveHistoryPath(process.cwd());
Expand Down
23 changes: 21 additions & 2 deletions packages/cli/src/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Command } from 'commander';
import { version } from 'src';
import { printWelcome } from 'src/lib/style';
import { loadClient } from '../lib/client';
import { isMemoryDatabase } from '../lib/database';
import {
getAppliedPaymeshMigrations,
getMigrationHistoryStatus,
Expand Down Expand Up @@ -31,11 +32,26 @@ export function registerStatusCommand(program: Command) {
try {
const migrationsDir = resolveMigrationsDir(process.cwd(), options.dir);
const historyPath = resolveHistoryPath(process.cwd());
const memoryDatabase = isMemoryDatabase(client.database);
const [history, applied] = await Promise.all([
getMigrationHistoryStatus(migrationsDir, historyPath, client.schema),
memoryDatabase
? Promise.resolve({
exists: false,
valid: true,
missingFiles: [],
checksumMismatches: [],
migrations: [],
})
: getMigrationHistoryStatus(
migrationsDir,
historyPath,
client.schema,
),
client.database == null
? Promise.resolve<string[]>([])
: getAppliedPaymeshMigrations(client.database, client.schema),
: memoryDatabase
? Promise.resolve<string[]>([])
: getAppliedPaymeshMigrations(client.database, client.schema),
]);
const expected = history.migrations;
const status = await getPaymeshStatus(
Expand All @@ -55,6 +71,9 @@ export function registerStatusCommand(program: Command) {
console.log(
` Database ${status.database.configured ? formatState(status.database.connected ? 'connected' : 'configured') : formatState('not configured', 'warn')}`,
);
console.log(
` Adapter ${formatValue(status.database.adapter ?? 'none')}`,
);
console.log(
` History ${status.history.exists ? (status.history.valid ? formatState('valid') : formatState('invalid', 'bad')) : formatState('missing', 'warn')}`,
);
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/src/lib/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { PaymeshDatabaseDriver } from 'paymesh';

export function isMemoryDatabase(
database: Pick<PaymeshDatabaseDriver, 'id'> | null | undefined,
) {
return database?.id === 'memory';
}
Loading