Self-Hosting
Self-hosting means running your own relayer, either pointing at an existing Walrus Memory package ID or deploying an entirely new Walrus Memory instance with your own contract, database, and server wallet.
The managed relayer that Walrus Foundation provides is a reference implementation. You can also build your own implementation that fits the same API surface with custom logic. You can run the reference implementation as your own self-hosted relayer.
If you want the default relayer-handled SDK flow while reducing trust in the host operator, see the TEE Deployment Pattern.
Who self-hosts the relayer and when?
Two primary personas typically self-host the relayer:
- Builders and teams: Self-hosting for their own agentic needs or internal team usage, keeping the trust boundary, encryption, and embeddings under their control.
- Infrastructure operators and managed service providers (MSPs): Hosting the relayer as a reliable platform or service for other external development teams and agentic builders.
The most common reasons to self-host include:
- Control the trust boundary: Keep plaintext, encryption, and embedding under your own control rather than trusting a third party.
- Run your own Walrus Memory instance: Deploy your own contract with a separate package ID, Seal encryption keys, and hard data isolation.
- Choose your own embedding provider: Use your own OpenAI-compatible API and credentials.
- Guarantee availability: The managed relayer carries no service-level agreement (SLA).
Data isolation with namespaces
With the current architecture, Walrus Memory isolates data strictly by User (Owner address) and Namespace. The relayer scopes all vector searches and storage operations by owner + namespace, so multiple agents or applications can safely share the same relayer deployment by using different namespaces or operating under different delegate keys.
How to scale the relayer horizontally
If you are a managed service provider or need to handle high agentic throughput, you can horizontally scale your hosted relayer natively. To run multiple instances of the relayer behind a load balancer for the same account and package ID:
- Point all relayer instances to the same PostgreSQL database.
- Supply the same
SERVER_SUI_PRIVATE_KEYSpool to all instances so they can seamlessly execute concurrent Walrus uploads. - Configure the same Redis cluster (
REDIS_URL) across all nodes so that the rate limiter sliding window accurately tracks global user quotas across your deployment.
What does a self-hosted backend run?
A self-hosted Walrus Memory backend has:
| Component | Location | Description |
|---|---|---|
| Rust relayer | services/server | Axum HTTP server for auth, routing, embedding, and vector search |
| TypeScript sidecar | services/server/scripts | Seal encrypt and decrypt, Walrus upload, blob query (uses @mysten/seal and @mysten/walrus) |
| PostgreSQL + pgvector | External | Vector storage, auth cache, indexer state |
| Indexer (recommended) | services/indexer | Reads Sui checkpoints, syncs account data into PostgreSQL |
The Rust relayer starts the TypeScript sidecar as a child process on boot. They communicate over HTTP (localhost:9000 by default). If the sidecar fails to start within 15 seconds, the relayer exits.
Quick start
If you do not already have PostgreSQL + pgvector running, start it with:
Source: relayer/self-hosting.md$ docker compose -f services/server/docker-compose.yml up -d postgres
Copy the example environment file, set the required environment variables in services/server/.env, and then run the relayer:
$ cp services/server/.env.example services/server/.env
$ cd services/server/scripts
$ npm ci
$ cd ..
$ cargo run
Then check:
Source: relayer/self-hosting.md$ curl http://localhost:8000/health
Which environment variables does the relayer use?
The relayer reads its configuration from environment variables, grouped by how likely you are to need them.
Required
DATABASE_URLMEMWAL_PACKAGE_IDMEMWAL_REGISTRY_IDSERVER_SUI_PRIVATE_KEYorSERVER_SUI_PRIVATE_KEYSSIDECAR_AUTH_TOKEN: Shared secret for Rust-to-sidecar calls. The sidecar refuses to start without it.
Recommended
OPENAI_API_KEY: Enables real embeddings (falls back to mock embeddings without it).OPENAI_API_BASE: Point to an OpenAI-compatible provider like OpenRouter.
Optional rate limits and storage
By default, the relayer enforces rate limits and storage quotas through Redis to prevent abuse. You can customize these limits:
RATE_LIMIT_REQUESTS_PER_MINUTE: Maximum burst weighted-requests per minute per user (default: 60).RATE_LIMIT_REQUESTS_PER_HOUR: Maximum sustained weighted-requests per hour per user (default: 500).RATE_LIMIT_DELEGATE_KEY_PER_MINUTE: Maximum weighted-requests per minute per delegate key (default: 30).RATE_LIMIT_STORAGE_BYTES: Maximum storage per user in bytes (default: 1GB,1073741824).REDIS_URL: Required to track sliding windows for rate limits (default:redis://localhost:6379).
Defaults
PORTdefaults to8000.SIDECAR_URLdefaults tohttp://localhost:9000.SUI_NETWORKdefaults tomainnet.SUI_RPC_URL, Walrus endpoints, andWALRUS_PACKAGE_IDfall back to network defaults based onSUI_NETWORK.SEAL_SERVER_CONFIGSandSEAL_KEY_SERVERSare optional overrides for encrypt and decrypt; preferSEAL_SERVER_CONFIGSfor custom committees. See Seal key server configuration for both variables.WALRUS_AGGREGATOR_URLScan add comma-separated proxy or aggregator candidates for cold-read tail racing after Redis cache misses.WALRUS_SKIP_CONSISTENCY_CHECK=falseby default; enable only for trusted Walrus Memory-written cold reads after accepting the consistency tradeoff.- The sidecar Walrus upload route defaults storage
epochsby network:5ontestnet,3onmainnet(unless the request passesepochs). Both the request value and theWALRUS_STORAGE_EPOCHSoverride cap at15; an over-cap override falls back to the network default. SEAL_THRESHOLDdefaults tomin(2, total configured server weight). A single committee server config defaults to threshold1.
Server keys
SERVER_SUI_PRIVATE_KEYis the main server key.SERVER_SUI_PRIVATE_KEYSis a comma-separated key pool for parallel Walrus uploads.- If you set both, the key pool takes priority for uploads.
Sui RPC transport (gRPC)
By default the relayer talks to Sui over JSON-RPC. Ahead of the Sui JSON-RPC sunset in July 2026, you can opt into gRPC using SUI_GRPC_URL. Set it to a Sui gRPC full node URL to route the relayer's Sui calls through gRPC. Leaving it empty keeps the existing JSON-RPC behavior, so setting it is opt-in and backward compatible.
# Opt the write path into gRPC (testnet example)
SUI_GRPC_URL=https://fullnode.testnet.sui.io
This one variable switches both paths. The write path (Walrus register and certify, Seal, and Enoki build) and the blob query and restore path both move to gRPC. When you set SUI_GRPC_URL, the query and restore path uses gRPC listOwnedObjects and getDynamicField; when you leave it unset, that path falls back to the JSON-RPC getOwnedObjects, getDynamicFieldObject, and transaction-block queries. SUI_RPC_URL configures that JSON-RPC fallback client.
SUI_GRPC_URL must point at a gRPC endpoint. The relayer parses the URL at startup and exits if the URL is malformed. A well-formed URL that points at a host that does not serve gRPC passes startup, because the client connects lazily, and the failure surfaces on the first gRPC call. Verify the endpoint before you deploy.
Package contract IDs
Use the IDs that match your target network.
Staging (Testnet)
Source: relayer/self-hosting.mdSUI_NETWORK=testnet
MEMWAL_PACKAGE_ID=0x0a625e2db2af6f591a4c80a3d8551ddf11656089cc3a20c5e9e7f8fb75b9265c
MEMWAL_REGISTRY_ID=0x736aef9906798fca4460490ccdf8e8502ef170122dc26ecae32111b78c6b42dd
Production (Mainnet)
Source: relayer/self-hosting.mdSUI_NETWORK=mainnet
MEMWAL_PACKAGE_ID=0xe7c16fbea0560e7057e2bf7422feaa4fb313749fc69c9e9092fac7a33b81d7f5
MEMWAL_REGISTRY_ID=0x8bf82c9e09e36b8d1c38298f68b7cb68e7b8762887e7592add9986d5e9cf199f
How to configure Seal key servers
Two variables control which Seal key servers the sidecar uses. SEAL_KEY_SERVERS takes a comma-separated list of independent key server object IDs. SEAL_SERVER_CONFIGS takes a JSON array of server entries (objectId, optional weight, aggregatorUrl, and an apiKeyName and apiKey pair); committee entries require an aggregatorUrl. If you set both, the sidecar uses SEAL_SERVER_CONFIGS and ignores SEAL_KEY_SERVERS. Prefer SEAL_SERVER_CONFIGS for custom committees.
If you set neither SEAL_SERVER_CONFIGS nor SEAL_KEY_SERVERS, the sidecar uses built-in defaults for the selected SUI_NETWORK. On testnet, the default remains Mysten's original independent key server pair so existing encrypted memories remain decryptable:
SEAL_KEY_SERVERS=0x73d05d62c18d9374e3ea529e8e0ed6161da1a141a94d3f76ae3fe4e99356db75,0xf5d14a81a982144ae441cd7d64b09027f116a468bd36e7eca494f750591623c8
On mainnet, the default remains the legacy independent key server pair until Mysten publishes an official committee aggregator.
Use SEAL_SERVER_CONFIGS to opt into a committee key server. Committee entries require an aggregatorUrl, for example:
SEAL_SERVER_CONFIGS=[{"objectId":"0x...","weight":1,"aggregatorUrl":"https://seal-aggregator.example.com"}]
Mysten's official Testnet committee aggregator is:
Source: relayer/self-hosting.mdSEAL_SERVER_CONFIGS=[{"objectId":"0xb012378c9f3799fb5b1a7083da74a4069e3c3f1c93de0b27212a5799ce1e1e98","weight":1,"aggregatorUrl":"https://seal-aggregator-testnet.mystenlabs.com"}]
Although that committee is 3-of-5 internally, Seal exposes it to the SDK as one logical server config. The aggregator handles the internal committee threshold, so leave SEAL_THRESHOLD unset or set it to 1 when using this committee config. Because it uses a different key server object, do not switch an existing deployment to it until you migrate or re-encrypt older data.
Changing Seal key server defaults only affects new encryption. If a deployment has already encrypted memories with the Testnet independent key servers, keep those servers as the default or pin them with SEAL_KEY_SERVERS until you migrate or re-encrypt the data. Otherwise, recall and restore for older blobs might fail to decrypt.
Use the official key server configuration where possible.
VITE_MEMWAL_PACKAGE_ID and VITE_MEMWAL_REGISTRY_ID are frontend environment variables for the app or playground, not for the relayer.
How to set up the database
The relayer requires PostgreSQL with the pgvector extension. The relayer runs migrations automatically on boot, creating these tables:
vector_entries: 1536-dimensional embeddings with HNSW index for cosine similarity search.delegate_key_cache: Auth optimization (delegate key to account mapping).accounts: The indexer populates this table (account to owner mapping).indexer_state: Legacy cursor table from the event-polling indexer. The current indexer does not write to it.
See Database Sync for the full schema.
Operational notes
- The server starts the sidecar automatically on boot. If sidecar startup fails, the relayer exits.
- Database migrations run automatically on boot (install
pgvectoras a PostgreSQL extension beforehand). - Connection pool: 10 max connections (relayer), 3 max connections (indexer).
/healthis the basic service check,/metricsexposes Prometheus metrics, and API routes live under/api/*.- The indexer records which owner has an account. Delegate-key authentication does not scan
AccountRegistry; a cold key needsx-account-idorMEMWAL_ACCOUNT_ID. - Without
OPENAI_API_KEY, the server uses deterministic mock embeddings (hash-based), useful for local testing but not production. - Use
LOG_FORMAT=jsonin production and see Observability for dashboards and alerts.
Run the indexer
The indexer is optional for development. Without it, the accounts table stays empty, so the account existence check and the owner-token route do not see any accounts. Delegate-key authentication does not depend on it. For production, run the memwal-indexer binary from services/indexer. It reads Sui checkpoints from a remote checkpoint store:
$ cd services/indexer
$ cargo run --release -- --remote-store-url <CHECKPOINT_STORE_URL> --first-checkpoint <CHECKPOINT>
Set --first-checkpoint to a checkpoint at or before the one that published your Walrus Memory package, so the indexer sees every AccountCreated event.
The indexer also reads these environment variables:
DATABASE_URL(required): PostgreSQL connection string.MEMWAL_PACKAGE_ID(required): Walrus Memory contract package ID to filter events.