Skip to content

✨ Add workspace-level encrypted secrets (server-only resolution) [draft — refs #627]#2458

Draft
klinux wants to merge 1 commit intobaptisteArno:mainfrom
klinux:feat/workspace-secrets-backend
Draft

✨ Add workspace-level encrypted secrets (server-only resolution) [draft — refs #627]#2458
klinux wants to merge 1 commit intobaptisteArno:mainfrom
klinux:feat/workspace-secrets-backend

Conversation

@klinux
Copy link
Copy Markdown

@klinux klinux commented Apr 15, 2026

Summary

Draft implementation of workspace-scoped encrypted secrets — a new WorkspaceSecret entity that lets users define reusable encrypted values (API keys, tokens, etc.) at the workspace level and reference them from typebots via {{$secrets.NAME}}. Refs #627.

Opening as a draft because I'd like design feedback before investing in the builder UI. We're using typebot in production and this gap (no way to store reusable secrets safely) is blocking a few flows, so I started an implementation — happy to rework the design if you'd prefer a different direction.

Why a new entity (not a flag on Variable)

The issue mentions "encryption option for a variable." I considered adding an isSecret flag to Variable, but the existing Variable type flows through ~35 call sites (text bubbles, client responses, Result.variables, SetVariableHistoryItem, code blocks executed in the browser, etc.). A runtime flag there is fragile — a single missed check leaks the secret.

A separate entity with a distinct type makes accidental leakage a compile error: secrets and variables literally can't be mixed. Downside: more surface area (schema + package + router) vs. a single field. I went with the safer option but happy to pivot if you prefer the flag approach.

Security model

  • Encrypted at rest — AES-256-GCM, reusing @typebot.io/credentials encrypt/decrypt (same ENCRYPTION_SECRET).
  • Server-only resolution — secrets are resolved just-in-time inside explicitly marked server-side execution points. This PR wires HTTP request block (URL, headers, body, basic auth). Email/other integrations can follow.
  • Never persisted at runtime — secrets are never written to Result.variables, SetVariableHistoryItem, chat replies, or any client-visible payload. They're fetched, substituted, and discarded.
  • Distinct typeWorkspaceSecret is separate from Variable, so they can't be mixed by accident.
  • assertNoSecretReferences helper is exported for builder-side validation (UI wiring comes in PR Sign in by email doesn't work behind a company proxy #2) to reject {{$secrets.X}} in client-side contexts like text bubbles.
  • RBAC — only workspace admins can create/update/delete secrets (reuses isWriteWorkspaceForbidden). Members can reference them in typebots they already have write access to.

Scope of this PR

This is PR 1 of ~3 (kept small for easier review):

  • PR 1 (this one): Schema, package, tRPC router, HTTP request block integration, unit tests.
  • PR 2 (held): Builder UI (Workspace Settings → Secrets tab, autocomplete in HTTP block fields, save-time validation).
  • PR 3 (held): Docs.

I'm not opening PR 2/3 until you validate the design here so we don't waste effort if you want a different approach.

Files changed

New package @typebot.io/workspace-secrets:

  • constants.ts{{$secrets.NAME}} regex (uppercase, digits, underscores; max 64 chars)
  • schemas.ts — Zod schemas for name/value
  • encryptSecretValue.ts / decryptSecretValue.ts — thin wrappers over credentials encrypt
  • extractSecretReferences.ts — pull names out of a string
  • getWorkspaceSecretsByNames.ts — batch fetch + decrypt by (workspaceId, names[])
  • resolveSecretReferences.ts — substitute {{$secrets.NAME}} with decrypted value
  • assertNoSecretReferences.ts — throw if a string contains secret refs (for builder validation)
  • 10 unit tests for regex/extract/assert

Prisma: new WorkspaceSecret model in both mysql and postgresql schemas (@@unique([workspaceId, name]), cascade on workspace delete). Migration needs to be generated by a maintainer with access to the migration tooling.

tRPC router: apps/builder/src/features/workspaceSecrets/api/ with createWorkspaceSecret, listWorkspaceSecrets (metadata only, no values), updateWorkspaceSecret, deleteWorkspaceSecret. Registered in apps/builder/src/app/api/router.ts as workspaceSecrets.

HTTP request block integration: parseHttpRequestAttributes in packages/bot-engine/src/blocks/integrations/httpRequest/executeHttpRequestBlock.ts now resolves secret references in URL, headers, body and basic auth after variable substitution. Server-only — viewer never sees the values.

Questions for you

  1. Design direction — separate entity vs. isSecret flag on Variable. I went with separate; which do you prefer?
  2. Syntax{{$secrets.NAME}}. Alternatives considered: {{secret:NAME}}, {{@NAME}}. Open to changing before UI work.
  3. Scope of server-side resolution — this PR covers HTTP request block only. Should I extend to email/Stripe/WhatsApp/forge blocks in this PR, or split into a follow-up?
  4. Feature flag — should I gate this behind an env var (e.g. NEXT_PUBLIC_WORKSPACE_SECRETS_ENABLED) so it can merge early and enable progressively?
  5. Migration — want me to generate the Prisma migration file, or will you handle that?

Test plan

  • bunx nx typecheck — passes for new/modified packages
  • bunx nx test @typebot.io/workspace-secrets — 10/10 unit tests passing (regex, extract, assert)
  • bunx nx format-and-lint --fix — clean
  • Pre-commit hook (nx affected -t format-and-lint,lint-repo,check-broken-links,test) — all green
  • Manual end-to-end test (blocked on UI — will cover in PR Sign in by email doesn't work behind a company proxy #2)
  • Integration test that verifies secrets never appear in Result.variables or SetVariableHistoryItem (happy to add before merge)

🤖 Generated with Claude Code

Introduces a new workspace-scoped `WorkspaceSecret` entity that lets users
define reusable encrypted values (API keys, tokens) and reference them from
typebots via `{{$secrets.NAME}}`.

Security model:
- AES-256-GCM at rest (reuses @typebot.io/credentials encrypt/decrypt).
- Resolution only happens in explicitly marked server-side execution points.
  This PR wires it into the HTTP request block (URL, headers, body, basic
  auth). Additional integrations (email, etc.) can follow in a separate PR.
- Secrets are never written to Result.variables, SetVariableHistoryItem, or
  any client-visible payload. They are fetched and substituted just-in-time
  inside the block executor and discarded.
- WorkspaceSecret is a distinct entity from Variable, so there is no way to
  accidentally mix a secret into a plaintext variable pipeline.
- assertNoSecretReferences helper is available to make builder-side
  validation reject `{{$secrets.X}}` in client-side contexts (UI wiring
  comes in the follow-up PR).

Refs baptisteArno#627

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 15, 2026

@klinux is attempting to deploy a commit to the Typebot Team on Vercel.

A member of the Team first needs to authorize it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant