Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Storage Backends

The SDK storage layer is pluggable. TaskLifecycleService uses TaskStore, and PushNotificationService uses PushNotificationStore.

In-memory stores

Use InMemoryStoreFactory for examples, tests, and single-process development:

a2a::server::stores::InMemoryStoreFactory factory;
auto stores = factory.CreateStoreBundle();

In-memory stores do not persist data or coordinate across processes.

PostgreSQL stores

PostgreSQL support is optional. Enable and link the PostgreSQL store target:

cmake -S . -B build-postgres -DA2A_ENABLE_POSTGRES_STORE=ON
cmake --build build-postgres
a2a::server::stores::PostgresStoreFactory factory({
    .connection_string = "postgresql://a2a:a2a@127.0.0.1:5432/a2a",
    .schema = "public",
    .auto_create_schema = true,
    .connection_pool_size = 8,
});
auto stores = factory.CreateStoreBundle();

Task and push-notification stores returned by CreateStoreBundle() share one connection pool. Separately constructed stores own separate pools. Storage matching uses libpq’s active connection target (selected host, resolved server address, active port, and database) plus the configured target_session_attrs value and schema. Exact coordinate equality confirms local authority; a different database or schema confirms external authority; other endpoint differences are uncertain. Separately constructed stores can set PostgresStoreOptions::storage_authority_id: matching non-empty IDs prove local authority, different non-empty IDs prove external authority, and a one-sided ID remains uncertain. Database/schema differences still remain external. The ID is a stable, non-secret deployment identifier and must reflect the real storage authority because it affects provenance and cleanup. Passwords and raw connection strings are not part of the identity. The effective role is tracked as pool-consistency metadata, but matching endpoint/schema/role metadata does not prove that independent sessions have the same row-security context. The one-command list shortcut therefore requires the task and push stores to share the exact connection pool and local storage authority. Separately constructed stores validate the task through the authoritative task store first, even when their DSNs and roles look equivalent. Do not leave pooled connections with out-of-band session state such as SET ROLE or custom policy GUCs.

Task-aware PostgreSQL behavior

For same-storage task and push stores, create/update uses one push-store acquisition and one PostgreSQL command. That statement calls the SECURITY DEFINER task-lock helper, holds FOR KEY SHARE, and performs the upsert atomically. It has no separate task precheck, revalidation, compensating cleanup, or explicit multi-command transaction. Missing tasks return TaskNotFound, and concurrent deletion cannot leave a locally owned orphan. PushConfig_CreateMany at fan-out eight therefore performs eight create commands rather than sixteen.

GetConfig remains push-store-only and uses one PostgreSQL statement. List uses one combined task/count/page statement when task and push stores share a connection pool and local storage authority; separate pools, different storage authority, or an external task store use the authoritative task lookup first and then one combined push-list statement. External-authority creates are marked with local_postgres_task=FALSE; direct non-task-aware CreateOrUpdate calls use the same external provenance. Only locally owned rows are removed by the task-delete cleanup trigger.

When auto_create_schema=true, the SDK installs the provenance column, lock and cleanup helpers, AFTER DELETE trigger, sequences, and indexes, removes the legacy push-to-task foreign key, and writes the task-aware-push-config-v2 migration markers last.

Externally managed PostgreSQL schemas

When auto_create_schema=false, the push-notification store always validates the provenance column and absence of the legacy push-to-task foreign key. If the lock helper, cleanup helper, and cleanup trigger are all absent, construction is allowed for push-only use with an external authoritative TaskStore; local PostgreSQL task-aware creation then fails before writing until the migration is installed. Capability detection happens during store construction rather than on the request path.

If any task-aware helper or trigger is present, the whole task-aware-push-config-v2 migration is required. Validation checks both helper implementations and markers, owner privileges, absence of PUBLIC EXECUTE, the enabled AFTER DELETE row trigger without a WHEN clause, and the cleanup owner’s ability to bypass any row-level security enabled on the push-config table. Partial or stale installations fail construction.

Apply the following migration before using local PostgreSQL task-aware create/update. The example uses the public schema and an SDK role named a2a_sdk; replace both names for your deployment. Grant the lock helper only to roles authorized to create push configurations. The invoking push role needs task-table SELECT plus helper EXECUTE; task UPDATE is required by the helper owner, not by every caller. Push-only roles paired with an external authoritative TaskStore do not need task-table access or helper execution. Task-table row-level security is allowed for those external-authority paths but is rejected when the local task-aware create helper is invoked. If push-table row-level security is enabled, the cleanup helper owner must bypass it via table ownership without FORCE ROW LEVEL SECURITY, or via BYPASSRLS or superuser status, so task deletion cannot silently retain local push configurations.

BEGIN;

ALTER TABLE public.a2a_push_notification_configs
  ADD COLUMN IF NOT EXISTS local_postgres_task BOOLEAN NOT NULL DEFAULT FALSE;

ALTER TABLE public.a2a_push_notification_configs
  DROP CONSTRAINT IF EXISTS a2a_push_configs_task_fk;

CREATE OR REPLACE FUNCTION public.a2a_lock_task_for_push_config(requested_task_id TEXT)
RETURNS BOOLEAN
LANGUAGE plpgsql
VOLATILE
SECURITY DEFINER
SET search_path = pg_catalog
AS $a2a$
DECLARE
  caller_role NAME;
BEGIN
  caller_role := NULLIF(pg_catalog.current_setting('role', true), 'none');
  IF caller_role IS NULL THEN
    caller_role := session_user;
  END IF;
  IF EXISTS (
    SELECT 1 FROM pg_catalog.pg_class
    WHERE oid = pg_catalog.to_regclass('public.a2a_tasks') AND relrowsecurity
  ) THEN
    RAISE EXCEPTION USING ERRCODE = '0A000', MESSAGE =
      'PostgreSQL task-aware push configuration does not support row-level security on a2a_tasks';
  END IF;
  IF NOT pg_catalog.has_table_privilege(caller_role, 'public.a2a_tasks', 'SELECT') THEN
    RAISE EXCEPTION USING ERRCODE = '42501', MESSAGE =
      'PostgreSQL push store role requires SELECT on a2a_tasks for task-aware creation';
  END IF;
  PERFORM 1
  FROM public.a2a_tasks
  WHERE id = requested_task_id
  FOR KEY SHARE;
  RETURN FOUND;
END
$a2a$;

REVOKE ALL ON FUNCTION public.a2a_lock_task_for_push_config(TEXT) FROM PUBLIC;
GRANT SELECT ON public.a2a_tasks TO a2a_sdk;
GRANT EXECUTE ON FUNCTION public.a2a_lock_task_for_push_config(TEXT) TO a2a_sdk;

CREATE OR REPLACE FUNCTION public.a2a_delete_task_push_configs()
RETURNS TRIGGER
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = pg_catalog
AS $a2a$
BEGIN
  DELETE FROM public.a2a_push_notification_configs
  WHERE task_id = OLD.id AND local_postgres_task;
  RETURN OLD;
END
$a2a$;

REVOKE ALL ON FUNCTION public.a2a_delete_task_push_configs() FROM PUBLIC;

DROP TRIGGER IF EXISTS a2a_delete_task_push_configs_trigger ON public.a2a_tasks;
CREATE TRIGGER a2a_delete_task_push_configs_trigger
AFTER DELETE ON public.a2a_tasks
FOR EACH ROW
EXECUTE FUNCTION public.a2a_delete_task_push_configs();

COMMENT ON FUNCTION public.a2a_delete_task_push_configs()
  IS 'task-aware-push-config-v2';

COMMENT ON FUNCTION public.a2a_lock_task_for_push_config(TEXT)
  IS 'task-aware-push-config-v2';

COMMIT;

The migration markers are written last inside the transaction, so partial migrations are rejected. Existing rows receive the conservative external or unknown provenance value from the new column’s FALSE default. The lock-helper owner needs schema USAGE plus task-table SELECT and UPDATE; the cleanup helper owner needs schema USAGE plus push-table SELECT and DELETE. These are SECURITY DEFINER owner requirements, not privileges for every push-store role.

Sensitive push-notification data

Push configurations can contain callback URLs, tokens, and authentication metadata. The SDK does not implement application-level encryption at rest; use database encryption, least-privilege access, protected backups, and appropriate audit logging.