|
| 1 | +--- |
| 2 | +title: Encryption |
| 3 | +description: Learn how Workflow DevKit encrypts user data end-to-end in the event log. |
| 4 | +type: conceptual |
| 5 | +summary: Understand how workflow and step data is encrypted at rest. |
| 6 | +prerequisites: |
| 7 | + - /docs/how-it-works/event-sourcing |
| 8 | +related: |
| 9 | + - /docs/observability |
| 10 | + - /docs/deploying/world/vercel-world |
| 11 | +--- |
| 12 | + |
| 13 | +<Callout> |
| 14 | +This guide explains how Workflow DevKit encrypts user data in the event log. Understanding these details is not required to use workflows — encryption is automatic and requires no code changes. For getting started, see the [getting started](/docs/getting-started) guides for your framework. |
| 15 | +</Callout> |
| 16 | + |
| 17 | +Workflow DevKit supports automatic end-to-end encryption of all user data before it is written to the event log. When a `World` implementation provides encryption support, it is safe to pass sensitive data — such as API keys, tokens, or user credentials — as workflow inputs, step arguments, and return values. The storage backend only ever sees ciphertext. |
| 18 | + |
| 19 | +Encryption support varies by `World` implementation. See the [Worlds](/worlds) page to check which worlds support this feature. `World` implementations opt into encryption by providing a `getEncryptionKeyForRun()` method — the core runtime will use it automatically when present. |
| 20 | + |
| 21 | +## What Is Encrypted |
| 22 | + |
| 23 | +All user data flowing through the event log is encrypted: |
| 24 | + |
| 25 | +- **Workflow inputs** — arguments passed when starting a workflow |
| 26 | +- **Workflow return values** — the final output of a workflow |
| 27 | +- **Step inputs** — arguments passed to step functions |
| 28 | +- **Step return values** — the result returned by step functions |
| 29 | +- **Hook metadata** — data attached when creating a hook |
| 30 | +- **Hook payloads** — data received by hooks and webhooks |
| 31 | +- **Stream data** — each frame in a `ReadableStream` or `WritableStream` |
| 32 | + |
| 33 | +Metadata such as workflow names, step names, entity IDs, timestamps, and lifecycle states are **not** encrypted. This allows the observability tools to display run structure and timelines without requiring decryption. |
| 34 | + |
| 35 | +## How It Works |
| 36 | + |
| 37 | +### Key Management |
| 38 | + |
| 39 | +Each workflow run is encrypted with its own unique key, provided by the `World` implementation via `getEncryptionKeyForRun()`. How the key is generated and stored is up to the `World`. |
| 40 | + |
| 41 | +For example, the [Vercel World](/docs/deploying/world/vercel-world) provides unique keys per run and execution environment, ensuring that a given run can only decrypt data from that run itself. |
| 42 | + |
| 43 | +### Encryption Algorithm |
| 44 | + |
| 45 | +Data is encrypted using **AES-256-GCM** via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API): |
| 46 | + |
| 47 | +- A random 12-byte nonce is generated for each encryption operation |
| 48 | +- The GCM authentication tag provides integrity verification — any tampering with the ciphertext is detected |
| 49 | +- The same plaintext produces different ciphertext each time due to the random nonce |
| 50 | + |
| 51 | +## Decrypting Data |
| 52 | + |
| 53 | +When viewing workflow runs through the observability tools, encrypted fields display as locked placeholders until you explicitly choose to decrypt them. |
| 54 | + |
| 55 | +### Permissions |
| 56 | + |
| 57 | +Decryption access is controlled by the `World` implementation. On Vercel, decryption follows the same permissions model as project environment variables — if you don't have permission to view environment variable values for a project, you won't be able to decrypt workflow data either. Each decryption request is recorded in your [Vercel audit log](https://vercel.com/docs/audit-log), giving your team full visibility into when and by whom workflow data was accessed. |
| 58 | + |
| 59 | +### Web Dashboard |
| 60 | + |
| 61 | +Click the **Decrypt** button in the run detail panel to decrypt all data fields. Decryption happens entirely in the browser via the Web Crypto API — the observability server retrieves the encryption key but never sees your plaintext data. |
| 62 | + |
| 63 | +### CLI |
| 64 | + |
| 65 | +Add the `--decrypt` flag to any `inspect` command: |
| 66 | + |
| 67 | +```bash |
| 68 | +# Inspect a specific run |
| 69 | +npx workflow inspect run <run-id> --decrypt |
| 70 | + |
| 71 | +# Inspect a specific step |
| 72 | +npx workflow inspect step <step-id> --run <run-id> --decrypt |
| 73 | + |
| 74 | +# List events for a run |
| 75 | +npx workflow inspect events --run <run-id> --decrypt |
| 76 | + |
| 77 | +# Inspect a specific stream |
| 78 | +npx workflow inspect stream <stream-id> --run <run-id> --decrypt |
| 79 | +``` |
| 80 | + |
| 81 | +Without `--decrypt`, encrypted fields display as `🔒 Encrypted` placeholders. |
| 82 | + |
| 83 | +## Custom World Implementations |
| 84 | + |
| 85 | +The core runtime encrypts data automatically when the `World` implementation provides a `getEncryptionKeyForRun()` method. This method receives the run ID and returns the raw encryption key bytes. |
| 86 | + |
| 87 | +To add encryption support to a custom `World`: |
| 88 | + |
| 89 | +1. Implement `getEncryptionKeyForRun(runId: string)` on your `World` class |
| 90 | +2. Return the raw 32-byte key as a `Uint8Array` — the core runtime uses it for AES-256-GCM operations |
| 91 | +3. Ensure the same key is returned for the same run ID across invocations (for decryption during replay) |
| 92 | + |
| 93 | +The [Vercel World](/docs/deploying/world/vercel-world) implementation uses HKDF derivation from a deployment-scoped key, but any consistent key management scheme will work. |
0 commit comments