Skip to content

Storing credentials & sensitive data

Your app probably needs to hold some sensitive data — third-party API keys, signing tokens, webhook secrets. LinkWorld gives you a dedicated encrypted store for that: ctx.secrets. Use it.

The one-line rule. If exposing the value in a log line, a stack trace, or an iterable listing would be a problem — put it in ctx.secrets, never in ctx.kv.

What you’re storingWhere it goesWhy
Third-party API key (ProxyCurl, Stripe, OpenAI…)ctx.secretsAES-256-GCM at rest, not enumerable, audited
OAuth refresh token your app holds for the tenantctx.secretsSame — token == credential
Webhook-signing secret you generated for the tenantctx.secretsSame
User-tunable settings the platform renders for youmanifest install_settings (non-secret fields)Platform owns the UI
Activity log, cursor, last-seen markerctx.kvSurvives restarts, app-private, not sensitive
Per-user preferences inside your appctx.kvSame shape, not credential-grade
Domain rows with relations + queriesapp_records (via write_app_records)Indexed, queryable, multi-row
# Reading a secret at request time
api_key = await ctx.secrets.get("PROXYCURL_KEY")
if api_key is None:
# Tenant didn't configure it — surface a clear error
raise RuntimeError("Configure PROXYCURL_KEY in the app settings.")
# Pass it straight to the third-party SDK / fetch
async with httpx.AsyncClient() as client:
resp = await client.get(
"https://nubela.co/proxycurl/api/v2/linkedin",
headers={"Authorization": f"Bearer {api_key}"},
params={"url": profile_url},
)
// TypeScript handler — same shape
const apiKey = await ctx.secrets.get('PROXYCURL_KEY')
if (!apiKey) {
throw new Error('Configure PROXYCURL_KEY in the app settings.')
}
const resp = await fetch('https://nubela.co/proxycurl/api/v2/linkedin', {
headers: { Authorization: `Bearer ${apiKey}` },
})

The secret never enters your application bundle, never appears in ctx.config, and is decrypted only inside the per-tenant container. See secrets reference for the storage internals.

# ❌ NEVER do this
await ctx.kv.set("proxycurl_key", "Bearer abc123...")

Why it’s wrong:

  • ctx.kv is not encrypted at the API layer (only protected by tenant-row-level security). Logs, error traces, and admin dumps can leak the value.
  • app_kv_list returns all your keys — including the credential — to anything that holds the app context. A debug surface that lists KV becomes a credential-leak surface.
  • No audit trail. ctx.secrets access is logged; ctx.kv access is not.
  • A linkworld lint rule blocks deploy when it detects credential-shaped values being written to KV. Save yourself the pre-commit failure.

Don’t: hardcode credentials in the bundle

Section titled “Don’t: hardcode credentials in the bundle”
# ❌ NEVER do this
PROXYCURL_KEY = "Bearer abc123..."

Even if “it’s just my dev key for testing” — the bundle ships to every tenant who installs your app. Their proxy logs will see your key. Use ctx.secrets with a dev-default if you need a shared key during development (see BYO API keys).

Don’t: read secrets from environment variables in production

Section titled “Don’t: read secrets from environment variables in production”
# ❌ NEVER do this in production code
import os
key = os.environ["PROXYCURL_KEY"]

Environment variables don’t survive the per-tenant container boundary — what works locally fails in production. Use ctx.secrets.get(), which has a graceful local-dev fallback to env vars (see secrets reference). The handler code stays identical.

  • Plattform-mediated credentials. OAuth tokens for M365, Odoo, LinkedIn etc. are stored by the platform on the user’s behalf and resolved automatically when your app calls scoped tools like email_send or linkedin_post. You don’t store these yourself — you just declare the scope in your manifest.
  • Public configuration. Anything safe to print in a screenshot belongs in manifest.install_settings as a normal (non-secret) field. Theme color, default folder name, max-batch-size — those go through ctx.config, not ctx.secrets.
  • BYO API keys — the canonical install-settings + tenant-override flow for user-supplied keys.
  • Secrets reference — storage internals, dev-default vs tenant-override, CLI commands.
  • Install settings — how manifest fields become the consent + config UI.
  • KV reference — what ctx.kv is for (non-sensitive state).