| name | viverse-key-protection-lambda |
| description | Protect third-party API keys by moving secret-bearing calls into VIVERSE Play Lambda scripts and keeping only non-secret user data in VIVERSE Storage. |
| prerequisites | ["VIVERSE Play SDK access token flow","Play Lambda Service authkey + game_id","event_name contract","VIVERSE Storage SDK"] |
| tags | ["viverse","lambda","storage","secrets","security","api-key"] |
VIVERSE Key Protection (Lambda + Storage Boundary)
Use this skill when a browser app currently holds third-party API keys (for example Google/Gemini keys) and needs a safe architecture.
Required Credentials — What You Need and Where to Get Them
Before starting integration, collect these three values. All are obtained from VIVERSE Studio.
| Credential | Used as | Where to get it |
|---|
| App ID | game_id in /env and /script API calls; appId in client SDK | VIVERSE Studio → your app → App ID field. Same value as VITE_VIVERSE_CLIENT_ID in the project .env (set by the publish flow — see viverse-world-publishing skill). |
| Lambda Authkey | Authkey: <value> request header for the Play Lambda admin REST API (GET/POST/DELETE /env, GET/POST/DELETE /script, GET /jobs). This is a server/CI-side credential — used in the sync script and CI automation to manage env vars and scripts. It is never used in browser code and never passed to lambda.invoke(). Store it only in .env.lambda.local or CI secret manager. | |
| VIVERSE access token | Third argument to lambda.invoke() on the client | Obtained at runtime via the VIVERSE auth flow (see viverse-auth skill). This is the end-user's session token, not the Authkey. |
App ID = game_id. The same ID used in VITE_VIVERSE_CLIENT_ID, viverse-cli app publish --app-id, and newMultiplayerClient(roomId, appId, ...) is the game_id passed to every Lambda API endpoint.
When To Use This Skill
Use this skill when a project needs:
- API key protection in frontend apps
- Lambda env/script/invoke integration
- secure event contracts for AI or external APIs
- clear boundary between secret and non-secret storage
Read Order
- This file
- patterns/lambda-secret-proxy.md
- patterns/storage-boundary.md
- examples/voxel-landmark-migration.md
- scripts/sync-lambda-config.sh for CI plan/apply bootstrap
- templates/.env.lambda.example for local env setup
Mandatory Compliance Gates (MUST PASS)
- MUST NOT keep production secrets in
VITE_* env variables.
- MUST store secrets in Play Lambda Env (
/env) scoped by game_id.
- MUST call secret-bearing external APIs from Lambda script (
getEnv + fetch), not directly from browser code.
- MUST define explicit
event_name contracts and validate context.data fields.
- MUST return sanitized result via
reply(...); never return raw secret values.
- MUST keep VIVERSE Storage usage to non-secret user data (cache/preferences/state only).
- MUST include failure-safe client handling for
status (failed, timeout, unauthorized, configuration_error).
- MUST rotate keys using
/env without requiring frontend redeploy.
Architecture Rule
- Lambda Env/Script: secrets + outbound protected requests
- Lambda Invoke: client-to-lambda request path
- Storage SDK: user-scoped non-secret persistence
If a value can compromise external services when leaked, it belongs in Lambda Env, not Storage and not frontend env.
Implementation Workflow
1) Inventory and classify existing key usage
Find all references to:
VITE_*_API_KEY
- query string
?key=...
- outbound
fetch with secret headers
Classify each call:
- Move to Lambda: secret-bearing, low/medium QPS request/response workloads
- Keep client-side with hard restrictions: high-frequency streaming paths that cannot use job-style invoke
2) Upload env variables to Lambda (server-side — must do before client invoke)
This step must be completed before any client invoke() call will succeed.
If env variables are missing, the script will receive empty values from getEnv() and likely reply with an error, or the job will return configuration_error.
Upload secret values via POST /env using the admin API (requires Authkey):
curl -X POST "https://broadcasting-gateway-gaming.vrprod.viveport.com/api/play-lambda-service/v1/env" \
-H "Authkey: $LAMBDA_AUTHKEY" \
-H "Content-Type: application/json" \
-d '{
"game_id": "<YOUR_APP_ID>",
"variables": {
"GEMINI_API_KEY": "your-actual-key",
"GEMINI_API_URL": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
}
}'
Or use the sync script: bash ./scripts/sync-lambda-config.sh --approve
Variables to upload:
GOOGLE_API_KEY / GEMINI_API_KEY
- endpoint URLs
- optional service-account JSON (
GOOGLE_APPLICATION_CREDENTIALS_JSON)
Never commit these values to repo. Verify upload with GET /env?game_id=<YOUR_APP_ID>.
3) Upload Lambda script per event (server-side — must do before client invoke)
This step must also be completed before any client invoke() call will succeed.
If the script does not exist for the event_name being invoked, the job returns configuration_error immediately.
Upload one script per backend capability via POST /script (requires Authkey):
curl -X POST "https://broadcasting-gateway-gaming.vrprod.viveport.com/api/play-lambda-service/v1/script" \
-H "Authkey: $LAMBDA_AUTHKEY" \
-H "Content-Type: application/json" \
-d '{
"game_id": "<YOUR_APP_ID>",
"event_name": "gemini_chat_event",
"code": "<your script content — see patterns/lambda-api-reference.md for examples>"
}'
Or use the sync script: bash ./scripts/sync-lambda-config.sh --approve
Event naming examples:
places_search_event
gemini_chat_event
tiles_root_bootstrap_event (bootstrap only; not per-tile proxy)
Script requirements:
- read secrets with
getEnv(...)
- validate
context.data input fields
- call external API with
fetch(...) — returns { status, body } (not a Promise)
reply({ success, ... }) — must be called at least once
- include
console.* logs for jobs debugging
Verify upload with GET /script?game_id=<YOUR_APP_ID>&event_name=<event_name>.
4) Invoke from client
Lambda invoke goes through the VIVERSE Play SDK multiplayer client. The full setup chain is:
Step 4a — Authenticate first
Get a VIVERSE accessToken via the auth flow (see viverse-auth skill). Lambda invoke requires a valid token.
const client = new globalThis.viverse.client();
await client.login();
const accessToken = client.getToken();
Step 4b — Build the multiplayer client
lambda.invoke() is exposed on the multiplayer client. You need a roomId even if your app has no multiplayer gameplay — use a stable per-user or per-session room ID derived from your appId and user identity.
const appId = "YOUR_VIVERSE_APP_ID";
const roomId = `${appId}-lambda`;
const userSessionId = accessToken;
const playClient = new globalThis.viverse.play();
const multiplayerClient = await playClient.newMultiplayerClient(roomId, appId, userSessionId);
await multiplayerClient.init();
Step 4c — Invoke the lambda event
const res = await multiplayerClient.lambda.invoke(
"places_search_event",
{ query: "coffee" },
accessToken
);
if (res.status !== "succeeded") {
if (res.status === "unauthorized") { }
throw new Error(res.error || res.status);
}
console.log(res.result);
Integration order summary:
Server-side setup (must complete first — requires Authkey):
- Upload env variables:
POST /env with game_id + variables
- Upload script(s):
POST /script with game_id + event_name + code
- Verify:
GET /env and GET /script confirm both are live
Client-side (only after server-side setup is confirmed):
4. Load VIVERSE SDK (globalThis.viverse available)
5. Authenticate → get accessToken
6. Create playClient → create multiplayerClient(roomId, appId, accessToken)
7. await multiplayerClient.init()
8. await multiplayerClient.lambda.invoke(eventName, payload, accessToken)
9. Handle all non-succeeded statuses (see Verification Checklist)
5) Storage SDK usage
Allowed in Storage:
- last search query
- user preferences
- cache metadata ids/hashes
Forbidden in Storage:
- API keys
- service account JSON
- upstream bearer tokens
6) Decommission frontend secrets
After Lambda path works:
- remove secret
VITE_* usage from production code
- keep local dev placeholders only when necessary
- add static checks to block key-like literals in bundle
7) Automate with manual approval
- Use read-before-write sync in CI (
GET first, then optional POST on approval).
- Generate diff artifacts before apply.
- Do not apply without explicit approval signal.
- Keep
Authkey outside repo and mask it in logs.
- Run post-apply API tests (
--test) that assert env keys and script hashes.
CI Runbook (Retained Fix Pattern)
Use the sync script exactly in this order:
- Plan only:
bash ./scripts/sync-lambda-config.sh
- Verify + test only:
bash ./scripts/sync-lambda-config.sh --verify --test
- Apply (manual-approved):
bash ./scripts/sync-lambda-config.sh --approve --verify --test
Mandatory guardrails:
- Always provide a real target app id, either:
--game-id <REAL_APP_ID>, or
LAMBDA_GAME_ID=<REAL_APP_ID> in .env.lambda.local.
YOUR_APP_ID is a hard failure and must never be used in apply mode.
- Keep
.env.lambda.local local-only and never commit real credentials.
Required local env keys (see template):
LAMBDA_AUTHKEY
LAMBDA_GAME_ID
GOOGLE_PLACES_API_KEY
GOOGLE_PLACES_TEXT_SEARCH_URL (optional override)
GOOGLE_TILES_API_KEY
GOOGLE_TILES_ROOT_URL (optional override)
Verification Checklist
Critical Gotchas
invoke() is job-style and not suitable for ultra-high-frequency streaming request fan-out.
- For tiles/streaming paths, use Lambda for bootstrap/metadata only; avoid per-request proxying via
invoke().
- Do not assume
result schema is universal; it is event-defined.
Authkey is for admin/service operations (/env, /script, /jobs) and must never be shipped in client code.
- Keep event names explicit and versioned (
places_search_v1) to avoid silent contract drift.
References