Replace OpenClaw's local file vault with Supabase Vault for AES-256 encrypted-at-rest secret storage. All API keys and auth tokens stored encrypted in Postgr...
Replaces the local secrets.json vault with Supabase Vault. All OpenClaw API keys, tokens, and auth credentials are stored AES-256 encrypted in your Supabase Postgres database. Bootstrap credentials (the Supabase URL + service_role key needed to reach the vault) are encrypted locally using OS keychain or machine-derived AES-256-GCM.
See references/architecture.md for the full threat model and design rationale.
npm install --prefix ~/.openclaw/skills/supabase-vault @supabase/supabase-js
Open your Supabase project → SQL Editor → paste and run assets/setup.sql.
This creates four wrapper functions (insert_secret, read_secret, delete_secret, list_secret_names) restricted to service_role only.
Verify with:
SELECT proname FROM pg_proc
WHERE proname IN ('insert_secret','read_secret','delete_secret','list_secret_names');
-- Should return 4 rows
Copy assets/rpc-handler.ts to src/gateway/server-methods/supabase-vault.ts in the OpenClaw source, then register it in the server-methods index:
// In src/gateway/server-methods.ts (or equivalent)
import { createSupabaseVaultHandlers } from "./supabase-vault.js";
// ...
Object.assign(handlers, createSupabaseVaultHandlers());
Copy the UI files to their destinations:
assets/controller.ts → ui/src/ui/controllers/supabase-vault.ts
assets/views.ts → ui/src/ui/views/supabase-vault.ts
Register as an Integrations tab using the plugin architecture (same pattern as pipedream-connect or discord-connect):
// In the plugin registration or plugins-ui.ts:
{
id: "supabase-vault",
label: "Supabase Vault",
icon: "🔐",
section: "integrations",
controller: "supabase-vault",
view: "supabase-vault",
}
cd ~/openclaw && npm run build
(sleep 3 && systemctl --user restart openclaw-gateway) &
Open the Control UI → Integrations → Supabase Vault. Enter your Project URL and service_role key, then click Connect & Test.
After connecting, the skill automatically adds this to ~/.openclaw/openclaw.json:
{
"secrets": {
"providers": {
"supabase": {
"source": "exec",
"command": "node",
"args": ["~/.openclaw/skills/supabase-vault/scripts/fetch-secrets.js"],
"jsonOnly": true,
"trustedDirs": ["~/.openclaw/skills/supabase-vault"],
"timeoutMs": 8000
}
}
}
}
After migrating secrets, SecretRefs in config will point to this provider:
{ "source": "exec", "provider": "supabase", "id": "/OPENAI_API_KEY" }
Gateway starts
→ exec provider triggers fetch-secrets.js
→ keychain.js retrieves SUPABASE_URL + SERVICE_ROLE_KEY
(macOS: Keychain Access / Linux: GNOME Keyring / fallback: AES-256-GCM file)
→ @supabase/supabase-js createClient(url, key)
→ supabase.rpc('read_secret', { secret_name }) for each requested key
→ outputs: { protocolVersion: 1, values: { "/KEY": "value" }, errors: {} }
→ OpenClaw runtime snapshot populated — secrets in memory only
| Platform | Method | Storage |
|---|---|---|
| macOS | security CLI | Keychain Access (hardware-backed on Apple Silicon) |
| Linux (desktop) | secret-tool | GNOME Keyring / KWallet |
| WSL2 / headless | AES-256-GCM | ~/.openclaw/supabase-vault-config.enc (machine-derived key) |
| Any | AES-256-GCM | Fallback always available |
The AES-256-GCM fallback uses PBKDF2-HMAC-SHA512 (600,000 iterations) with a key derived from /etc/machine-id + $USER + app-salt. The encrypted file is unreadable on any other machine or as any other user.
From the dashboard: Integrations → Supabase Vault → Migrate from Local Vault.
Or from the CLI:
node ~/.openclaw/skills/supabase-vault/scripts/migrate.js
node ~/.openclaw/skills/supabase-vault/scripts/migrate.js --yes # non-interactive
node ~/.openclaw/skills/supabase-vault/scripts/migrate.js --dry-run # preview only
Migration moves all keys from secrets.json to Supabase Vault and updates all SecretRefs in openclaw.json from file → exec/supabase. The local secrets.json is left in place as a safety backup.
trustedDirs and file permissions on fetch-secrets.js before execution.ZIP package — ready to use