Skip to content

Hyperswitch Card Vault v0.9.0

Latest

Choose a tag to compare

@hyperswitch-bot hyperswitch-bot released this 31 Jul 12:51
· 7 commits to main since this release
Immutable release. Only release title and notes can be modified.

0.9.0 (2026-07-31)

This release introduces a runtime-controlled, Redis-backed write path for the fingerprint, locker, hash-table, and vault tables. When enabled, writes are first persisted to Redis and asynchronously replayed to Postgres by the drainer, while reads consult Redis first and fall back to Postgres on misses. This decouples request handling from the Postgres write path and supports low-downtime database maintenance. It also adds new operation-level metrics across the vault.

Features

Refactors

Miscellaneous Tasks

Database Migrations

CREATE TABLE IF NOT EXISTS reverse_lookup (
    lookup_id VARCHAR NOT NULL PRIMARY KEY,
    secondary_key VARCHAR NOT NULL,
    partition_key VARCHAR NOT NULL,
    source VARCHAR NOT NULL,
    updated_by VARCHAR(32) NOT NULL
);

-- Add `updated_by` to the KV-participating tables.
-- Tracks which storage backend wrote a row (`postgres_only` or `redis_kv`).
ALTER TABLE fingerprint ADD COLUMN IF NOT EXISTS updated_by VARCHAR(32);
ALTER TABLE hash_table ADD COLUMN IF NOT EXISTS updated_by VARCHAR(32);
ALTER TABLE locker ADD COLUMN IF NOT EXISTS updated_by VARCHAR(32);
ALTER TABLE vault ADD COLUMN IF NOT EXISTS updated_by VARCHAR(32);

CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS hash_table_hash_id_key_idx ON hash_table (hash_id);
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS hash_table_data_hash_pkey_idx ON hash_table (data_hash);
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '5s';
ALTER TABLE hash_table
    DROP CONSTRAINT hash_table_pkey,
    DROP CONSTRAINT hash_table_data_hash_key,
    ADD CONSTRAINT hash_table_pkey PRIMARY KEY USING INDEX hash_table_data_hash_pkey_idx,
    ADD CONSTRAINT hash_table_hash_id_key UNIQUE USING INDEX hash_table_hash_id_key_idx;

CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS locker_hash_id_merchant_id_customer_id_idx ON locker (hash_id, merchant_id, customer_id);

Configuration Changes

diff --git a/config/config.example.toml b/config/config.example.toml
index 7d73d5aaa9ec..867d6c66ca72 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -16,28 +16,37 @@ duration = 60     # duration to rate limit the delete api (in sec)
 tti = 7200          # Idle time after a get/insert of a cache entry to free the cache (in secs)
 max_capacity = 5000 # Max capacity of a single table cache
 
 [database]
 username = "sam"   # username for the database
 password = "damn"  # password of the database
 host = "localhost" # the host where the database is hosted on
 port = 5432        # the port of the database
 dbname = "locker"  # the name of the database where the cards are stored
 
+# Optional read replica
+# [read_replica]
+# username = "sam"   # username for the read replica database
+# password = "damn"  # password of the read replica database
+# host = "localhost" # the host where the read replica is hosted on
+# port = 5432        # the port of the read replica database
+# dbname = "locker"  # the name of the read replica database
+# pool_size = 10     # optional: pool size for the read replica connection pool
+
 [secrets]
 locker_private_key = "" # the locker private key to used used and the private key present with the tenant
 
 [tenant_secrets]
 # configure master_key and public_key for each tenant
 # master_key - used for database encryption this could be aes encrypted by key custodian
 # public_key - used for signature verification and encryption of response payload which is sent back to tenant
-hyperswitch = { master_key = "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", public_key = "", schema = "public" }
+hyperswitch = { master_key = "feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308", public_key = "", schema = "public", redis_key_prefix = "hyperswitch" }
 
 # To protect secret/sensitive values like:
 # - database.passwod
 # - secrets.master_key
 # - secrets.tenant_public_key
 # - secrets.locker_private_key
 #
 # Following possible encryption schemes are used, of of them are mutually exclusive, the sections are:
 # - aws_kms (AWS KMS Symmetric Encryption)
 # - hashi_corp_vault (HashiCorp Vault Secrets Engine KV version 2)
@@ -97,26 +106,65 @@ stream_read_count = 1              # entries read per stream read
 auto_pipeline = true               # automatically pipeline commands
 broadcast_channel_capacity = 32    # pub/sub broadcast channel capacity
 disable_auto_backpressure = false  # disable automatic backpressure
 max_in_flight_commands = 5000      # max in-flight commands before backpressure (0 = disabled)
 default_command_timeout = 30       # command timeout (seconds)
 unresponsive_timeout = 10          # mark a connection unresponsive after (seconds)
 unresponsive_check_interval = 2    # how often to check for unresponsiveness (seconds)
 max_feed_count = 200               # max commands fed to the server at once
 max_failure_threshold_seconds = 5  # max seconds Redis may be unreachable before it's failed
 
+# KV (write-through Redis) configuration — global drainer settings (only used with the `kv` feature)
+[kv]
+drainer_stream_suffix = "DRAINER_STREAM"  # suffix for the drainer stream name
+drainer_num_partitions = 16        # number of shard partitions (must be > 0)
+ttl_for_kv = 900                   # TTL for KV entries in Redis (seconds)
+
+# KV enablement is controlled SOLELY at runtime via the runtime-config endpoint (see
+# [runtime_config] below).  There is no TOML fallback — a single source of truth avoids drift.
+# When the endpoint is disabled or unreachable the service fails closed to "disabled"
+# (all reads/writes go to Postgres).  The endpoint returns the config object as its `value`:
+#   {"enable_kv": "disabled" | "enabled" | "soft_kill", "use_replica": true | false}
+#   "enabled"   → write-through Redis (writes → Redis + drainer; reads → Redis-first)
+#   "soft_kill" → gradual rollout (inserts → Postgres, reads → Redis-first)
+
+# Per-tenant `redis_key_prefix` (under `[tenant_secrets.<id>]`) MUST be non-empty
+# and unique across tenants whenever KV is enabled, otherwise the service fails to
+# start.  The drainer must consume the prefixed
+# `{prefix}:{shard_N}_DRAINER_STREAM` for each tenant.
+
 # Metrics configuration: export metrics for monitoring.
 # - disabled: no metrics exported
 # - otlp: configure endpoint, endpoint_timeout_secs, metrics_export_interval_secs
 # - prometheus: configure host, port (scraped at /metrics on a separate HTTP server)
 [metrics]
 mode = "otlp" # Options: "disabled", "otlp", "prometheus"
 
 # When mode is "otlp", configure endpoint, endpoint_timeout_secs and metrics_export_interval_secs:
 endpoint = "http://localhost:4317"               # OTLP gRPC endpoint
 endpoint_timeout_secs = 10                       # OTLP exporter connection timeout (seconds)
 metrics_export_interval_secs = 60                # How often to export metrics (seconds)
+background_metrics_collection_interval_secs = 15 # How often to collect metrics in the background (in secs)
 
 # When mode is "prometheus", configure host and port:
 # host = "127.0.0.1"                               # IP address the Prometheus HTTP server binds to
 # port = 9090                                      # Port the Prometheus HTTP server listens on
+# background_metrics_collection_interval_secs = 15 # How often to collect metrics in the background (in secs)
+
+# Runtime configuration endpoint
+# [runtime_config]
+# mode = "enabled"
+# refresh_interval_seconds = 30
+
+# Endpoint needs to be specified if mode is "enabled"
+# [runtime_config.endpoint]
+# base_url = "http://localhost:8090/configs"
+# api_key = "test_admin"
+# path = ""
+#
+# The endpoint at `{base_url}{path}` returns `{"key": "...", "value": "<config json string>"}`,
+# where `value` is a JSON string holding the flat config object, e.g.:
+#   {"key":"locker.configs","value":"{\"enable_kv\":\"enabled\",\"use_replica\":false}"}
 
+# Optional static headers sent on every runtime config pull request (omit entirely to send none)
+# [runtime_config.endpoint.headers]
+# accept = "application/json"

Full Changelog: v0.8.0...v0.9.0