Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ A template for building your own workflow automation platform. Built on top of W

![Workflow Builder Screenshot](screenshot.png)

## Deploy Your Own

You can deploy your own version of the workflow builder to Vercel with one click:

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fworkflow-builder-template&project-name=workflow-builder&repository-name=workflow-builder&demo-title=Workflow%20Builder&demo-description=A%20free%2C%20open-source%20template%20for%20building%20visual%20workflow%20automation%20platforms%20with%20real%20integrations%20and%20code%20generation&demo-url=https%3A%2F%2Fworkflow-builder-template.vercel.app&demo-image=https%3A%2F%2Fraw.githubusercontent.com%2Fvercel-labs%2Fworkflow-builder-template%2Fmain%2Fscreenshot.png&env=BETTER_AUTH_SECRET,INTEGRATION_ENCRYPTION_KEY,AI_GATEWAY_API_KEY&envDescription=BETTER_AUTH_SECRET+and+INTEGRATION_ENCRYPTION_KEY+are+required+secrets.+AI_GATEWAY_API_KEY+is+optional.&stores=%5B%7B%22type%22%3A%22postgres%22%7D%5D)

**What happens during deployment:**

- **Automatic Database Setup**: A Neon Postgres database is automatically created and connected to your project
- **Environment Configuration**: You'll be prompted to provide required environment variables (Better Auth credentials and AI Gateway API key)
- **Ready to Use**: After deployment, you can start building workflows immediately

## What's Included

- **Visual Workflow Builder** - Drag-and-drop interface powered by React Flow
Expand Down Expand Up @@ -71,14 +83,6 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **Database Query** - Query or update PostgreSQL
- **HTTP Request** - Call external APIs

### Condition Nodes

- Conditional branching based on data

### Transform Nodes

- Data transformation and mapping

## Code Generation

Workflows can be converted to executable TypeScript code with the `"use workflow"` directive:
Expand Down
2 changes: 1 addition & 1 deletion lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Vercel deployment configuration
export const VERCEL_DEPLOY_URL =
"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fworkflow-builder-template&env=DATABASE_URL,BETTER_AUTH_SECRET,BETTER_AUTH_URL,VERCEL_API_TOKEN,OPENAI_API_KEY&envDescription=Required+environment+variables+for+workflow-builder-template.+See+README+for+details.&stores=%5B%7B%22type%22%3A%22postgres%22%7D%5D&project-name=workflow-builder-template&repository-name=workflow-builder-template";
"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel-labs%2Fworkflow-builder-template&project-name=workflow-builder&repository-name=workflow-builder&demo-title=Workflow%20Builder&demo-description=A%20free%2C%20open-source%20template%20for%20building%20visual%20workflow%20automation%20platforms%20with%20real%20integrations%20and%20code%20generation&demo-url=https%3A%2F%2Fworkflow-builder-template.vercel.app&demo-image=https%3A%2F%2Fraw.githubusercontent.com%2Fvercel-labs%2Fworkflow-builder-template%2Fmain%2Fscreenshot.png&env=BETTER_AUTH_SECRET,INTEGRATION_ENCRYPTION_KEY,AI_GATEWAY_API_KEY&envDescription=BETTER_AUTH_SECRET+and+INTEGRATION_ENCRYPTION_KEY+are+required+secrets.+AI_GATEWAY_API_KEY+is+optional.&stores=%5B%7B%22type%22%3A%22postgres%22%7D%5D";

// Vercel button URL for markdown
export const VERCEL_DEPLOY_BUTTON_URL = `[![Deploy with Vercel](https://vercel.com/button)](${VERCEL_DEPLOY_URL})`;
22 changes: 11 additions & 11 deletions lib/db/integrations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import "server-only";

import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
import {
createCipheriv,
createDecipheriv,
createHash,
randomBytes,
} from "node:crypto";
import { and, eq } from "drizzle-orm";
import { db } from "./index";
import { integrations, type NewIntegration } from "./schema";
Expand All @@ -12,24 +17,19 @@ const ENCRYPTION_KEY_ENV = "INTEGRATION_ENCRYPTION_KEY";

/**
* Get or generate encryption key from environment
* Key should be a 32-byte hex string (64 characters)
* Accepts any string and hashes it to a 32-byte key
*/
function getEncryptionKey(): Buffer {
const keyHex = process.env[ENCRYPTION_KEY_ENV];
Copy link
Contributor

@vercel vercel bot Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change from hex-string-based key derivation to SHA256-hashing introduces a breaking change that could cause data loss if existing encrypted credentials are migrated to this new version.

View Details
📝 Patch Details
diff --git a/lib/db/integrations.ts b/lib/db/integrations.ts
index 7a186fe..313a63e 100644
--- a/lib/db/integrations.ts
+++ b/lib/db/integrations.ts
@@ -18,8 +18,9 @@ const ENCRYPTION_KEY_ENV = "INTEGRATION_ENCRYPTION_KEY";
 /**
  * Get or generate encryption key from environment
  * Accepts any string and hashes it to a 32-byte key
+ * For backward compatibility, also supports 64-character hex strings (legacy format)
  */
-function getEncryptionKey(): Buffer {
+function getEncryptionKey(useLegacy: boolean = false): Buffer {
   const keyString = process.env[ENCRYPTION_KEY_ENV];
 
   if (!keyString) {
@@ -28,7 +29,17 @@ function getEncryptionKey(): Buffer {
     );
   }
 
-  // Hash the input string to get a consistent 32-byte key
+  if (useLegacy) {
+    // Legacy format: treat as 64-character hex string
+    if (keyString.length !== 64) {
+      throw new Error(
+        `${ENCRYPTION_KEY_ENV} must be a 64-character hex string (32 bytes) when using legacy decryption`
+      );
+    }
+    return Buffer.from(keyString, "hex");
+  }
+
+  // New format: hash the input string to get a consistent 32-byte key
   return createHash("sha256").update(keyString).digest();
 }
 
@@ -52,9 +63,9 @@ export function encrypt(plaintext: string): string {
 
 /**
  * Decrypt encrypted data
+ * Attempts to decrypt with the new key format first, then falls back to legacy format
  */
 export function decrypt(ciphertext: string): string {
-  const key = getEncryptionKey();
   const parts = ciphertext.split(":");
 
   if (parts.length !== 3) {
@@ -65,13 +76,32 @@ export function decrypt(ciphertext: string): string {
   const authTag = Buffer.from(parts[1], "hex");
   const encrypted = parts[2];
 
-  const decipher = createDecipheriv(ALGORITHM, key, iv);
-  decipher.setAuthTag(authTag);
+  // Try new key format first
+  try {
+    const key = getEncryptionKey(false);
+    const decipher = createDecipheriv(ALGORITHM, key, iv);
+    decipher.setAuthTag(authTag);
 
-  let decrypted = decipher.update(encrypted, "hex", "utf8");
-  decrypted += decipher.final("utf8");
+    let decrypted = decipher.update(encrypted, "hex", "utf8");
+    decrypted += decipher.final("utf8");
 
-  return decrypted;
+    return decrypted;
+  } catch (error) {
+    // If new format fails, try legacy format
+    try {
+      const key = getEncryptionKey(true);
+      const decipher = createDecipheriv(ALGORITHM, key, iv);
+      decipher.setAuthTag(authTag);
+
+      let decrypted = decipher.update(encrypted, "hex", "utf8");
+      decrypted += decipher.final("utf8");
+
+      return decrypted;
+    } catch (legacyError) {
+      // Both formats failed, throw the original error
+      throw error;
+    }
+  }
 }
 
 /**

Analysis

Breaking change in encryption key derivation causes silent data loss on upgrade

What fails: The getEncryptionKey() function was changed from treating the INTEGRATION_ENCRYPTION_KEY environment variable as a 64-character hex string to treating it as a plain string to hash with SHA256. This breaks decryption of existing encrypted credentials when upgrading between versions.

How to reproduce:

  1. With the old code, set INTEGRATION_ENCRYPTION_KEY=abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234 (64-char hex)
  2. Create and encrypt integration credentials, storing them in the database
  3. Update to the new version (without changing the env var)
  4. Attempt to retrieve the credentials by calling getIntegrations() or getIntegration()

Result: The encrypted credentials are decrypted with the new SHA256-based key, which produces a completely different key than the old hex-based derivation. The authentication tag mismatch causes decryption to fail with "Unsupported state or unable to authenticate data". The error is silently caught in decryptConfig() (lines 87-94), returning an empty object {} instead of the actual config.

Expected behavior: Existing encrypted credentials should remain readable after upgrading. New credentials can use the improved key derivation, but old data should not become inaccessible.

Root cause:

  • Old: Buffer.from(keyHex, 'hex') interprets the env var as hex-encoded binary (32 bytes directly)
  • New: createHash("sha256").update(keyString).digest() interprets the env var as a UTF-8 string to hash

For the same env var value, these produce entirely different 32-byte keys, rendering all old encrypted data unreadable.

Fix implemented: Modified decrypt() to support both key formats by attempting decryption with the new format first, then falling back to the legacy format if it fails. This allows seamless reading of both old encrypted data and new data, while all new encryptions use the improved format.

const keyString = process.env[ENCRYPTION_KEY_ENV];

if (!keyHex) {
if (!keyString) {
throw new Error(
`${ENCRYPTION_KEY_ENV} environment variable is required for encrypting integration credentials`
);
}

if (keyHex.length !== 64) {
throw new Error(
`${ENCRYPTION_KEY_ENV} must be a 64-character hex string (32 bytes)`
);
}

return Buffer.from(keyHex, "hex");
// Hash the input string to get a consistent 32-byte key
return createHash("sha256").update(keyString).digest();
}

/**
Expand Down
1 change: 0 additions & 1 deletion vercel-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@
}
]
}