Skip to content

signing: Postgres ReplayStore reference adapter (adcp/signing/pgreplay) #54

@bokelley

Description

@bokelley

Problem

The in-package MemoryReplayStore is single-instance only. Multi-pod AdCP verifiers need a shared replay store, and the spec is explicit that the step-13 insert MUST be atomic with a cap check in distributed deployments to prevent cap drift. A naive implementer will write SeenInsert as two round trips and ship a race condition.

Proposal

Ship adcp/signing/pgreplay/ mirroring the shape of adcp/idempotency/postgres.go (which already handles dedup under atomic INSERT semantics):

package pgreplay

import (
    "database/sql"
    "github.com/adcontextprotocol/adcp-go/adcp/signing"
)

const Schema = \`
CREATE TABLE IF NOT EXISTS adcp_signing_replay (
    keyid     TEXT NOT NULL,
    nonce     BYTEA NOT NULL,
    expires_at TIMESTAMPTZ NOT NULL,
    PRIMARY KEY (keyid, nonce)
);
CREATE INDEX IF NOT EXISTS adcp_signing_replay_expires_at_idx
    ON adcp_signing_replay (expires_at);
\`

type PgBackend struct { ... }

func New(db *sql.DB, opts Options) *PgBackend
\`\`\`

Implementation uses a single `INSERT ... ON CONFLICT DO NOTHING` (or `INSERT ... RETURNING` with a rate-limit check) so the step-13 atomic cap + insert is one round trip. Driver-agnostic via `database/sql`, matching `idempotency.PgBackend`.

## Why Postgres instead of Redis

adcp-go already ships `adcp/idempotency/postgres.go`. Postgres is the house persistence; adding Redis just for replay dedup introduces a second system for no gain. Deployments that already run Postgres (governance agents, media buy sellers with idempotency_key dedup) get replay dedup on the same infra.

## Notes

- Also: consider changing `ReplayStore.Insert` signature to `(ok bool, err error)` so a distributed store can distinguish "cap rejected" from "couldn't reach the DB." Current `bool` collapses both into `request_signature_rate_abuse`.
- Separate `go.mod` sub-module to keep the zero-third-party-deps guarantee of the main signing packagematches how `adcp/idempotency` is structured.
- Redis adapter remains an option if a deployment specifically needs it, but is not the reference.

## Context

Raised in the DX expert review pass on the initial signing implementation for #43. The interface (`ReplayStore`) is in place; this is the reference adapter.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions