Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Drizzle

Add Ownfold's PostgreSQL schema to a Drizzle client, apply migrations, and preserve adapter transaction guarantees.

@ownfold/drizzle is the PostgreSQL adapter for applications that already use Drizzle ORM. It exports both the adapter and typed Ownfold table definitions. The shared core contract does not expose Drizzle objects, so the rest of the SDK remains ORM-independent.

Install

pnpm add @ownfold/drizzle @ownfold/server drizzle-orm pg
pnpm add -D drizzle-kit @types/pg

Drizzle ORM is a peer dependency. Configure the database driver using the same approach as the rest of your application.

Register the schema

import { drizzle } from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import {
  ownfoldDevices,
  ownfoldPairings,
  ownfoldRotations,
  ownfoldVaults,
} from "@ownfold/drizzle/schema"

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

export const db = drizzle(pool, {
  schema: {
    ownfoldVaults,
    ownfoldDevices,
    ownfoldPairings,
    ownfoldRotations,
  },
})

If your application already passes a schema object, merge the four exports into that object. Their SQL names are prefixed with ownfold_, but verify they do not collide with existing tables.

Apply migrations

Generate the ordered SQL or apply it directly:

pnpm add -D @ownfold/cli
pnpm ownfold init --database drizzle --identity custom
pnpm ownfold migrate --database-url "$DATABASE_URL"

If Drizzle Kit owns deployment, commit the generated ownfold/migrations files to the application’s migration history instead of running the second command.

The package publishes ordered SQL files under @ownfold/drizzle/migrations. Copy them into the application’s migration history and apply 0000 through 0004 in order.

These migrations are the canonical schema for the adapter. Do not use drizzle-kit push against a shared production database as a substitute for reviewed, versioned migrations. For future Ownfold upgrades, apply only newly released migration files.

Create the adapter

import { drizzleVaultAdapter } from "@ownfold/drizzle"
import { createVaultServer } from "@ownfold/server"
import { db } from "./database"

export const vaultServer = createVaultServer({
  adapter: drizzleVaultAdapter(db),
  getUserId: async ({ request }) => {
    const session = await readApplicationSession(request)
    return session?.user.id ?? null
  },
})

The adapter accepts a schema-registered PostgreSQL PgDatabase. It uses Drizzle’s parameterized query builder and returns the framework-independent VaultAdapter & VaultRotationAdapter contract.

Atomic operations

Drizzle transactions protect operations that must change multiple rows together:

  • pairing approval and device registration;
  • root-key rotation cutover;
  • replacement of all active device envelopes during rotation; and
  • vault key-version and Recovery Kit metadata updates.

Every mutable security record uses an expected revision. A stale write returns a ConflictError. The adapter deliberately validates all active devices before completing a rotation; one stale device causes the entire transaction to roll back.

Existing Drizzle applications

Add the package schema

Merge all four Ownfold table exports into the schema object passed to drizzle(...).

Copy migrations

Add the published SQL files to the application’s immutable migration history.

Deploy schema first

Apply migrations before code starts calling the new adapter.

Run compliance tests

Exercise the adapter against the same PostgreSQL major version and driver used in production.

Application records

Ownfold’s Drizzle schema stores lifecycle metadata only. Define encrypted application records in your own schema:

import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core"
import type { EncryptedEnvelopeV1 } from "@ownfold/core"

export const privateNotes = pgTable("private_notes", {
  id: text("id").primaryKey(),
  ownerId: text("owner_id").notNull(),
  encryptedPayload: jsonb("encrypted_payload").$type<EncryptedEnvelopeV1>().notNull(),
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
})

Type metadata does not validate untrusted rows at runtime. Parse an envelope through @ownfold/core at the application boundary when data may have been written outside the typed code path.

Test the integration

import { checkVaultRotationAdapterCompliance } from "@ownfold/testing"
import { drizzleVaultAdapter } from "@ownfold/drizzle"

const result = await checkVaultRotationAdapterCompliance(drizzleVaultAdapter(db))
if (result.status === "error") throw result.error

Use an isolated schema or disposable database. Ownfold’s release suite runs this path against both a fast PGlite lane and real PostgreSQL; real PostgreSQL remains the production release gate.

Troubleshooting

Type errors when constructing the adapter

Use a PostgreSQL Drizzle client and include Ownfold’s schema exports in the client schema. The adapter is not compatible with Drizzle’s MySQL or SQLite database types.

A table is missing at runtime

Registering TypeScript schema objects does not create database tables. Apply all published SQL migrations to the same database URL used by the application.

A migration generator wants to recreate Ownfold tables

Ensure your application’s Drizzle schema includes the four Ownfold table definitions before generating a diff. Review generated SQL; never accept destructive changes automatically.

Rotation reports a stale device

Another lifecycle operation changed device state after rotation began. Reload, resume from the checkpoint, and regenerate the affected replacement envelope locally.

Production checklist

  • Include all four exported tables in the Drizzle schema object.
  • Apply numbered package migrations in order.
  • Review migration diffs before deployment.
  • Use one production-grade PostgreSQL pool per process.
  • Run the shared compliance suite against real PostgreSQL.
  • Keep encrypted application records in host-owned tables.
  • Never add plaintext columns as a fallback for encrypted payload failures.

Last updated on August 4, 2026

Was this page helpful?