Skip to main content

Cookbook - Multi-Tenant Server Apps

This is the pattern for SaaS-style apps where one operator runs a server that stores memory on behalf of many end users. For example, a Next.js app on Vercel where users connect a Sui wallet only for sign-in, and the server holds the credentials.

The pattern at a glance

LayerWhat it holdsWhy
OperatorOne MemWalAccount + one delegate keyCreated once on the dashboard. Pays for storage, owns all memory.
Server (API routes)MEMWAL_PRIVATE_KEY + MEMWAL_ACCOUNT_ID as env varsThe delegate key authenticates every relayer call. Never sent to the browser.
End userA connected Sui wallet, used for auth onlyThe wallet proves identity. The user does not create a MemWalAccount.
IsolationOne namespace per wallet, for example myapp-{walletAddress}Keeps each user's memories in a separate logical bucket under the shared account.
                  ┌─────────────────────────────────────┐
End user wallet │ Server (Next.js API route, Vercel) │ Relayer + Walrus
(auth only) │ │
│ sign-in │ MemWal.create({ │
├─────────▶│ key: MEMWAL_PRIVATE_KEY, │
│ │ accountId: MEMWAL_ACCOUNT_ID, │ one MemWalAccount
│ │ }) │ ├─ ns: myapp-0xalice…
│ request │ ns = `myapp-${address.toLowerCase()}`│──▶├─ ns: myapp-0xbob…
├─────────▶│ memwal.remember(text, ns) │ └─ ns: myapp-0xcarol…
│ │ memwal.recall({ query, namespace }) │
└──────────┴──────────────────────────────────────┘

1. Set up the delegate key (not the owner key)

Create the account and a delegate key once, on the Walrus Memory dashboard. The dashboard gives you:

  • A MemWalAccount object ID (0x…) → your MEMWAL_ACCOUNT_ID
  • A delegate private key (hex) → your MEMWAL_PRIVATE_KEY
warning

Use a delegate key, never the owner wallet key. The owner key can transfer the account and manage delegates; a delegate key can only read and write memory. If a delegate key leaks, you revoke it without touching the owner wallet. See Delegate Key Management.

Store both as server-side environment variables:

MEMWAL_PRIVATE_KEY=<delegate-private-key-hex>
MEMWAL_ACCOUNT_ID=0x<memwal-account-object-id>

Revoke flow

If a delegate key is compromised, revoke it from the dashboard, or programmatically with the owner credentials:

import { removeDelegateKey } from "@mysten-incubation/memwal/account";

await removeDelegateKey({
packageId: "0x<contract-package-id>",
accountId: process.env.MEMWAL_ACCOUNT_ID!,
publicKey: "<delegate-public-key-hex>", // the leaked key's public key
suiPrivateKey: "suiprivkey1...", // owner key: kept offline, NOT on the server
});

Then generate a fresh delegate key, register it with addDelegateKey, and rotate MEMWAL_PRIVATE_KEY. See the full lifecycle in Delegate Key Management.

2. Namespace naming for multi-user apps

Derive a deterministic namespace from the authenticated wallet address. Normalize it so the same wallet always maps to the same namespace:

// lib/memory/namespace.ts
export function userNamespace(walletAddress: string): string {
return `myapp-${walletAddress.toLowerCase()}`;
}

Guidelines:

  • Prefix with your app name (myapp-…) so multiple apps can share one account without colliding.
  • Lowercase the address. Namespaces are matched exactly, with no prefix or hierarchy, so 0xAB… and 0xab… are different buckets.
  • Keep it stable. Never include a timestamp or session ID, or you lose the user's history on the next request.
warning

A namespace serves as a data-organization boundary, not a security boundary between users. The delegate key can read and write every namespace under the account. Cross-user isolation depends entirely on your server mapping each authenticated wallet to the correct namespace. Verify the wallet signature before choosing the namespace. Never take the namespace (or raw address) from an unauthenticated request body.

3. Next.js / serverless example

Create one client per request (or memoize per warm Lambda) and pass the per-user namespace into each call.

// lib/memory/client.ts
import { MemWal } from "@mysten-incubation/memwal";

export function getMemWal() {
return MemWal.create({
key: process.env.MEMWAL_PRIVATE_KEY!,
accountId: process.env.MEMWAL_ACCOUNT_ID!,
serverUrl: "https://relayer.memory.walrus.xyz",
});
}
// app/api/chat/route.ts  (Next.js App Router)
import { getMemWal } from "@/lib/memory/client";
import { userNamespace } from "@/lib/memory/namespace";
import { verifyWalletAuth } from "@/lib/auth"; // your wallet-signature check

export async function POST(req: Request) {
// 1. Authenticate the user from a signed message, NOT from a raw address field.
const session = await verifyWalletAuth(req);
if (!session) return new Response("Unauthorized", { status: 401 });

const ns = userNamespace(session.walletAddress);
const { message } = await req.json();
const memwal = getMemWal();

// 2. Load relevant memory for this user.
const recalled = await memwal.recall({
query: message,
namespace: ns,
limit: 5,
});

// 3. Generate a reply with your model, injecting recalled.results as context…
const reply = await generateReply(message, recalled.results);

// 4. Persist new memory. Use rememberAndWait when the next read must see it
// immediately (for example, same-session follow-up); use remember
// (fire-and-forget) when eventual indexing is fine and you don't want to
// block the response.
await memwal.remember(`User said: ${message}`, ns);

return Response.json({ reply, recalled: recalled.results });
}
note

Structured agent state (a single profile JSON): semantic recall is built for fuzzy retrieval by meaning, so it is not a reliable way to fetch one authoritative "current profile" record. If you need deterministic key-based reads/upserts, follow issue #247. For now, prefer a dedicated namespace per structured record and treat free-form semantic lines separately.

Keep the delegate key server-side only

MEMWAL_PRIVATE_KEY must live in server env vars. Never ship it to the browser, a client component, or NEXT_PUBLIC_*. Any code path that reaches the client must not import the memory client.

Use the wallet for identity, not authorization

The user's wallet signature proves who they are. It does not grant memory access; your delegate key does. Verify the signature server-side and derive the namespace from the verified address.

Never trust a client-supplied namespace

Compute the namespace on the server from the authenticated address. If a client could send its own namespace, any user could read another user's bucket.

Keep the owner key offline

The owner wallet (used for createAccount / addDelegateKey / removeDelegateKey) should never run on the server. Run those operations from a local machine or a secure admin tool.

5. For demos and submissions

Your MemWalAccount is a regular Sui object. Share its object ID and view it on a Sui explorer (for example, Suiscan or SuiVision) to prove memory is really onchain, not just in a local cache. Pair it with a live health() check (see the API Reference) so judges can confirm the relayer is reachable.

See also