Skip to content
Ownfold
Esc
navigateopen⌘Jpreview
On this page

Custom database adapter

Implement Ownfold's vault and rotation storage contracts with explicit ownership, revisions, transactions, validation, and compliance tests.

Build a custom adapter when the official SQLite, PostgreSQL, Drizzle, and Prisma packages do not support your database or runtime. The adapter stores coordination metadata; application records remain in your own repository.

Install contracts and tests

pnpm add @ownfold/core@beta better-result
pnpm add -D @ownfold/testing@beta vitest

Implement both VaultAdapter and VaultRotationAdapter for full lifecycle support.

import type { VaultAdapter, VaultRotationAdapter } from "@ownfold/core"

export class ApplicationVaultAdapter implements VaultAdapter, VaultRotationAdapter {
  // Implement the methods documented below.
}

Do not use an unsafe type assertion to claim compatibility. Let TypeScript verify every method, input, and result.

Vault methods

Method Required behavior
getVault Return the user’s vault or null; validate stored data before returning it.
createVault Be idempotent for the same user/vault input and reject conflicting ownership.
updateRecoveryStatus Match expectedRevision, update status, and increment the revision atomically.
replaceRecoveryKit Match current kit ID and revision, preserve verified status, and increment revision.
listDevices Scope every row by user and vault; never return another owner’s device.
registerDevice Be idempotent for the same device input and reject conflicting reuse.
touchDevice Move lastActiveAt forward only and do not increment the security revision.
revokeDevice Match the expected revision, set revocation time, and reject stale writes.
createPairing Persist a pending offer scoped to the authenticated owner.
getPairing Scope lookup by both user ID and request ID.
approvePairing Approve the request and register its device in one atomic transaction.
cancelPairing Match the expected revision and transition pending to cancelled atomically.

Every method returns Result<T, StorageAdapterError | ConflictError> according to the public contract. Database exceptions, SQL strings, connection details, and raw driver errors must not cross the adapter boundary.

Rotation methods

Method Required behavior
getRotation Return the current rotation for the user and vault, or null.
beginRotation Match the vault revision and create one active from/to-version transition.
updateRotationProgress Match the rotation revision and persist monotonic progress/checkpoint state.
completeRotation Atomically update every active device envelope, Recovery Kit ID, vault key version, vault revision, and rotation state.

completeRotation is the most important transaction. If any device revision is stale, the entire operation must roll back. Partial device or vault updates can make data permanently inaccessible.

Ownership and keys

Use composite uniqueness and lookup predicates that include the application user ID where the contract provides it. Request IDs, vault IDs, and device IDs are not authorization credentials.

Recommended logical keys:

Record Key or uniqueness rule
Vault One per userId; vaultId globally unique or additionally owner-scoped.
Device Unique deviceId, with every operation scoped to userId + vaultId.
Pairing Unique requestId, with every read/write scoped to userId.
Rotation At most one active rotation per userId + vaultId.

Validate stored rows

Database contents are untrusted input. Parse JSON envelopes and reconstructed records with @ownfold/core schemas before returning them. A malformed stored row must become a typed StorageAdapterError, not a partially populated domain value.

Do not silently replace invalid fields with defaults. Preserve the row for investigation and make the failure observable to operators without exposing stored ciphertext in logs.

Map conflicts explicitly

Convert expected concurrency and uniqueness failures to ConflictError with a stable code. Convert driver outages, malformed rows, and unexpected transaction failures to StorageAdapterError.

Examples of conflicts:

  • a stale expectedRevision;
  • reuse of a device ID with different key material;
  • a pairing transition from a state other than pending;
  • beginning a second incompatible rotation; and
  • completing rotation after another operation changed a device revision.

Transaction boundaries

At minimum, these operations require atomic transactions:

  • pairing approval plus device registration;
  • recovery replacement revision check plus update;
  • device revocation revision check plus update; and
  • complete rotation across vault, rotation, and all device rows.

Use database-native compare-and-swap predicates rather than a read followed by an unconditional write. A transaction alone does not prevent lost updates if the expected revision is not part of the write predicate.

Run the compliance suite

import { checkVaultRotationAdapterCompliance } from "@ownfold/testing"
import { describe, expect, it } from "vitest"
import { createTestAdapter } from "./application-vault-adapter"

describe("ApplicationVaultAdapter", () => {
  it("satisfies the Ownfold lifecycle and rotation contract", async () => {
    const adapter = await createTestAdapter()
    const result = await checkVaultRotationAdapterCompliance(adapter)

    expect(result.status).toBe("ok")
  })
})

Create a fresh isolated database for every run. The suite writes fixed compliance IDs and exercises idempotency, stale revisions, recovery replacement, device activity, revocation, pairing, rotation progress, atomic completion, and rollback after a stale device.

The suite is necessary but not sufficient. Add driver-specific tests for:

  • schema migration from the previous released version;
  • malformed JSON and missing columns;
  • deadlocks, timeouts, and connection loss;
  • real concurrent writers;
  • transaction rollback after each statement; and
  • the exact runtime and database version used in production.

Package design

Keep driver and ORM dependencies in the leaf adapter package. Do not add them to @ownfold/core or @ownfold/server. Export a constructor or factory that accepts the application’s existing client or query executor; do not create hidden global connections.

Document:

  • supported driver and database versions;
  • schema and migration ownership;
  • connection and shutdown responsibility;
  • transaction and isolation requirements;
  • runtime limitations; and
  • the tested compliance and real-database lanes.

Production checklist

  • All stored values are parsed before entering Ownfold domain code.
  • All ownership predicates include the authenticated user boundary.
  • Stale revisions return conflicts and preserve existing state.
  • Pairing approval is atomic with device registration.
  • Rotation completion is atomic across every affected record.
  • Errors are typed and scrubbed of driver details.
  • The shared compliance suite passes against the real driver.
  • Migration, concurrency, rollback, backup, and restoration tests pass.

Last updated on August 4, 2026

Was this page helpful?