Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/metal-ghosts-cut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
Comment thread
TooTallNate marked this conversation as resolved.
38 changes: 24 additions & 14 deletions docs/components/worlds/WorldCardSimple.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
CheckCircle2,
Clock,
HeartHandshake,
ShieldCheck,
XCircle,
} from 'lucide-react';
import Link from 'next/link';
Expand Down Expand Up @@ -140,26 +141,35 @@ export function WorldCardSimple({ id, world }: WorldCardSimpleProps) {
<p className="text-xs">E2E Test Suite Coverage</p>
</TooltipContent>
</Tooltip>
{/* PERF - right */}
{/* <Tooltip>
{/* Encryption - right */}
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 px-4 py-2.5 text-sm">
<Timer className="h-3.5 w-3.5 text-purple-500" />
<span className="text-muted-foreground">PERF</span>
<span className="font-mono text-foreground">
{world.benchmark10SeqMs !== null
? `${(world.benchmark10SeqMs / 1000).toFixed(2)}s`
: '—'}
<ShieldCheck
className={cn(
'h-3.5 w-3.5',
world.features.includes('encryption')
? 'text-green-600/70'
: 'text-red-600/70'
)}
/>
<span className="text-muted-foreground">Encrypted</span>
<span
className={cn(
'font-mono',
world.features.includes('encryption')
? 'text-green-600/70'
: 'text-red-600/70'
)}
>
{world.features.includes('encryption') ? 'Yes' : 'No'}
</span>
</div>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-[260px]">
<p className="text-xs">
Avg time to run a 10 step workflow where each step sleeps 1
second
</p>
<TooltipContent side="bottom" className="max-w-[200px]">
<p className="text-xs">End-to-end user data encryption</p>
</TooltipContent>
</Tooltip> */}
</Tooltip>
</div>
</Card>
</Link>
Expand Down
58 changes: 41 additions & 17 deletions docs/components/worlds/WorldDetailHero.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,25 @@
'use client';

import { useState } from 'react';
import {
ExternalLink,
ChevronRight,
CheckIcon,
CopyIcon,
AlertCircle,
BadgeCheck,
HeartHandshake,
CheckCircle2,
AlertCircle,
XCircle,
CheckIcon,
ChevronRight,
Clock,
Timer,
Package,
Github,
Code,
CopyIcon,
ExternalLink,
Github,
HeartHandshake,
Package,
ShieldCheck,
Timer,
XCircle,
} from 'lucide-react';
import Link from 'next/link';
import { useState } from 'react';
import { toast } from 'sonner';

import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import {
Breadcrumb,
BreadcrumbItem,
Expand All @@ -34,6 +29,11 @@ import {
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';

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

Expand Down Expand Up @@ -305,6 +305,30 @@ export function WorldDetailHero({ id, world }: WorldDetailHeroProps) {
<span>Example</span>
</a>
)}

{/* Encryption */}
{world.features.includes('encryption') && (
<Tooltip>
<TooltipTrigger asChild>
<Link
href="/docs/how-it-works/encryption"
className="flex items-center gap-2 text-muted-foreground hover:text-foreground transition-colors"
>
<ShieldCheck className="h-4 w-4 text-green-500" />
<span>E2E Encrypted</span>
</Link>
</TooltipTrigger>
<TooltipContent
side="top"
align="start"
className="max-w-[200px]"
>
<p className="text-xs">
User data is encrypted end-to-end in the event log
</p>
</TooltipContent>
</Tooltip>
)}
</div>
</div>
</section>
Expand Down
8 changes: 8 additions & 0 deletions docs/components/worlds/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ export interface WorldBenchmark {
lastRun: string | null;
}

/**
* Known world features declared in worlds-manifest.json.
* Each feature corresponds to an optional World interface method.
*/
export const WORLD_FEATURES = ['encryption'] as const;
export type WorldFeature = (typeof WORLD_FEATURES)[number];

export interface World {
type: 'official' | 'community';
name: string;
Expand All @@ -53,6 +60,7 @@ export interface World {
docs: string;
repository?: string;
example?: string;
features: WorldFeature[];
e2e: WorldE2E | null;
benchmark: WorldBenchmark | null;
/**
Expand Down
3 changes: 2 additions & 1 deletion docs/content/docs/deploying/world/vercel-world.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ summary: Deploy workflows to Vercel with fully-managed storage, queuing, and aut
prerequisites:
- /docs/deploying
related:
- /docs/how-it-works/encryption
- /docs/deploying/world/local-world
- /docs/deploying/world/postgres-world
---
Expand Down Expand Up @@ -125,7 +126,7 @@ This ensures long-running workflows complete reliably without being affected by

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

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

Expand Down
93 changes: 93 additions & 0 deletions docs/content/docs/how-it-works/encryption.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: Encryption
description: Learn how Workflow DevKit encrypts user data end-to-end in the event log.
type: conceptual
summary: Understand how workflow and step data is encrypted at rest.
prerequisites:
- /docs/how-it-works/event-sourcing
related:
- /docs/observability
- /docs/deploying/world/vercel-world
---

<Callout>
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.
</Callout>

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.

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.

## What Is Encrypted

All user data flowing through the event log is encrypted:

- **Workflow inputs** — arguments passed when starting a workflow
- **Workflow return values** — the final output of a workflow
- **Step inputs** — arguments passed to step functions
- **Step return values** — the result returned by step functions
- **Hook metadata** — data attached when creating a hook
- **Hook payloads** — data received by hooks and webhooks
- **Stream data** — each frame in a `ReadableStream` or `WritableStream`

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.

## How It Works

### Key Management

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`.

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.

### Encryption Algorithm

Data is encrypted using **AES-256-GCM** via the [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API):

- A random 12-byte nonce is generated for each encryption operation
- The GCM authentication tag provides integrity verification — any tampering with the ciphertext is detected
- The same plaintext produces different ciphertext each time due to the random nonce

## Decrypting Data

When viewing workflow runs through the observability tools, encrypted fields display as locked placeholders until you explicitly choose to decrypt them.

### Permissions

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.

### Web Dashboard

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.

### CLI

Add the `--decrypt` flag to any `inspect` command:

```bash
# Inspect a specific run
npx workflow inspect run <run-id> --decrypt

# Inspect a specific step
npx workflow inspect step <step-id> --run <run-id> --decrypt

# List events for a run
npx workflow inspect events --run <run-id> --decrypt

# Inspect a specific stream
npx workflow inspect stream <stream-id> --run <run-id> --decrypt
```

Without `--decrypt`, encrypted fields display as `🔒 Encrypted` placeholders.

## Custom World Implementations

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.

To add encryption support to a custom `World`:

1. Implement `getEncryptionKeyForRun(runId: string)` on your `World` class
2. Return the raw 32-byte key as a `Uint8Array` — the core runtime uses it for AES-256-GCM operations
3. Ensure the same key is returned for the same run ID across invocations (for decryption during replay)
Comment on lines +85 to +91

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.
3 changes: 2 additions & 1 deletion docs/content/docs/how-it-works/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"understanding-directives",
"code-transform",
"framework-integrations",
"event-sourcing"
"event-sourcing",
"encryption"
],
"defaultOpen": false
}
3 changes: 3 additions & 0 deletions docs/content/docs/observability/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ prerequisites:
- /docs/foundations
related:
- /docs/how-it-works/event-sourcing
- /docs/how-it-works/encryption
---

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.
Expand Down Expand Up @@ -60,3 +61,5 @@ To inspect workflows running on Vercel, ensure you're logged in to the Vercel CL
# Inspect workflows running on Vercel
npx workflow inspect runs --backend vercel
```

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.
8 changes: 7 additions & 1 deletion docs/lib/worlds-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
*/

import { unstable_cache } from 'next/cache';
import type { World, WorldsStatus } from '@/components/worlds/types';
import type {
World,
WorldFeature,
WorldsStatus,
} from '@/components/worlds/types';

// Import manifest data at build time
import worldsManifest from '../../worlds-manifest.json';
Expand Down Expand Up @@ -90,6 +94,8 @@ function buildInitialWorldsStatus(): Record<string, World> {
docs: world.docs,
repository: (world as { repository?: string }).repository,
example: (world as { example?: string }).example,
features: ((world as { features?: string[] }).features ??
[]) as WorldFeature[],
e2e: null,
Comment on lines +97 to 99
benchmark: null,
benchmark10SeqMs: null,
Expand Down
9 changes: 8 additions & 1 deletion worlds-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"description": "Zero-config world bundled with Workflow for local development. No external services required.",
"docs": "/docs/deploying/world/local-world",
"env": {},
"services": []
"services": [],
"features": []
},
{
"id": "postgres",
Expand All @@ -18,6 +19,7 @@
"description": "Production-ready, self-hosted world using PostgreSQL for durable storage and graphile-worker for reliable job processing.",
"docs": "/docs/deploying/world/postgres-world",
"example": "https://github.com/vercel/workflow-examples/tree/main/postgres",
"features": [],
"env": {
"WORKFLOW_TARGET_WORLD": "@workflow/world-postgres",
"WORKFLOW_POSTGRES_URL": "postgres://world:world@localhost:5432/world"
Expand Down Expand Up @@ -49,6 +51,7 @@
"name": "Vercel",
"description": "Fully-managed world for Vercel deployments. Zero config, infinitely scalable, built-in authentication.",
"docs": "/docs/deploying/world/vercel-world",
"features": ["encryption"],
"env": {
"WORKFLOW_VERCEL_ENV": "production"
},
Expand All @@ -63,6 +66,7 @@
"description": "Turso/libSQL World for embedded or remote SQLite databases",
"repository": "https://github.com/mizzle-dev/workflow-worlds",
"docs": "https://github.com/mizzle-dev/workflow-worlds/tree/main/packages/turso",
"features": [],
"env": {
"WORKFLOW_TARGET_WORLD": "@workflow-worlds/turso",
"WORKFLOW_TURSO_DATABASE_URL": "file:workflow.db"
Expand All @@ -78,6 +82,7 @@
"description": "MongoDB World using native driver",
"repository": "https://github.com/mizzle-dev/workflow-worlds",
"docs": "https://github.com/mizzle-dev/workflow-worlds/tree/main/packages/mongodb",
"features": [],
"env": {
"WORKFLOW_TARGET_WORLD": "@workflow-worlds/mongodb",
"WORKFLOW_MONGODB_URI": "mongodb://localhost:27017",
Expand Down Expand Up @@ -105,6 +110,7 @@
"description": "Redis World using BullMQ for queues, Redis Streams for output",
"repository": "https://github.com/mizzle-dev/workflow-worlds",
"docs": "https://github.com/mizzle-dev/workflow-worlds/tree/main/packages/redis",
"features": [],
"env": {
"WORKFLOW_TARGET_WORLD": "@workflow-worlds/redis",
"WORKFLOW_REDIS_URI": "redis://localhost:6379"
Expand All @@ -131,6 +137,7 @@
"description": "Jazz Cloud world for local-first sync and real-time collaboration",
"repository": "https://github.com/garden-co/workflow-world-jazz",
"docs": "https://github.com/garden-co/workflow-world-jazz",
"features": [],
"env": {
"WORKFLOW_TARGET_WORLD": "workflow-world-jazz"
},
Expand Down
Loading