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 inctx.kv.
Decision table
Section titled “Decision table”| What you’re storing | Where it goes | Why |
|---|---|---|
| Third-party API key (ProxyCurl, Stripe, OpenAI…) | ctx.secrets | AES-256-GCM at rest, not enumerable, audited |
| OAuth refresh token your app holds for the tenant | ctx.secrets | Same — token == credential |
| Webhook-signing secret you generated for the tenant | ctx.secrets | Same |
| User-tunable settings the platform renders for you | manifest install_settings (non-secret fields) | Platform owns the UI |
| Activity log, cursor, last-seen marker | ctx.kv | Survives restarts, app-private, not sensitive |
| Per-user preferences inside your app | ctx.kv | Same shape, not credential-grade |
| Domain rows with relations + queries | app_records (via write_app_records) | Indexed, queryable, multi-row |
Canonical solution
Section titled “Canonical solution”# Reading a secret at request timeapi_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 / fetchasync 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 shapeconst 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.
❌ Anti-patterns
Section titled “❌ Anti-patterns”Don’t: store secrets in ctx.kv
Section titled “Don’t: store secrets in ctx.kv”# ❌ NEVER do thisawait ctx.kv.set("proxycurl_key", "Bearer abc123...")Why it’s wrong:
ctx.kvis 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_listreturns 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.secretsaccess is logged;ctx.kvaccess is not. - A
linkworld lintrule 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 thisPROXYCURL_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 codeimport oskey = 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.
When this doesn’t apply
Section titled “When this doesn’t apply”- 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_sendorlinkedin_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_settingsas a normal (non-secret) field. Theme color, default folder name, max-batch-size — those go throughctx.config, notctx.secrets.
Related
Section titled “Related”- 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.kvis for (non-sensitive state).