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 3 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
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
Loading