# PostgreSQL management

> Standalone management storage and encryption options.

Source: https://good-webhooks.dev/docs/reference/postgres



`good-webhooks/management/postgres` stores endpoints in one PostgreSQL table, with signing secrets encrypted by your storage encryption key.

```ts
import {
  createPostgresManagement,
  createPostgresManagementRepository,
} from 'good-webhooks/management/postgres'
import type {
  PostgresManagement,
  PostgresManagementOptions,
} from 'good-webhooks/management/postgres'
```

[`createWebhooks()`](https://good-webhooks.dev/docs/reference/core.md) builds this provider for you. Use it directly to run management without delivery, or to pass old keys during [key rotation](https://good-webhooks.dev/docs/operations/key-rotation.md).

## createPostgresManagement [#createpostgresmanagement]

```ts
createPostgresManagement(options: PostgresManagementOptions): PostgresManagement
```

```ts title="management.ts"
import { createPostgresManagement } from 'good-webhooks/management/postgres'
import { Pool } from 'pg'

export const pool = new Pool({ connectionString: process.env.DATABASE_URL })

export const management = createPostgresManagement({
  database: pool,
  eventTypes: ['invoice.paid'],
  encryptionKey: process.env.WEBHOOK_ENCRYPTION_KEY!,
})
```

Returns the complete [EndpointManagement interface](https://good-webhooks.dev/docs/reference/management.md#endpointmanagement) plus `check()`. It uses only the configured schema's `webhook_endpoints` table. It creates no delivery records and starts no sender.

| Option           | Default    | Contract                                                                                  |
| ---------------- | ---------- | ----------------------------------------------------------------------------------------- |
| `database`       | Required   | PostgreSQL `Database` with `query` and `connect`. `pg.Pool` satisfies this shape.         |
| `eventTypes`     | Required   | Nonempty array of configured event names. Management does not validate event payloads.    |
| `encryptionKey`  | Required   | Base64-encoded 32 bytes or a 32-byte `Uint8Array`. Encrypts newly stored signing secrets. |
| `decryptionKeys` | `[]`       | Older keys accepted when reading existing ciphertext.                                     |
| `schema`         | `'public'` | PostgreSQL schema containing `webhook_endpoints`.                                         |
| `allowLocalhost` | `false`    | Permits HTTP loopback destinations for development.                                       |

Invalid options, including a malformed key, throw `INVALID_CONFIG`. The host owns the pool and closes it on shutdown.

### Destination checks [#destination-checks]

The provider resolves hostnames during endpoint creation, URL updates, and resume. It rejects destinations that resolve to unsafe addresses with `UNSAFE_URL`. A hostname that fails to resolve, or takes longer than 10 seconds, also produces `UNSAFE_URL`. This admission check does not replace the sender's checks before each connection. See [security](https://good-webhooks.dev/docs/concepts/security.md).

### Storage encryption [#storage-encryption]

Signing secrets use AES-256-GCM storage encryption. Decryption tries the current key followed by the retained `decryptionKeys`. If no key decrypts a stored secret, the read rejects with `INVALID_CONFIG`.

`reencrypt(scope)` writes live records with the current key while preserving receiver-facing signing values. The host must keep old keys until every affected scope has been rewritten. See [key rotation](https://good-webhooks.dev/docs/operations/key-rotation.md).

```ts
import { createPostgresManagement } from 'good-webhooks/management/postgres'
import { pool } from './management.js'

const management = createPostgresManagement({
  database: pool,
  eventTypes: ['invoice.paid'],
  encryptionKey: process.env.WEBHOOK_ENCRYPTION_KEY!,
  decryptionKeys: [process.env.WEBHOOK_ENCRYPTION_KEY_OLD!],
})
const rewritten = await management.reencrypt({ type: 'organization', id: 'org_123' })
```

### check [#check]

```ts
check(): Promise<void>
```

```ts
import { management } from './management.js'

await management.check()
```

Reads the expected table columns without returning rows. Missing schema, table, or columns produce `SCHEMA_MISMATCH`. Other database failures propagate. This method neither executes migrations nor validates an injected delivery engine.

`good-webhooks/migrations/management.sql` initializes this provider in `public`. [Generated migration SQL](https://good-webhooks.dev/docs/reference/migrations.md) can target a custom schema.

## createPostgresManagementRepository [#createpostgresmanagementrepository]

```ts
createPostgresManagementRepository(
	database: Database,
	options?: { schema?: string },
): ManagementRepository
```

```ts
import { createManagement } from 'good-webhooks/management'
import { createPostgresManagementRepository } from 'good-webhooks/management/postgres'
import { pool } from './management.js'
import { cipher } from './cipher.js' // Your SecretCipher.

const management = createManagement({
  repository: createPostgresManagementRepository(pool, { schema: 'webhooks' }),
  cipher,
  eventTypes: ['invoice.paid'],
})
```

Creates the storage implementation without the lifecycle API or cipher. This is an extension point for [`createManagement`](https://good-webhooks.dev/docs/reference/management.md#createmanagement); most standalone applications use `createPostgresManagement`.

| Option   | Default    | Contract                                                                            |
| -------- | ---------- | ----------------------------------------------------------------------------------- |
| `schema` | `'public'` | Follows the [PostgreSQL identifier rules](https://good-webhooks.dev/docs/reference/migrations.md#schema-names). |

Invalid database or schema configuration produces `INVALID_CONFIG`.

The repository scopes every query and uses revision-guarded updates. Creation uses a transaction and an advisory lock to enforce the 1,000 nondeleted endpoint limit; a full scope produces `INVALID_STATE`. Enumeration rejects with `INVALID_STATE` if storage exceeds that limit rather than returning a partial recipient set.
