Skip to content

Commit 31bc57e

Browse files
authored
Add encryption documentation and world features manifest (vercel#1432)
* Add encryption documentation and world features manifest - Add encryption.mdx under How it Works explaining end-to-end encryption - Add features array to worlds-manifest.json (encryption for Vercel World) - Display encryption support in WorldDetailHero and WorldCardSimple - Add cross-references from Vercel World and Observability pages * Derive WorldFeature type from const array * Address review feedback: simplify key explanation, show 'No' for missing encryption, fix tooltip
1 parent cd4abd8 commit 31bc57e

10 files changed

Lines changed: 190 additions & 35 deletions

File tree

.changeset/metal-ghosts-cut.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

docs/components/worlds/WorldCardSimple.tsx

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
CheckCircle2,
77
Clock,
88
HeartHandshake,
9+
ShieldCheck,
910
XCircle,
1011
} from 'lucide-react';
1112
import Link from 'next/link';
@@ -140,26 +141,35 @@ export function WorldCardSimple({ id, world }: WorldCardSimpleProps) {
140141
<p className="text-xs">E2E Test Suite Coverage</p>
141142
</TooltipContent>
142143
</Tooltip>
143-
{/* PERF - right */}
144-
{/* <Tooltip>
144+
{/* Encryption - right */}
145+
<Tooltip>
145146
<TooltipTrigger asChild>
146147
<div className="flex items-center gap-1.5 px-4 py-2.5 text-sm">
147-
<Timer className="h-3.5 w-3.5 text-purple-500" />
148-
<span className="text-muted-foreground">PERF</span>
149-
<span className="font-mono text-foreground">
150-
{world.benchmark10SeqMs !== null
151-
? `${(world.benchmark10SeqMs / 1000).toFixed(2)}s`
152-
: '—'}
148+
<ShieldCheck
149+
className={cn(
150+
'h-3.5 w-3.5',
151+
world.features.includes('encryption')
152+
? 'text-green-600/70'
153+
: 'text-red-600/70'
154+
)}
155+
/>
156+
<span className="text-muted-foreground">Encrypted</span>
157+
<span
158+
className={cn(
159+
'font-mono',
160+
world.features.includes('encryption')
161+
? 'text-green-600/70'
162+
: 'text-red-600/70'
163+
)}
164+
>
165+
{world.features.includes('encryption') ? 'Yes' : 'No'}
153166
</span>
154167
</div>
155168
</TooltipTrigger>
156-
<TooltipContent side="bottom" className="max-w-[260px]">
157-
<p className="text-xs">
158-
Avg time to run a 10 step workflow where each step sleeps 1
159-
second
160-
</p>
169+
<TooltipContent side="bottom" className="max-w-[200px]">
170+
<p className="text-xs">End-to-end user data encryption</p>
161171
</TooltipContent>
162-
</Tooltip> */}
172+
</Tooltip>
163173
</div>
164174
</Card>
165175
</Link>

docs/components/worlds/WorldDetailHero.tsx

Lines changed: 41 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,25 @@
11
'use client';
22

3-
import { useState } from 'react';
43
import {
5-
ExternalLink,
6-
ChevronRight,
7-
CheckIcon,
8-
CopyIcon,
4+
AlertCircle,
95
BadgeCheck,
10-
HeartHandshake,
116
CheckCircle2,
12-
AlertCircle,
13-
XCircle,
7+
CheckIcon,
8+
ChevronRight,
149
Clock,
15-
Timer,
16-
Package,
17-
Github,
1810
Code,
11+
CopyIcon,
12+
ExternalLink,
13+
Github,
14+
HeartHandshake,
15+
Package,
16+
ShieldCheck,
17+
Timer,
18+
XCircle,
1919
} from 'lucide-react';
2020
import Link from 'next/link';
21+
import { useState } from 'react';
2122
import { toast } from 'sonner';
22-
23-
import {
24-
Tooltip,
25-
TooltipContent,
26-
TooltipTrigger,
27-
} from '@/components/ui/tooltip';
2823
import {
2924
Breadcrumb,
3025
BreadcrumbItem,
@@ -34,6 +29,11 @@ import {
3429
BreadcrumbSeparator,
3530
} from '@/components/ui/breadcrumb';
3631
import { Button } from '@/components/ui/button';
32+
import {
33+
Tooltip,
34+
TooltipContent,
35+
TooltipTrigger,
36+
} from '@/components/ui/tooltip';
3737

3838
import type { World } from './types';
3939

@@ -305,6 +305,30 @@ export function WorldDetailHero({ id, world }: WorldDetailHeroProps) {
305305
<span>Example</span>
306306
</a>
307307
)}
308+
309+
{/* Encryption */}
310+
{world.features.includes('encryption') && (
311+
<Tooltip>
312+
<TooltipTrigger asChild>
313+
<Link
314+
href="/docs/how-it-works/encryption"
315+
className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
316+
>
317+
<ShieldCheck className="h-4 w-4 text-green-500" />
318+
<span>E2E Encrypted</span>
319+
</Link>
320+
</TooltipTrigger>
321+
<TooltipContent
322+
side="top"
323+
align="start"
324+
className="max-w-[200px]"
325+
>
326+
<p className="text-xs">
327+
User data is encrypted end-to-end in the event log
328+
</p>
329+
</TooltipContent>
330+
</Tooltip>
331+
)}
308332
</div>
309333
</div>
310334
</section>

docs/components/worlds/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ export interface WorldBenchmark {
4545
lastRun: string | null;
4646
}
4747

48+
/**
49+
* Known world features declared in worlds-manifest.json.
50+
* Each feature corresponds to an optional World interface method.
51+
*/
52+
export const WORLD_FEATURES = ['encryption'] as const;
53+
export type WorldFeature = (typeof WORLD_FEATURES)[number];
54+
4855
export interface World {
4956
type: 'official' | 'community';
5057
name: string;
@@ -53,6 +60,7 @@ export interface World {
5360
docs: string;
5461
repository?: string;
5562
example?: string;
63+
features: WorldFeature[];
5664
e2e: WorldE2E | null;
5765
benchmark: WorldBenchmark | null;
5866
/**

docs/content/docs/deploying/world/vercel-world.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ summary: Deploy workflows to Vercel with fully-managed storage, queuing, and aut
66
prerequisites:
77
- /docs/deploying
88
related:
9+
- /docs/how-it-works/encryption
910
- /docs/deploying/world/local-world
1011
- /docs/deploying/world/postgres-world
1112
---
@@ -125,7 +126,7 @@ This ensures long-running workflows complete reliably without being affected by
125126

126127
The Vercel World uses Vercel's infrastructure for workflow execution:
127128

128-
- **Storage** - Workflow data is stored in Vercel's cloud with automatic replication and encryption
129+
- **Storage** - Workflow data is stored in Vercel's cloud with automatic replication and [end-to-end encryption](/docs/how-it-works/encryption)
129130
- **Queuing** - Steps are distributed across serverless functions with automatic retries
130131
- **Authentication** - OIDC tokens provide secure, automatic authentication
131132

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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.

docs/content/docs/how-it-works/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"understanding-directives",
55
"code-transform",
66
"framework-integrations",
7-
"event-sourcing"
7+
"event-sourcing",
8+
"encryption"
89
],
910
"defaultOpen": false
1011
}

docs/content/docs/observability/index.mdx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ prerequisites:
77
- /docs/foundations
88
related:
99
- /docs/how-it-works/event-sourcing
10+
- /docs/how-it-works/encryption
1011
---
1112

1213
Workflow DevKit provides powerful tools to inspect, monitor, and debug your workflows through the CLI and Web UI. These tools allow you to inspect workflow runs, steps, webhooks, [events](/docs/how-it-works/event-sourcing), and stream output.
@@ -60,3 +61,5 @@ To inspect workflows running on Vercel, ensure you're logged in to the Vercel CL
6061
# Inspect workflows running on Vercel
6162
npx workflow inspect runs --backend vercel
6263
```
64+
65+
When deployed to Vercel, workflow data is [encrypted end-to-end](/docs/how-it-works/encryption). Encrypted fields display as locked placeholders until you choose to decrypt them using the **Decrypt** button in the web UI or the `--decrypt` flag in the CLI.

docs/lib/worlds-data.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
*/
55

66
import { unstable_cache } from 'next/cache';
7-
import type { World, WorldsStatus } from '@/components/worlds/types';
7+
import type {
8+
World,
9+
WorldFeature,
10+
WorldsStatus,
11+
} from '@/components/worlds/types';
812

913
// Import manifest data at build time
1014
import worldsManifest from '../../worlds-manifest.json';
@@ -90,6 +94,8 @@ function buildInitialWorldsStatus(): Record<string, World> {
9094
docs: world.docs,
9195
repository: (world as { repository?: string }).repository,
9296
example: (world as { example?: string }).example,
97+
features: ((world as { features?: string[] }).features ??
98+
[]) as WorldFeature[],
9399
e2e: null,
94100
benchmark: null,
95101
benchmark10SeqMs: null,

worlds-manifest.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"description": "Zero-config world bundled with Workflow for local development. No external services required.",
99
"docs": "/docs/deploying/world/local-world",
1010
"env": {},
11-
"services": []
11+
"services": [],
12+
"features": []
1213
},
1314
{
1415
"id": "postgres",
@@ -18,6 +19,7 @@
1819
"description": "Production-ready, self-hosted world using PostgreSQL for durable storage and graphile-worker for reliable job processing.",
1920
"docs": "/docs/deploying/world/postgres-world",
2021
"example": "https://github.com/vercel/workflow-examples/tree/main/postgres",
22+
"features": [],
2123
"env": {
2224
"WORKFLOW_TARGET_WORLD": "@workflow/world-postgres",
2325
"WORKFLOW_POSTGRES_URL": "postgres://world:world@localhost:5432/world"
@@ -49,6 +51,7 @@
4951
"name": "Vercel",
5052
"description": "Fully-managed world for Vercel deployments. Zero config, infinitely scalable, built-in authentication.",
5153
"docs": "/docs/deploying/world/vercel-world",
54+
"features": ["encryption"],
5255
"env": {
5356
"WORKFLOW_VERCEL_ENV": "production"
5457
},
@@ -63,6 +66,7 @@
6366
"description": "Turso/libSQL World for embedded or remote SQLite databases",
6467
"repository": "https://github.com/mizzle-dev/workflow-worlds",
6568
"docs": "https://github.com/mizzle-dev/workflow-worlds/tree/main/packages/turso",
69+
"features": [],
6670
"env": {
6771
"WORKFLOW_TARGET_WORLD": "@workflow-worlds/turso",
6872
"WORKFLOW_TURSO_DATABASE_URL": "file:workflow.db"
@@ -78,6 +82,7 @@
7882
"description": "MongoDB World using native driver",
7983
"repository": "https://github.com/mizzle-dev/workflow-worlds",
8084
"docs": "https://github.com/mizzle-dev/workflow-worlds/tree/main/packages/mongodb",
85+
"features": [],
8186
"env": {
8287
"WORKFLOW_TARGET_WORLD": "@workflow-worlds/mongodb",
8388
"WORKFLOW_MONGODB_URI": "mongodb://localhost:27017",
@@ -105,6 +110,7 @@
105110
"description": "Redis World using BullMQ for queues, Redis Streams for output",
106111
"repository": "https://github.com/mizzle-dev/workflow-worlds",
107112
"docs": "https://github.com/mizzle-dev/workflow-worlds/tree/main/packages/redis",
113+
"features": [],
108114
"env": {
109115
"WORKFLOW_TARGET_WORLD": "@workflow-worlds/redis",
110116
"WORKFLOW_REDIS_URI": "redis://localhost:6379"
@@ -131,6 +137,7 @@
131137
"description": "Jazz Cloud world for local-first sync and real-time collaboration",
132138
"repository": "https://github.com/garden-co/workflow-world-jazz",
133139
"docs": "https://github.com/garden-co/workflow-world-jazz",
140+
"features": [],
134141
"env": {
135142
"WORKFLOW_TARGET_WORLD": "workflow-world-jazz"
136143
},

0 commit comments

Comments
 (0)