Database Sync
The indexer syncs account data into PostgreSQL so the relayer can resolve ownership quickly without hitting the blockchain on every request.
What database tables does the indexer use?
Both the relayer and the indexer connect to the same PostgreSQL instance (with the pgvector extension enabled). Migrations run automatically on boot.
vector_entries
The primary search table, stores vector embeddings linked to encrypted Walrus blobs.
| Column | Type | Description |
|---|---|---|
id | TEXT (PK) | UUID for this entry |
owner | TEXT | Owner's Sui address |
namespace | TEXT | Namespace label (default: "default") |
blob_id | TEXT | Walrus blob ID pointing to the encrypted payload |
embedding | vector(1536) | 1536-dimensional vector embedding (pgvector) |
created_at | TIMESTAMPTZ | Insertion timestamp |
Indexes:
idx_vector_entries_owner, B-tree onowneridx_vector_entries_blob_id, B-tree onblob_ididx_vector_entries_owner_ns, composite B-tree on(owner, namespace)for scoped queriesidx_vector_entries_embedding, HNSW onembeddingusingvector_cosine_opsfor fast similarity search
delegate_key_cache
Auth optimization, caches the mapping from delegate public key to account, so the relayer doesn't need to scan the onchain registry on every request.
| Column | Type | Description |
|---|---|---|
public_key | TEXT (PK) | Hex-encoded Ed25519 public key |
account_id | TEXT | MemWalAccount object ID |
owner | TEXT | Owner's Sui address |
cached_at | TIMESTAMPTZ | When this mapping was cached |
The cache is populated lazily during auth. If a cached entry becomes stale (key was removed onchain), the relayer re-resolves from the chain and updates the cache.
accounts
Populated by the indexer, maps owner addresses to their MemWalAccount object IDs.
| Column | Type | Description |
|---|---|---|
account_id | TEXT (PK) | MemWalAccount object ID |
owner | TEXT (unique) | Owner's Sui address |
created_at | TIMESTAMPTZ | When this row was indexed |
indexer_state
A legacy table from the event-polling indexer. The relayer's initial migration still creates it, but the current indexer does not write to it: it records its progress as a checkpoint watermark through the Sui checkpoint-ingestion framework.
| Column | Type | Description |
|---|---|---|
key | TEXT (PK) | State key (for example, "event_cursor") |
value | TEXT | JSON-serialized cursor (txDigest + eventSeq) |
Why does the indexer sync data to PostgreSQL?
- Constant-time account lookup, the relayer checks
delegate_key_cacheandaccountsinstead of scanning the onchain registry - Resumable ingestion, the indexer records its checkpoint watermark, so it picks up where it left off after restarts without re-processing old checkpoints
- Reactive cleanup, when Walrus returns 404 for an expired blob during recall, the relayer deletes the corresponding
vector_entriesrows automatically
How does similarity search work?
Recall queries use pgvector's cosine distance operator (<=>) against the HNSW index:
SELECT blob_id, (embedding <=> $1)::float8 AS distance
FROM vector_entries
WHERE owner = $2 AND namespace = $3
ORDER BY embedding <=> $1
LIMIT $4
The HNSW index provides approximate nearest neighbor search, which is fast enough for interactive recall even with large numbers of stored memories.
Recall limits and distance cutoffs
The relayer fills $4 from the request limit. The limit defaults to 10, and the relayer caps it with body.limit.min(100) so a request cannot scan the whole table. The relayer applies no relevance threshold, so the query returns the closest matches even when they are weak. To drop weak matches, pass maxDistance to the SDK recall call. The SDK then removes results where distance >= maxDistance on the client side.
Where migrations live
No migrations table and no manual sqlx migrate step exist. The SQL files live in services/server/migrations. VectorDb::new() in services/server/src/storage/db.rs lists each file explicitly and embeds it through include_str!. Every listed file re-runs in order on every server boot against DATABASE_URL, so restarting the server reapplies them. For this reason, each statement is idempotent and uses IF NOT EXISTS, IF EXISTS, or a guard query. When a file fails, the server reports Failed to run migration with the file name.