Skip to content

Conversation

N2D4
Copy link
Contributor

@N2D4 N2D4 commented Sep 2, 2025


Important

Introduces per-project workflows with management capabilities, workflow token authentication, and various improvements and bug fixes.

  • Workflows:
    • Introduced per-project workflows with operations: create, edit, duplicate, delete, enable/disable, and edit TypeScript source.
    • Workflows execute on user sign-up; email-based workflows supported.
    • Added workflow token authentication and header support in smart-request.tsx.
  • Database:
    • Added tables WorkflowTriggerToken, WorkflowTrigger, WorkflowExecution, CurrentlyCompilingWorkflow, and CompiledWorkflow in migration.sql.
  • API:
    • Added triggerWorkflows function in workflows.tsx to handle workflow triggers.
    • Updated users/crud.tsx to trigger workflows on user sign-up.
  • UI:
    • Added workflow management pages in workflows/page.tsx and workflows/[workflowId]/page.tsx.
  • Misc:
    • Improved number formatting in numbers.tsx.
    • Added CORS allowance for workflow token header in middleware.tsx.
    • Payments page retitled to “Offers & Items.”
    • Provider settings clarify OAuth key changes in providers.tsx.
    • Fixed email API behavior for consistent error throwing.

This description was created by Ellipsis for 7adeee4. You can customize this summary. It will automatically update as commits are pushed.


Summary by CodeRabbit

  • New Features

    • Per-project Workflows UI: create/edit/duplicate/delete, enable/disable, edit TypeScript source, save with toasts.
    • Backend workflow orchestration: compile, persist, execute workflows; workflow tokens and authentication; sign-up triggers.
    • Workflow detail page for editing source.
  • Improvements

    • CORS allows workflow token header.
    • More robust number formatting and env fallback handling.
    • Payments page renamed to “Offers & Items”.
    • OAuth provider UI clarifies account transfer.
  • Bug Fixes

    • Email send behavior and tests updated for clearer error handling.
  • Tests

    • New end-to-end workflow email tests.

@Copilot Copilot AI review requested due to automatic review settings September 2, 2025 09:28
Copy link
Contributor

coderabbitai bot commented Sep 2, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Caution

Review failed

The pull request is closed.

Walkthrough

Adds multi-tenant workflow orchestration: DB models + migration, compile/execute runtime via Freestyle, workflow-token auth and CORS header, dashboard UI for managing workflows and editing TS source, config schema and defaults, user-signup trigger wiring, E2E tests, and several utility/interface adjustments.

Changes

Cohort / File(s) Summary
Workflow DB schema & migration
apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/20250903080405_workflows/migration.sql
Adds models/tables: WorkflowTriggerToken, WorkflowTrigger, WorkflowExecution, CurrentlyCompilingWorkflow, CompiledWorkflow with composite primary keys, unique indexes, foreign keys, and audit timestamps.
Backend workflow orchestration & runtime
apps/backend/src/lib/workflows.tsx, apps/backend/src/lib/freestyle.tsx, apps/backend/src/lib/email-rendering.tsx
New workflows module: bundling/compilation, runtime trigger registration, Freestyle-based execution returning Result wrappers, compiled-artifact persistence, trigger/execution recording; Freestyle mock-key safety and normalized error handling; email-rendering adapted to Freestyle output shape.
Auth, headers & known-errors
apps/backend/src/route-handlers/smart-request.tsx, apps/backend/src/middleware.tsx, packages/stack-shared/src/known-errors.tsx
Adds x-stack-workflow-token handling with tenancy-scoped hashed lookup and explicit WorkflowTokenDoesNotExist/WorkflowTokenExpired KnownErrors; adds header to CORS allowlist.
User lifecycle integration
apps/backend/src/app/api/latest/users/crud.tsx
Triggers "sign-up" workflows on user creation and on anonymous→non-anonymous upgrade.
Dashboard workflows UI
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx, .../[workflowId]/page.tsx, .../workflows/workflow-list.tsx
Adds workflows management UI: create/edit/duplicate/delete, enable/disable, list/search, and detail editor that saves workflow TypeScript source to project config.
Config schema & defaults
packages/stack-shared/src/config/schema.ts
Adds branch-level workflows schema and organization defaults; switches certain record keys to user-specified ID schemas; tweaks config override error context.
Shared utilities & helpers
packages/stack-shared/src/utils/arrays.tsx, packages/stack-shared/src/utils/numbers.tsx, packages/stack-shared/src/utils/promises.tsx
Adds array type-guards; revises pretty-print magnitudes to multi-step 1_000 suffixing and edge-case handling; timeout now accepts a Promise or thunk.
Server interfaces & templates
packages/stack-shared/src/interface/server-interface.ts, packages/template/src/lib/stack-app/apps/interfaces/server-app.ts, .../implementations/server-app-impl.ts, .../implementations/admin-app-impl.ts, .../implementations/common.ts
sendEmail API changed to return/throw directly (removed Result wrapper), default extraRequestHeaders now derived from helper, expanded environment-variable fallbacks for base URLs and defaults.
Prisma retry & Vercel utils
apps/backend/src/prisma-client.tsx, apps/backend/src/utils/vercel.tsx
retryTransaction gains options.level to enable serializable isolation; runAsynchronouslyAndWaitUntil and a new allPromisesAndWaitUntilEach accept thunks/promises.
E2E & unit tests
apps/e2e/tests/backend/workflows.test.ts, apps/e2e/tests/js/email.test.ts, packages/stack-shared/src/utils/arrays.test.tsx, packages/stack-shared/src/utils/numbers.test.tsx
Adds E2E tests for sign-up workflows and mailbox assertions; updates email tests to assert promise resolves/rejects and KnownErrors; adds tests for new utils and number formatting behavior.
Misc UI / docker tweaks
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx, apps/dashboard/.../auth-methods/providers.tsx, docker/dependencies/docker.compose.yaml
Small UI text change, provider dialog footnote, and docker extra_hosts + HOST_ON_HOST env for freestyle-mock.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant API as Users CRUD (API)
  participant WF as triggerWorkflows()
  participant COMP as compileAndGetEnabledWorkflows()
  participant DB as Prisma (Workflows*)
  participant RT as Freestyle Runtime

  U->>API: Sign up / upgrade from anonymous
  API->>WF: triggerWorkflows(tenancy, {type:"sign-up", ...})
  WF->>COMP: load enabled workflows for tenancy
  COMP->>DB: read config, compiled artifacts
  alt not compiled or stale
    COMP->>RT: bundle & initialize compiled code
    RT-->>COMP: Result {compiledCode, registeredTriggers} / {compileError}
    COMP->>DB: persist CompiledWorkflow / compile error
  end
  WF->>DB: create WorkflowExecution
  par For each workflow registering "sign-up"
    WF->>RT: execute trigger with env + data
    RT-->>WF: Result {output} / {error}
    WF->>DB: persist WorkflowTrigger (output or error)
  end
Loading
sequenceDiagram
  autonumber
  actor C as Client
  participant SR as Smart Request
  participant DB as Prisma (WorkflowTriggerToken)
  note over C,SR: Request with header x-stack-workflow-token

  C->>SR: Request + x-stack-workflow-token
  alt Admin request
    SR-->>C: 400 KnownErrors.AdminAuthenticationRequired
  else Client/Server request
    SR->>SR: hashWorkflowTriggerToken(token)
    SR->>DB: find token by tenancyId + tokenHash
    alt Token not found
      SR-->>C: 400 KnownErrors.WorkflowTokenDoesNotExist
    else Token expired
      SR-->>C: 400 KnownErrors.WorkflowTokenExpired
    else Valid
      SR-->>C: Proceed (authenticated via workflow token)
    end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Workflows #873 — Direct overlap: implements the same workflow feature set (Prisma models, workflows runtime, smart-request token auth, dashboard files).
  • Config override CRUD #803 — Related: changes touching admin/config surfaces and email rendering that overlap with workflow config/storage and interfaces.
  • Several project config improvements #811 — Related: touches config schema and defaults that overlap with the workflows schema additions.

Poem

a nibble of code, a hop through the night,
triggers awaken under soft monitor light,
I bundle the scripts and stitch each call,
tokens tucked warm, compiled bits stand tall.
emails hop out, delivered with a carrot crunch—hoppy logs. 🐇✨


📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 437bafa and 7adeee4.

📒 Files selected for processing (2)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/data-vault/page.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx (1 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch workflows

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

vercel bot commented Sep 2, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
stack-backend Ready Ready Preview Comment Sep 3, 2025 8:29am
stack-dashboard Ready Ready Preview Comment Sep 3, 2025 8:29am
stack-demo Ready Ready Preview Comment Sep 3, 2025 8:29am
stack-docs Ready Ready Preview Comment Sep 3, 2025 8:29am

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Greptile Summary

This PR introduces a comprehensive workflows feature to Stack Auth that allows users to create, manage, and execute custom TypeScript workflows. The implementation spans multiple layers of the application:

Core Infrastructure Changes:

  • New Prisma schema models (WorkflowTrigger, WorkflowExecution, CurrentlyCompilingWorkflow, CompiledWorkflow) establish a complete workflow lifecycle system with compilation versioning, execution tracking, and trigger management
  • Enhanced environment variable support in the Stack SDK template to work beyond Next.js applications by adding fallback environment variables without the NEXT_PUBLIC_ prefix
  • Updated configuration schema to support workflow definitions with TypeScript source code, display names, and enabled/disabled states

Workflow Engine:

  • New workflow compilation and execution system using esbuild for TypeScript compilation and Freestyle sandboxes for secure code execution
  • Trigger registration system for events like user sign-ups with proper database persistence
  • Result-based error handling pattern adoption across the Freestyle integration

Dashboard UI:

  • Complete CRUD interface for workflow management including list view, individual workflow editing pages, search functionality, and workflow state management (enabled/disabled)
  • Code editor interface with syntax highlighting for editing TypeScript workflow source code
  • Integration with existing project configuration system for persistence

Supporting Changes:

  • New type guard utilities for runtime array validation
  • Enhanced timeout function to support lazy promise evaluation
  • Updated email rendering to use the new Freestyle API structure
  • Cosmetic improvement changing "Payments" to "Offers & Items" in the dashboard for better clarity

The workflows feature enables users to define custom automation and business logic in TypeScript that can be triggered by authentication events, extending Stack Auth's capabilities beyond basic authentication to include custom workflows and integrations.

Confidence score: 1/5

  • This PR has critical implementation issues that will cause immediate problems in production, including broken number formatting and incomplete core functionality
  • Score severely lowered due to completely broken prettyPrintWithMagnitudes function, unimplemented core getCompiledWorkflows function, and hardcoded placeholder credentials in workflow execution
  • Pay immediate attention to packages/stack-shared/src/utils/numbers.tsx and apps/backend/src/lib/workflows.tsx which contain blocking issues that must be fixed before merge

13 files reviewed, 6 comments

Edit Code Review Bot Settings | Greptile


const handleSubmit = async () => {
if (!displayName) {
alert("Please fill in all required fields");
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Using alert() instead of consistent toast notifications

Suggested change
alert("Please fill in all required fields");
toast({ title: "Please fill in all required fields", variant: "destructive" });

Comment on lines 1 to 7
const magnitudes = [
[1_000_000_000_000_000, "trln"],
[1_000_000_000_000, "bln"],
[1_000_000_000, "bn"],
[1_000_000, "M"],
[1_000, "k"],
[1_000, "M"],
[1_000, "bn"],
[1_000, "bln"],
[1_000, "trln"],
] as const;
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: Critical error: All magnitude values are set to 1,000 instead of their correct values (1M, 1B, 1T, etc.). This will break the conversion logic completely.

Suggested change
const magnitudes = [
[1_000_000_000_000_000, "trln"],
[1_000_000_000_000, "bln"],
[1_000_000_000, "bn"],
[1_000_000, "M"],
[1_000, "k"],
[1_000, "M"],
[1_000, "bn"],
[1_000, "bln"],
[1_000, "trln"],
] as const;
const magnitudes = [
[1_000, "k"],
[1_000_000, "M"],
[1_000_000_000, "bn"],
[1_000_000_000_000, "bln"],
[1_000_000_000_000_000, "trln"],
] as const;

Comment on lines +21 to +22
const availableWorkflows = config.workflows.availableWorkflows;
const workflow = workflowId in availableWorkflows ? availableWorkflows[workflowId] : undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider using Map<string, WorkflowType> instead of plain object access to avoid potential prototype pollution vulnerabilities when using dynamic keys

Context Used: Rule - Use Map<A, B> instead of plain objects when using dynamic keys to avoid prototype pollution vulnerabilities. (link)

Copy link

recurseml bot commented Sep 2, 2025

Review by RecurseML

🔍 Review performed on 1e06ff4..694425c

✨ No bugs found, your code is sparkling clean

✅ Files analyzed, no issues (5)

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
apps/backend/src/lib/workflows.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx
packages/stack-shared/src/utils/arrays.tsx

⏭️ Files skipped (low suspicion) (8)

apps/backend/prisma/schema.prisma
apps/backend/src/lib/email-rendering.tsx
apps/backend/src/lib/freestyle.tsx
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
packages/stack-shared/src/config/schema.ts
packages/stack-shared/src/utils/numbers.tsx
packages/stack-shared/src/utils/promises.tsx
packages/template/src/lib/stack-app/apps/implementations/common.ts

Need help? Join our Discord

Copy link

patched-codes bot commented Sep 2, 2025

Documentation Changes Required

1. docs/templates/getting-started/setup.mdx

  1. Update the environment variable section:
    • Indicate that both NEXT_PUBLIC_STACK_PROJECT_ID and STACK_PROJECT_ID are supported
    • Indicate that both NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY and STACK_PUBLISHABLE_CLIENT_KEY are supported
    • Mention that the non-prefixed versions can be used as alternatives

2. docs/templates/snippets/stack-app-constructor-options-before-ssk.mdx

  1. Update the projectId parameter description:

    • Indicate that it defaults to either NEXT_PUBLIC_STACK_PROJECT_ID or STACK_PROJECT_ID environment variable
  2. Update the publishableClientKey parameter description:

    • Indicate that it defaults to either NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY or STACK_PUBLISHABLE_CLIENT_KEY environment variable

Please ensure these changes are reflected in the relevant documentation files to maintain consistency with the recent updates.

Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR introduces a workflow automation system to Stack Auth, adding the ability to create, manage, and execute TypeScript-based workflows within projects. The implementation includes workflow compilation, execution infrastructure, database schema updates, and comprehensive UI components for workflow management.

  • Adds workflow automation capabilities with TypeScript source code compilation and execution
  • Implements workflow configuration schema, database models, and UI management components
  • Updates environment variable handling to support fallback patterns for better configuration flexibility

Reviewed Changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/template/src/lib/stack-app/apps/implementations/common.ts Adds fallback environment variables for configuration values
packages/stack-shared/src/utils/promises.tsx Enhances timeout function to accept functions that return promises
packages/stack-shared/src/utils/numbers.tsx Fixes magnitude calculation algorithm and adds comprehensive test coverage
packages/stack-shared/src/utils/arrays.tsx Adds type guard functions for array validation
packages/stack-shared/src/config/schema.ts Adds workflow configuration schema and updates error handling
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/*.tsx Implements workflow management UI components
apps/backend/src/lib/workflows.tsx Core workflow compilation and execution logic
apps/backend/src/lib/freestyle.tsx Updates freestyle sandbox integration for workflow execution
apps/backend/src/lib/email-rendering.tsx Updates email rendering to use improved freestyle API
apps/backend/prisma/schema.prisma Adds database models for workflow storage and execution tracking
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx Minor UI title update

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +3 to +6
[1_000, "M"],
[1_000, "bn"],
[1_000, "bln"],
[1_000, "trln"],
Copy link
Preview

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

All magnitude values are incorrectly set to 1_000. The values should be [1_000, "k"], [1_000_000, "M"], [1_000_000_000, "bn"], [1_000_000_000_000, "bln"], [1_000_000_000_000_000, "trln"] to represent the correct numerical thresholds for each suffix.

Suggested change
[1_000, "M"],
[1_000, "bn"],
[1_000, "bln"],
[1_000, "trln"],
[1_000_000, "M"],
[1_000_000_000, "bn"],
[1_000_000_000_000, "bln"],
[1_000_000_000_000_000, "trln"],

Copilot uses AI. Check for mistakes.


const handleSubmit = async () => {
if (!displayName) {
alert("Please fill in all required fields");
Copy link
Preview

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

Replace alert() with a toast notification for better user experience. The rest of the codebase consistently uses toast() for user feedback.

Suggested change
alert("Please fill in all required fields");
toast("Please fill in all required fields");

Copilot uses AI. Check for mistakes.

@@ -855,7 +876,7 @@ export async function getConfigOverrideErrors<T extends yup.AnySchema>(schema: T
}
export async function assertNoConfigOverrideErrors<T extends yup.AnySchema>(schema: T, config: unknown, options: { allowPropertiesThatCanNoLongerBeOverridden?: boolean, extraInfo?: any } = {}): Promise<void> {
const res = await getConfigOverrideErrors(schema, config, options);
if (res.status === "error") throw new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${res.error}`, { options, config, schema });
if (res.status === "error") throw new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${res.error}`, { options, config });
Copy link
Preview

Copilot AI Sep 2, 2025

Choose a reason for hiding this comment

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

[nitpick] The schema parameter was removed from the error context, but it could be valuable for debugging configuration validation issues. Consider keeping it or documenting why it was removed.

Suggested change
if (res.status === "error") throw new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${res.error}`, { options, config });
if (res.status === "error") throw new StackAssertionError(`Config override is invalid — at a place where it should have already been validated! ${res.error}`, { options, config, schema });

Copilot uses AI. Check for mistakes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (23)
packages/stack-shared/src/config/schema.ts (1)

143-152: Workflows schema: guard against empty strings

Consider ensuring displayName and tsSource aren’t empty to avoid silent misconfigs.

Apply:

-      displayName: yupString(),
-      tsSource: yupString(),
+      displayName: yupString().trim().min(1),
+      tsSource: yupString().trim().min(1),

Will users commonly toggle only “enabled” via overrides without setting displayName/tsSource? If yes, confirm that warning handling remains acceptable when these fields are missing at override time.

packages/stack-shared/src/utils/arrays.tsx (2)

205-216: Deduplicate with a generic isArrayOf helper; add a “plain object” variant to avoid ambiguity.

  • The three primitives share identical structure; factor via a reusable helper.
  • isObjectArray currently treats arrays as “object” (true) and excludes functions; the TS type object includes functions, so the runtime/type semantics can surprise callers. Consider leaving isObjectArray as-is but also exporting isPlainObjectArray that excludes arrays and nulls and narrows to Record<string, unknown>[].

Apply this diff:

+const isArrayOf = <T>(pred: (v: unknown) => v is T) =>
+  (arr: unknown): arr is T[] =>
+    Array.isArray(arr) && arr.every(pred);
+
-export function isStringArray(arr: unknown): arr is string[] {
-  return Array.isArray(arr) && arr.every((item) => typeof item === "string");
-}
+export const isStringArray = isArrayOf((item): item is string => typeof item === "string");
 
-export function isNumberArray(arr: unknown): arr is number[] {
-  return Array.isArray(arr) && arr.every((item) => typeof item === "number");
-}
+export const isNumberArray = isArrayOf((item): item is number => typeof item === "number");
 
-export function isBooleanArray(arr: unknown): arr is boolean[] {
-  return Array.isArray(arr) && arr.every((item) => typeof item === "boolean");
-}
+export const isBooleanArray = isArrayOf((item): item is boolean => typeof item === "boolean");
 
 export function isObjectArray(arr: unknown): arr is object[] {
   return Array.isArray(arr) && arr.every((item) => typeof item === "object" && item !== null);
 }
+
+export function isPlainObjectArray(arr: unknown): arr is Record<string, unknown>[] {
+  return Array.isArray(arr)
+    && arr.every((item) => typeof item === "object" && item !== null && !Array.isArray(item));
+}

217-228: Broaden tests to lock in semantics (functions, array-of-arrays, and plain objects).

Add cases to document behavior explicitly and cover the new isPlainObjectArray.

Apply this diff:

 import.meta.vitest?.test("is<Type>Array", ({ expect }) => {
   expect(isStringArray([])).toBe(true);
   expect(isNumberArray([1, 2, 3])).toBe(true);
   expect(isBooleanArray([true, false, true])).toBe(true);
   expect(isObjectArray([{ a: 1 }, { b: 2 }, { c: 3 }])).toBe(true);
+  expect(isPlainObjectArray([{ a: 1 }, { b: 2 }])).toBe(true);
+  expect(isObjectArray([[1], [2]])).toBe(true);           // arrays are objects
+  expect(isPlainObjectArray([[1], [2]])).toBe(false);     // but not “plain objects”
+  expect(isObjectArray([() => {}])).toBe(false);          // functions are not typeof "object"
   expect(isStringArray([1, 2, 3])).toBe(false);
   expect(isNumberArray(["a", "b", "c"])).toBe(false);
   expect(isBooleanArray([1, 2, 3])).toBe(false);
   expect(isObjectArray([1, 2, 3])).toBe(false);
   expect(isObjectArray([{ a: 1 }, null, { b: 2 }])).toBe(false);
   expect(isObjectArray([{ a: 1 }, undefined, { b: 2 }])).toBe(false);
 });
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx (3)

4-4: Use inline Alert for blocking errors and disable Save when unchanged.

Per dashboard UI guideline, prefer alerts (not toasts) for blocking errors. Also prevent no-op saves.

- import { Button, Card, CardContent, CardHeader, CardTitle, toast } from "@stackframe/stack-ui";
+ import { Button, Card, CardContent, CardHeader, CardTitle, toast, Alert, AlertDescription } from "@stackframe/stack-ui";
- const [workflowContent, setWorkflowContent] = useState("");
+ const [workflowContent, setWorkflowContent] = useState("");
+ const [errorMsg, setErrorMsg] = useState<string | null>(null);
+ const hasChanges = workflowContent !== (workflow?.tsSource ?? "");
   const handleSave = async () => {
     setIsLoading(true);
     try {
+      setErrorMsg(null);
       await project.updateConfig({
         [`workflows.availableWorkflows.${workflowId}.tsSource`]: workflowContent
       });
       toast({ title: "Workflow saved successfully" });
     } catch (error) {
-      toast({ title: "Failed to save workflow", variant: "destructive" });
+      setErrorMsg("Failed to save workflow. Please try again.");
     } finally {
       setIsLoading(false);
     }
   };
-          <Button onClick={handleSave} size="sm" disabled={isLoading}>
+          <Button onClick={handleSave} size="sm" disabled={isLoading || !hasChanges}>
             <Save className="h-4 w-4 mr-2" />
             {isLoading ? "Saving..." : "Save"}
           </Button>
           <CardContent>
             <div className="relative">
+              {errorMsg && (
+                <Alert variant="destructive" className="mb-3">
+                  <AlertDescription>{errorMsg}</AlertDescription>
+                </Alert>
+              )}
               <textarea
                 value={workflowContent}
                 onChange={(e) => setWorkflowContent(e.target.value)}
                 className="w-full min-h-[600px] p-4 font-mono text-sm bg-muted/50 border rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-primary"
                 spellCheck={false}
                 placeholder="Enter your workflow code here..."
                 disabled={isLoading}
               />
             </div>
           </CardContent>

Also applies to: 23-25, 32-44, 73-76, 88-99


54-55: Render the workflow id as code; avoid JSON.stringify in UI text.

-          <p className="text-muted-foreground mb-4">The workflow {JSON.stringify(workflowId)} was not found.</p>
+          <p className="text-muted-foreground mb-4">The workflow <code>{workflowId}</code> was not found.</p>

80-80: Avoid always-on scrollbars; use overflow-auto and Tailwind basis.

-      <div className="flex gap-6 flex-1" style={{ flexBasis: "0px", overflow: "scroll" }}>
+      <div className="flex gap-6 flex-1 basis-0 overflow-auto">
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (2)

9-14: Export Workflow type for reuse across pages (remove anys elsewhere).

-type Workflow = {
+export type Workflow = {
   id: string,
   displayName: string,
   tsSource: string,
   enabled: boolean,
 };

127-134: Consider limiting content search for performance on large tsSource.

For better UX on big sources: search id/displayName by default and add a “Search code” toggle, or debounce and skip tsSource when query < 3 chars.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx (4)

113-114: Remove unsupported width prop on Input; use className or style.

-              onChange={(e) => setDisplayName(e.target.value)}
-              width="200px"
+              onChange={(e) => setDisplayName(e.target.value)}
+              className="w-[200px]"

11-11: Type workflows explicitly and fix blocking error toast in Edit form.
Reuse Workflow type; use inline Alert for validation instead of toast.

-import { WorkflowList } from "./workflow-list";
+import { WorkflowList, type Workflow } from "./workflow-list";
-  const [showEditDialog, setShowEditDialog] = useState(false);
-  const [editingWorkflow, setEditingWorkflow] = useState<any>(null);
+  const [showEditDialog, setShowEditDialog] = useState(false);
+  const [editingWorkflow, setEditingWorkflow] = useState<Workflow | null>(null);
-  const handleEditWorkflow = (workflow: any) => {
+  const handleEditWorkflow = (workflow: Workflow) => {
-  const handleDuplicateWorkflow = async (workflow: any) => {
+  const handleDuplicateWorkflow = async (workflow: Workflow) => {
-  const handleToggleEnabled = async (workflow: any) => {
+  const handleToggleEnabled = async (workflow: Workflow) => {
-  workflow: any,
+  workflow: Workflow,
-  const [displayName, setDisplayName] = useState(workflow.displayName);
-  const [tsSource, setTsSource] = useState(workflow.tsSource);
-  const [isSubmitting, setIsSubmitting] = useState(false);
+  const [displayName, setDisplayName] = useState<string>(workflow.displayName);
+  const [tsSource, setTsSource] = useState<string>(workflow.tsSource);
+  const [isSubmitting, setIsSubmitting] = useState(false);
+  const [formError, setFormError] = useState<string | null>(null);
-    if (!displayName) {
-      toast({ title: "Please fill in all required fields", variant: "destructive" });
-      return;
-    }
+    if (!displayName) {
+      setFormError("Please fill in all required fields");
+      return;
+    }
+    setFormError(null);
   return (
     <>
       <div className="space-y-4">
+        {formError && (
+          <Alert variant="destructive">
+            <AlertDescription>{formError}</AlertDescription>
+          </Alert>
+        )}
         <div>
           <Label htmlFor="edit-workflow-id">Workflow ID</Label>
           <Input
             id="edit-workflow-id"
             value={workflow.id}
             disabled
             className="bg-muted"
           />
         </div>

Also applies to: 136-137, 158-161, 185-189, 207-214, 293-299, 301-306, 315-345


232-232: Avoid inline overflow:scroll; use Tailwind utilities.

-        <div className="flex gap-6 flex-1" style={{ flexBasis: "0px", overflow: "scroll" }}>
+        <div className="flex gap-6 flex-1 basis-0 overflow-auto">

186-195: Optional: make duplicate IDs robust against races.
If two duplicates happen quickly, the while-loop on a stale snapshot can collide. Consider using generateUuid() for the new id or delegating uniqueness to the backend.

packages/stack-shared/src/utils/numbers.tsx (3)

15-25: Avoid string math in the loop; keep it numeric until the final format.

Reduces conversions and clarifies intent. Also adds a graceful clamp when exceeding the last suffix.

-  let current = toFixedMax(num, 1);
-  let lastSuffix = "";
-  for (const [difference, suffix] of magnitudes) {
-    if (+current >= difference) {
-      current = toFixedMax(+current / difference, 1);
-      lastSuffix = suffix;
-    } else {
-      break;
-    }
-  }
-  return current + lastSuffix;
+  let value = num;
+  let lastSuffix = "";
+  for (const [, suffix] of magnitudes) {
+    if (value >= 1_000) {
+      value = value / 1_000;
+      lastSuffix = suffix;
+    } else {
+      break;
+    }
+  }
+  // Optional: clamp when we run out of suffixes (prevents "1000T"/"1000Q")
+  const maxSuffix = magnitudes[magnitudes.length - 1][1];
+  if (lastSuffix === maxSuffix && value >= 1_000) {
+    return toFixedMax(value, 1) + lastSuffix + "+";
+  }
+  return toFixedMax(value, 1) + lastSuffix;

1-7: Nit: This file has no JSX; consider .ts instead of .tsx.

Minor build/IDE signal improvement.


38-43: Adjust magnitude suffixes and test expectations for short-scale abbreviations
Mapping entries ("bn", "bln", "trln") and the corresponding tests in packages/stack-shared/src/utils/numbers.tsx must be updated to use “B”, “T”, “Q” when switching to short-scale symbols.

packages/stack-shared/src/utils/promises.tsx (1)

416-418: Update timeoutThrow to match the new timeout signature

The timeoutThrow function should also accept the new union type to maintain API consistency with timeout.

-export async function timeoutThrow<T>(promise: Promise<T>, ms: number): Promise<T> {
+export async function timeoutThrow<T>(promiseOrFunc: Promise<T> | (() => Promise<T>), ms: number): Promise<T> {
-  return Result.orThrow(await timeout(promise, ms));
+  return Result.orThrow(await timeout(promiseOrFunc, ms));
}
apps/backend/prisma/schema.prisma (2)

776-787: Consider stronger typing and indexing for DataVaultEntry.

  • If hashedKey has a fixed/hash length, constrain it (e.g., @db.VarChar(64)).
  • encrypted appears to carry binary data encoded as JSON strings. Prefer a structured JSON with explicit fields and length checks, or split into Bytes fields to avoid double-encoding overhead.
  • If lookups happen by hashedKey alone, add an index on [tenancyId, hashedKey] to avoid relying solely on the composite unique.

Example diff:

 model DataVaultEntry {
-  hashedKey String
-  encrypted Json // Contains { edkBase64, ciphertextBase64 } from encryptWithKms()
+  hashedKey String @db.VarChar(64)
+  // Contains { edkBase64, ciphertextBase64 } from encryptWithKms()
+  encrypted Json
   createdAt DateTime @default(now())
   updatedAt DateTime @updatedAt

   @@id([tenancyId, id])
   @@unique([tenancyId, storeId, hashedKey])
   @@index([tenancyId, storeId])
+  @@index([tenancyId, hashedKey])
 }

824-835: Double-check concurrency semantics for CurrentlyCompilingWorkflow.

Primary key [tenancyId, workflowId, sourceHash] permits multiple “compiling” rows per workflow across source revisions. If only one compile may run per workflow/source, this is fine; otherwise, consider a uniqueness guard on (tenancyId, workflowId) for active compiles (enforced via status column or separate “active” table). Also consider a TTL/cleanup job for stale rows.

apps/backend/src/lib/workflows.tsx (5)

61-74: Guard against unknown triggers at runtime.

If registeredTriggers.get(triggerData.type) is undefined, calling it will throw a vague TypeError. Fail fast with a clear error.

Apply:

-        const trigger = registeredTriggers.get(triggerData.type);
-        return {
-          triggerOutput: trigger(triggerData.data),
-        };
+        const trigger = registeredTriggers.get(triggerData.type);
+        if (!trigger) {
+          throw new Error(\`Unknown trigger: \${triggerData.type}\`);
+        }
+        return { triggerOutput: trigger(triggerData.data) };

85-123: Timeout handling is good; also consider honoring workflow.enabled.

You check existence but not enabled. If disabled workflows should not compile, gate here.

Example:

-  const workflow = tenancy.config.workflows.availableWorkflows[workflowId];
+  const workflow = tenancy.config.workflows.availableWorkflows[workflowId];
+  if (workflow.enabled === false) {
+    return Result.error({ compileError: `Workflow ${workflowId} is disabled` });
+  }

178-184: Unimplemented getCompiledWorkflows blocks triggerWorkflows.

This currently throws. Add a minimal DB-backed implementation with a serializable transaction to avoid races: compute source hashes, upsert “compiling” rows, perform compile with timeout, write results to CompiledWorkflow, then return a Map of latest compiled artifacts.

I can draft an implementation sketch with Prisma transaction boundaries if you’d like.


191-197: Replace placeholder secrets with tenancy-aware values before shipping.

Hardcoded placeholders for STACK_PUBLISHABLE_CLIENT_KEY and STACK_SECRET_SERVER_KEY will break runtime behavior.

Would you like me to thread these from tenancy.config and add validation that they’re present for non-test environments?


231-237: Surface per-workflow results and handle partial failures.

Promise.all will reject on the first failure and mask others. Consider Promise.allSettled and returning a map of successes/failures for observability.

Apply:

 export async function triggerWorkflows(tenancy: Tenancy, trigger: WorkflowTrigger) {
   const compiledWorkflows = await getCompiledWorkflows(tenancy);
-  const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => {
-    await triggerWorkflow(tenancy, compiledWorkflow, trigger);
-  });
-  await Promise.all(promises);
+  const promises = [...compiledWorkflows].map(([_, cw]) => triggerWorkflow(tenancy, cw, trigger));
+  await Promise.allSettled(promises);
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1e06ff4 and 694425c.

📒 Files selected for processing (13)
  • apps/backend/prisma/schema.prisma (2 hunks)
  • apps/backend/src/lib/email-rendering.tsx (1 hunks)
  • apps/backend/src/lib/freestyle.tsx (2 hunks)
  • apps/backend/src/lib/workflows.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (1 hunks)
  • packages/stack-shared/src/config/schema.ts (5 hunks)
  • packages/stack-shared/src/utils/arrays.tsx (1 hunks)
  • packages/stack-shared/src/utils/numbers.tsx (2 hunks)
  • packages/stack-shared/src/utils/promises.tsx (1 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/common.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
{apps/dashboard,apps/dev-launchpad,packages/stack-ui,packages/react}/**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

For blocking alerts and errors in UI, do not use toast notifications; use alerts instead

Files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx
  • packages/stack-shared/src/utils/arrays.tsx
  • packages/stack-shared/src/utils/promises.tsx
  • packages/template/src/lib/stack-app/apps/implementations/common.ts
  • apps/backend/src/lib/workflows.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx
  • apps/backend/src/lib/email-rendering.tsx
  • apps/backend/src/lib/freestyle.tsx
  • packages/stack-shared/src/config/schema.ts
  • packages/stack-shared/src/utils/numbers.tsx
{apps/dashboard,apps/dev-launchpad,packages/stack-ui,packages/react}/**/*.{tsx,jsx,css}

📄 CodeRabbit inference engine (AGENTS.md)

Keep hover/click animations snappy; avoid pre-transition delays on hover and apply transitions after the action (e.g., fade-out on hover end)

Files:

  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx
packages/template/**

📄 CodeRabbit inference engine (AGENTS.md)

When modifying the SDK copies, make changes in packages/template (source of truth)

Files:

  • packages/template/src/lib/stack-app/apps/implementations/common.ts
🧬 Code graph analysis (10)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/page-layout.tsx (1)
  • PageLayout (3-42)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/page.tsx (5)
apps/dashboard/src/components/illustrated-info.tsx (1)
  • IllustratedInfo (4-31)
packages/stack-shared/src/utils/uuids.tsx (1)
  • generateUuid (3-8)
packages/stack-shared/src/utils/strings.tsx (1)
  • deindent (235-238)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
  • useAdminApp (27-34)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (1)
  • WorkflowList (108-160)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/[workflowId]/page.tsx (2)
apps/dashboard/src/components/router.tsx (1)
  • useRouter (15-33)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/use-admin-app.tsx (1)
  • useAdminApp (27-34)
packages/stack-shared/src/utils/promises.tsx (1)
packages/stack-shared/src/utils/results.tsx (2)
  • Result (4-12)
  • Result (26-56)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
packages/stack-shared/src/utils/errors.tsx (1)
  • throwErr (10-19)
apps/backend/src/lib/workflows.tsx (10)
packages/stack-shared/src/utils/bytes.tsx (1)
  • encodeBase64 (114-116)
packages/stack-shared/src/utils/crypto.tsx (1)
  • hash (111-113)
packages/stack-shared/src/utils/esbuild.tsx (1)
  • bundleJavaScript (33-128)
apps/backend/src/lib/tenancies.tsx (1)
  • Tenancy (47-47)
packages/stack-shared/src/utils/errors.tsx (3)
  • StackAssertionError (69-85)
  • captureError (126-134)
  • errorToNiceString (89-92)
packages/stack-shared/src/utils/promises.tsx (1)
  • timeout (390-396)
packages/stack-shared/src/utils/arrays.tsx (1)
  • isStringArray (205-207)
packages/stack-shared/src/utils/uuids.tsx (1)
  • generateUuid (3-8)
apps/backend/src/lib/freestyle.tsx (1)
  • Freestyle (7-45)
apps/backend/src/prisma-client.tsx (1)
  • getPrismaClientForTenancy (51-53)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (2)
apps/dashboard/src/components/router.tsx (1)
  • useRouter (15-33)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/list-section.tsx (1)
  • ListSection (19-90)
apps/backend/src/lib/email-rendering.tsx (1)
apps/backend/src/lib/freestyle.tsx (1)
  • Freestyle (7-45)
apps/backend/src/lib/freestyle.tsx (2)
packages/stack-shared/src/utils/env.tsx (2)
  • getEnvVariable (16-58)
  • getNodeEnvironment (65-67)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
packages/stack-shared/src/config/schema.ts (2)
packages/stack-shared/src/schema-fields.ts (5)
  • userSpecifiedIdSchema (426-426)
  • yupObject (247-251)
  • yupRecord (283-322)
  • yupString (187-190)
  • yupBoolean (195-198)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: restart-dev-and-test
  • GitHub Check: setup-tests
  • GitHub Check: docker
  • GitHub Check: all-good
  • GitHub Check: Security Check
🔇 Additional comments (21)
packages/stack-shared/src/config/schema.ts (5)

189-190: Wiring workflows into branchConfigSchema — LGTM

Schema composition looks consistent with other branch-level sections.


508-514: Organization defaults for workflows — LGTM

Defaults align with existing pattern (emails/themes/templates) and should render sensible fallback UI.


878-880: Dropping schema from error.extraData is safe
Search for .extraData.schema across the repo returned no consumer references—telemetry/debug code does not inspect it.


45-47: Drop redundant .defined() on connectionStringId schema
The .defined() call is unnecessary and inconsistent; please verify no existing configs use invalid connectionString IDs (e.g., starting with a digit or containing disallowed characters).

-        userSpecifiedIdSchema("connectionStringId").defined(),
+        userSpecifiedIdSchema("connectionStringId"),

181-187: Verify compatibility of existing data vault store IDs
The switch to userSpecifiedIdSchema("storeId") enforces stricter ID rules (no leading digits or illegal characters). Automated scanning didn’t flag any store keys in code—please manually confirm your database/config entries comply or add a migration.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/workflows/workflow-list.tsx (1)

43-45: Nice snappy hover behavior.
Switching to duration-0 on hover matches the “snappy interactions” guideline.

packages/stack-shared/src/utils/numbers.tsx (1)

32-35: LGTM: Boundary rounding around 1M is correct.

The transitions at 999_499, 999_500, 999_949, 999_950 behave as expected with 1-decimal rounding.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/page-client.tsx (1)

776-776: Title change looks good

The title change from "Payments" to "Offers & Items" better reflects the page content and terminology.

packages/template/src/lib/stack-app/apps/implementations/common.ts (3)

68-68: LGTM! Good addition of environment variable fallbacks

The addition of fallback to STACK_PROJECT_ID when NEXT_PUBLIC_STACK_PROJECT_ID is not set improves flexibility for server-side configurations.


72-72: LGTM! Consistent fallback pattern

The addition of fallback to STACK_PUBLISHABLE_CLIENT_KEY maintains consistency with the project ID fallback pattern.


84-84: LGTM! Complete fallback implementation

The addition of fallback to STACK_EXTRA_REQUEST_HEADERS completes the consistent fallback pattern across all three environment variable getters.

packages/stack-shared/src/utils/promises.tsx (1)

390-391: Good enhancement for lazy promise creation

The updated signature accepting Promise<T> | (() => Promise<T>) enables lazy promise creation, which is particularly useful for avoiding side effects until the timeout actually starts. This aligns well with the workflow orchestration needs where expensive operations should only be initiated when needed.

apps/backend/src/lib/email-rendering.tsx (2)

115-115: LGTM! Simplified Freestyle instantiation

The removal of explicit API key handling simplifies the code since Freestyle now handles the environment variable fallback internally.


122-125: Improved error handling with Result pattern

The updated error handling properly checks for output.status === "error" and returns the error directly, aligning with the Result pattern used throughout the codebase. The data access path output.data.result correctly reflects the new Result wrapper structure.

apps/backend/src/lib/freestyle.tsx (3)

10-11: Good API design with optional apiKey

Making the apiKey optional with a default empty object improves flexibility. The fallback to getEnvVariable("STACK_FREESTYLE_API_KEY") maintains backward compatibility while allowing explicit key override.


35-41: Robust error unwrapping with Result pattern

The error unwrapping logic thoroughly handles nested error structures that may come from the Freestyle service. The multi-level unwrapping (wrap1 through wrap4) with JSON parsing ensures that the actual error is extracted regardless of how it's wrapped. This is a good defensive programming practice.


13-17: Ensure test environments and CI don’t default to the mock key

  • apps/backend/.env.development sets STACK_FREESTYLE_API_KEY=mock_stack_freestyle_key (line 48) – add or update a .env.test (or test config) to use a real API key instead of falling back to the mock.
  • Verify CI workflows (e.g. under .github/workflows/) explicitly set STACK_FREESTYLE_API_KEY to a non-mock value.
apps/backend/prisma/schema.prisma (3)

27-27: No-op change acknowledged.

Whitespace-only; no behavioral impact.


837-857: Enforce XOR for compiledCode vs compileError and add “latest” retrieval indexes.

  • Add a CHECK constraint to ensure exactly one of compiledCode or compileError is set.
  • Add an index to fetch the latest compiled artifact per workflow quickly (by compilationVersion or compiledAt).
    [Suggest_essential_refactor]
    Suggested changes:
 model CompiledWorkflow {
   tenancyId          String @db.Uuid
   id                 String @default(uuid()) @db.Uuid
   workflowId         String @db.Uuid // note: The workflow with this ID may have been edited or deleted in the meantime, so there may be multiple CompiledWorkflows with the same workflowId
   compilationVersion Int
   sourceHash         String

   // exactly one of [compiledCode, compileError] must be set
   compiledCode String?
   compileError String?

   compiledAt         DateTime @default(now())
   registeredTriggers String[]

   createdAt         DateTime            @default(now())
   updatedAt         DateTime            @updatedAt
   WorkflowExecution WorkflowExecution[]

   @@id([tenancyId, id])
   @@unique([tenancyId, workflowId, compilationVersion, sourceHash])
+  @@index([tenancyId, workflowId, compilationVersion(sort: Desc)], name: "CompiledWorkflow_latest_by_version")
+  @@index([tenancyId, workflowId, compiledAt(sort: Desc)], name: "CompiledWorkflow_latest_by_time")
 }

And in SQL migration:

ALTER TABLE "CompiledWorkflow"
ADD CONSTRAINT "compiledworkflow_code_xor_error"
CHECK (
  (("compiledCode" IS NULL) <> ("compileError" IS NULL))
);

808-822: Add cascade delete, recency index, and document triggerIds invariant

  • compiledWorkflow relation: add onDelete: Cascade (verify your Prisma version supports referential actions or enable referentialActions in previewFeatures)
  • Add @@index([tenancyId, compiledWorkflowId, createdAt(sort: Desc)], name: "WorkflowExecution_byCompiled_recent")
  • Add a comment above triggerIds explaining its consistency invariant with the triggers relation
apps/backend/src/lib/workflows.tsx (1)

125-176: Tests confirm behavior; they also expose the missing "compile" union type.

Once the union is fixed, these expectations align. No further action here.

Comment on lines +3 to +6
[1_000, "M"],
[1_000, "bn"],
[1_000, "bln"],
[1_000, "trln"],
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

Fix incorrect/ambiguous suffix mapping beyond 1e9 (bn/bln/trln).

Current mapping labels 10^12 as "bln" and 10^15 as "trln", which conflicts with the short scale (US) and mixes styles ("bn" vs "bln"). This is user-facing and misleading.

Apply short-scale, widely-recognized symbols:

   [1_000, "k"],
-  [1_000, "M"],
-  [1_000, "bn"],
-  [1_000, "bln"],
-  [1_000, "trln"],
+  [1_000, "M"], // 10^6
+  [1_000, "B"], // 10^9 (billion)
+  [1_000, "T"], // 10^12 (trillion)
+  [1_000, "Q"], // 10^15 (quadrillion)

If you must keep word-like abbreviations, use a consistent short scale: ["k","M","bn","trln","qdln"].
To align expectations, update the tests accordingly (see below).

I can push the test updates once you confirm the preferred style.


Use consistent short-scale suffixes for numeric formatting
Replace ambiguous word-like suffixes beyond million with standard short-scale symbols and update tests accordingly:

-  [1_000_000, "M"],      // 10^6
-  [1_000_000_000, "bn"],   // 10^9
-  [1_000_000_000_000, "bln"], // 10^12
-  [1_000_000_000_000_000, "trln"], // 10^15
+  [1_000_000, "M"],      // 10^6
+  [1_000_000_000, "B"],    // 10^9
+  [1_000_000_000_000, "T"],  // 10^12
+  [1_000_000_000_000_000, "Q"], // 10^15
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[1_000, "M"],
[1_000, "bn"],
[1_000, "bln"],
[1_000, "trln"],
[1_000_000, "M"], // 10^6
[1_000_000_000, "B"], // 10^9
[1_000_000_000_000, "T"], // 10^12
[1_000_000_000_000_000, "Q"], // 10^15
🤖 Prompt for AI Agents
In packages/stack-shared/src/utils/numbers.tsx around lines 3 to 6, the numeric
suffixes use ambiguous word-like forms ("bn", "bln", "trln") instead of standard
short-scale symbols; update the mappings to use consistent short-scale suffixes
(e.g., "K" for thousand, "M" for million, "B" for billion, "T" for trillion,
etc.) and adjust any downstream logic that formats numbers accordingly, then
update unit tests to expect the new symbols.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
apps/backend/src/lib/workflows.tsx (3)

16-16: Include "compile" in the registered trigger type union.

compileWorkflowSource registers a "compile" trigger and tests assert it; the union excludes it, forcing unsafe casts.

Apply:

-type WorkflowRegisteredTriggerType = "sign-up";
+type WorkflowRegisteredTriggerType = "compile" | "sign-up";

318-332: Return execution metadata from triggerWorkflowRaw.

You generate triggerId and executionId but discard them. Persisting triggers/executions later needs these IDs to correlate callback chains; return them alongside the result.

Apply:

-async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> {
+async function triggerWorkflowRaw(
+  tenancy: Tenancy,
+  compiledWorkflowCode: string,
+  trigger: WorkflowTrigger
+): Promise<Result<{ result: unknown, triggerId: string, executionId: string }, string>> {
   const triggerId = generateUuid();
   const executionId = trigger.type === "callback" ? trigger.executionId : generateUuid();

   const freestyle = new Freestyle();
   const freestyleRes = await freestyle.executeScript(compiledWorkflowCode, {
     envVars: {
       STACK_WORKFLOW_TRIGGER_DATA: JSON.stringify(trigger),
       STACK_PROJECT_ID: tenancy.project.id,
       STACK_PUBLISHABLE_CLIENT_KEY: "insert actual publishable client key here",
       STACK_SECRET_SERVER_KEY: "insert actual secret server key here",
     },
   });
-  return Result.map(freestyleRes, (data) => data.result);
+  return Result.map(freestyleRes, (data) => ({
+    result: data.result,
+    triggerId,
+    executionId,
+  }));
 }

334-361: Fix incorrect database associations and use generated IDs for proper execution tracking.

The current implementation has critical issues:

  1. Uses compiledWorkflow.id (the compiled workflow ID) as the execution ID in the where clause, which is incorrect
  2. Doesn't preserve the triggerId and executionId generated in triggerWorkflowRaw
  3. Always tries to connectOrCreate with incorrect IDs instead of creating new executions

Apply this fix to properly track executions and triggers:

 async function triggerWorkflow(tenancy: Tenancy, compiledWorkflow: CompiledWorkflow, trigger: WorkflowTrigger): Promise<Result<void, string>> {
   if (compiledWorkflow.compiledCode === null) {
     return Result.error(`Workflow ${compiledWorkflow.id} failed to compile: ${compiledWorkflow.compileError}`);
   }
   const res = await triggerWorkflowRaw(tenancy, compiledWorkflow.compiledCode, trigger);
   const prisma = await getPrismaClientForTenancy(tenancy);
-  await prisma.workflowTrigger.create({
-    data: {
-      triggerData: trigger,
-      ...(res.status === "ok" ? { output: (res.data ?? Prisma.JsonNull) as any } : { error: res.error }),
-      execution: {
-        connectOrCreate: {
-          where: {
-            tenancyId_id: {
-              tenancyId: tenancy.id,
-              id: compiledWorkflow.id,
-            },
-          },
-          create: {
-            tenancyId: tenancy.id,
-            compiledWorkflowId: compiledWorkflow.id,
-          },
-        },
-      },
-    },
-  });
+  if (res.status === "ok") {
+    await prisma.workflowTrigger.create({
+      data: {
+        tenancyId: tenancy.id,
+        id: res.data.triggerId,
+        triggerData: trigger,
+        output: (res.data.result ?? Prisma.JsonNull) as any,
+        execution: {
+          create: {
+            tenancyId: tenancy.id,
+            id: res.data.executionId,
+            compiledWorkflowId: compiledWorkflow.id,
+          },
+        },
+      },
+    });
+  } else {
+    // Generate IDs for failed executions too, for tracking
+    await prisma.workflowTrigger.create({
+      data: {
+        tenancyId: tenancy.id,
+        id: generateUuid(),
+        triggerData: trigger,
+        error: res.error,
+        execution: {
+          create: {
+            tenancyId: tenancy.id,
+            id: generateUuid(),
+            compiledWorkflowId: compiledWorkflow.id,
+          },
+        },
+      },
+    });
+  }
   return Result.ok(undefined);
 }
🧹 Nitpick comments (1)
apps/backend/src/lib/workflows.tsx (1)

287-287: Remove console.log statement.

Debug logging should use proper logging infrastructure instead of console.log.

-        console.log(`Compiled workflow ${workflowId}`);
+        // Use proper logging infrastructure if needed
+        // logger.info(`Compiled workflow ${workflowId}`);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 694425c and 5894613.

📒 Files selected for processing (6)
  • apps/backend/prisma/migrations/20250902221648_workflows/migration.sql (1 hunks)
  • apps/backend/prisma/schema.prisma (2 hunks)
  • apps/backend/src/lib/workflows.tsx (1 hunks)
  • apps/backend/src/prisma-client.tsx (2 hunks)
  • apps/backend/src/utils/vercel.tsx (1 hunks)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/prisma/schema.prisma
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • apps/backend/src/utils/vercel.tsx
  • apps/backend/src/prisma-client.tsx
  • apps/backend/src/lib/workflows.tsx
🧬 Code graph analysis (1)
apps/backend/src/lib/workflows.tsx (11)
packages/stack-shared/src/utils/bytes.tsx (1)
  • encodeBase64 (114-116)
packages/stack-shared/src/utils/crypto.tsx (1)
  • hash (111-113)
packages/stack-shared/src/utils/esbuild.tsx (1)
  • bundleJavaScript (33-128)
apps/backend/src/lib/tenancies.tsx (1)
  • Tenancy (47-47)
packages/stack-shared/src/utils/errors.tsx (4)
  • StackAssertionError (69-85)
  • captureError (126-134)
  • errorToNiceString (89-92)
  • throwErr (10-19)
packages/stack-shared/src/utils/promises.tsx (2)
  • timeout (390-396)
  • wait (260-268)
packages/stack-shared/src/utils/arrays.tsx (1)
  • isStringArray (205-207)
apps/backend/src/prisma-client.tsx (3)
  • globalPrismaClient (31-31)
  • retryTransaction (124-193)
  • getPrismaClientForTenancy (51-53)
apps/backend/src/utils/vercel.tsx (1)
  • allPromisesAndWaitUntilEach (10-15)
packages/stack-shared/src/utils/uuids.tsx (1)
  • generateUuid (3-8)
apps/backend/src/lib/freestyle.tsx (1)
  • Freestyle (7-45)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: build (22.x)
  • GitHub Check: docker
  • GitHub Check: docker
  • GitHub Check: restart-dev-and-test
  • GitHub Check: setup-tests
  • GitHub Check: all-good
  • GitHub Check: Security Check
🔇 Additional comments (4)
apps/backend/src/utils/vercel.tsx (1)

10-15: LGTM!

The implementation of allPromisesAndWaitUntilEach correctly registers each promise with Vercel's background execution and then awaits all results via Promise.all. The function properly propagates errors and maintains the original promise results ordering.

apps/backend/prisma/migrations/20250902221648_workflows/migration.sql (2)

37-37: Fix typo in primary key constraint.

There's a typo in the primary key constraint name: "tenantryId" should be "tenancyId" to match the column name.

-    CONSTRAINT "CurrentlyCompilingWorkflow_pkey" PRIMARY KEY ("tenantryId","workflowId","compilationVersion","sourceHash")
+    CONSTRAINT "CurrentlyCompilingWorkflow_pkey" PRIMARY KEY ("tenancyId","workflowId","compilationVersion","sourceHash")

Likely an incorrect or invalid review comment.


58-58: Index name exceeds PostgreSQL's 63-character limit.

The index name "CompiledWorkflow_tenancyId_workflowId_compilationVersion_so_key" is 64 characters long, which exceeds PostgreSQL's maximum identifier length of 63 characters. This will cause the index name to be truncated, potentially leading to naming conflicts or confusion.

Consider shortening the index name to stay within the 63-character limit:

-CREATE UNIQUE INDEX "CompiledWorkflow_tenancyId_workflowId_compilationVersion_so_key" ON "CompiledWorkflow"("tenancyId", "workflowId", "compilationVersion", "sourceHash");
+CREATE UNIQUE INDEX "CompiledWorkflow_tenancy_workflow_version_hash_key" ON "CompiledWorkflow"("tenancyId", "workflowId", "compilationVersion", "sourceHash");

Likely an incorrect or invalid review comment.

apps/backend/src/prisma-client.tsx (1)

124-126: LGTM! Clean API extension for transaction isolation levels.

The addition of the optional options parameter with isolation level control is well-designed. The default behavior is preserved, and the API provides a clean way to request serializable isolation when needed for workflow compilation and other critical operations.

};

let retryInfo = [];
const prisma = globalPrismaClient; //await getPrismaClientForTenancy(tenancy);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Use tenancy-specific Prisma client instead of global client.

The code uses globalPrismaClient but has the correct getPrismaClientForTenancy(tenancy) commented out. This could cause issues in multi-tenant environments where workflows should be isolated per tenancy.

-  const prisma = globalPrismaClient; //await getPrismaClientForTenancy(tenancy);
+  const prisma = await getPrismaClientForTenancy(tenancy);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const prisma = globalPrismaClient; //await getPrismaClientForTenancy(tenancy);
// Replace global client with tenancy-scoped Prisma client
const prisma = await getPrismaClientForTenancy(tenancy);
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around line 252, the file currently
assigns prisma = globalPrismaClient but should use a tenancy-scoped client;
replace that line with an awaited call to getPrismaClientForTenancy(tenancy)
(e.g. const prisma = await getPrismaClientForTenancy(tenancy)), ensure the
surrounding function is async or handle the promise, remove reliance on
globalPrismaClient, and add minimal error handling if the tenancy client is not
returned.

Comment on lines 365 to 368
const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => {
await triggerWorkflow(tenancy, compiledWorkflow, trigger);
});
await Promise.all(promises);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for workflow trigger failures.

The function doesn't handle or log errors from individual workflow triggers, which could make debugging difficult when workflows fail.

 export async function triggerWorkflows(tenancy: Tenancy, trigger: WorkflowTrigger) {
   const compiledWorkflows = await compileAndGetEnabledWorkflows(tenancy);
   const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => {
-    await triggerWorkflow(tenancy, compiledWorkflow, trigger);
+    try {
+      const result = await triggerWorkflow(tenancy, compiledWorkflow, trigger);
+      if (result.status === "error") {
+        captureError("workflow-trigger-failed", new StackAssertionError(`Failed to trigger workflow ${workflowId}`, { 
+          workflowId, 
+          error: result.error,
+          trigger 
+        }));
+      }
+    } catch (e) {
+      captureError("workflow-trigger-exception", e);
+    }
   });
   await Promise.all(promises);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => {
await triggerWorkflow(tenancy, compiledWorkflow, trigger);
});
await Promise.all(promises);
export async function triggerWorkflows(tenancy: Tenancy, trigger: WorkflowTrigger) {
const compiledWorkflows = await compileAndGetEnabledWorkflows(tenancy);
const promises = [...compiledWorkflows].map(async ([workflowId, compiledWorkflow]) => {
try {
const result = await triggerWorkflow(tenancy, compiledWorkflow, trigger);
if (result.status === "error") {
captureError(
"workflow-trigger-failed",
new StackAssertionError(
`Failed to trigger workflow ${workflowId}`,
{
workflowId,
error: result.error,
trigger,
}
)
);
}
} catch (e) {
captureError("workflow-trigger-exception", e);
}
});
await Promise.all(promises);
}
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 365 to 368, the current
mapping of compiledWorkflows calls triggerWorkflow without handling per-workflow
failures; wrap the async operation for each workflow in a try/catch (or switch
to Promise.allSettled) so individual trigger errors are caught and logged with
the workflowId and error details, ensuring failures are recorded but other
workflows continue to run and Promise.all (or post-processing of allSettled
results) completes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5894613 and a56d3d3.

📒 Files selected for processing (1)
  • apps/backend/src/app/api/latest/users/crud.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
apps/backend/src/app/api/latest/**

📄 CodeRabbit inference engine (AGENTS.md)

apps/backend/src/app/api/latest/**: Organize backend API routes by resource under /api/latest (e.g., auth at /api/latest/auth/, users at /api/latest/users/, teams at /api/latest/teams/, oauth providers at /api/latest/oauth-providers/)
Use the custom route handler system in the backend to ensure consistent API responses

Files:

  • apps/backend/src/app/api/latest/users/crud.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • apps/backend/src/app/api/latest/users/crud.tsx
🧬 Code graph analysis (1)
apps/backend/src/app/api/latest/users/crud.tsx (1)
apps/backend/src/lib/workflows.tsx (1)
  • triggerWorkflows (363-369)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: all-good
  • GitHub Check: restart-dev-and-test
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: setup-tests
  • GitHub Check: Security Check
🔇 Additional comments (1)
apps/backend/src/app/api/latest/users/crud.tsx (1)

8-8: LGTM on import.

Import is correct and used below.

Comment on lines +652 to +658
// if the user is not an anonymous user, trigger onSignUp workflows
if (!result.is_anonymous) {
await triggerWorkflows(auth.tenancy, {
type: "sign-up",
userId: result.id,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Don’t block user creation on workflow execution; make it fire-and-forget.

If any workflow throws, the endpoint 500s even though the user is already persisted. This couples signup UX to arbitrary workflow code and increases latency. Mirror the webhook pattern and dispatch asynchronously.

Apply this diff:

-    if (!result.is_anonymous) {
-      await triggerWorkflows(auth.tenancy, {
-        type: "sign-up",
-        userId: result.id,
-      });
-    }
+    if (!result.is_anonymous) {
+      // Run in the background to avoid failing user creation on workflow errors
+      runAsynchronouslyAndWaitUntil(
+        triggerWorkflows(auth.tenancy, {
+          type: "sign-up",
+          userId: result.id,
+        })
+      );
+    }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/backend/src/app/api/latest/users/crud.tsx around lines 652 to 658, the
code awaits triggerWorkflows during signup which can cause the endpoint to 500
or increase latency when workflows fail; change this to fire-and-forget by
invoking triggerWorkflows without awaiting and attach a .catch that logs any
errors (or schedule via void Promise.resolve().then(() =>
triggerWorkflows(...)).catch(...)) so failures don’t propagate to the response
path and signup remains non-blocking.

Comment on lines +960 to +968
// if we went from anonymous to non-anonymous:
if (oldUser.isAnonymous && data.is_anonymous === false) {
// trigger onSignUp workflows
await triggerWorkflows(auth.tenancy, {
type: "sign-up",
userId: params.user_id,
});

// rename the personal team
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Critical: Triggering workflows inside retryTransaction causes duplicate side effects and long transactions.

retryTransaction(...) may re-run the callback on serialization conflicts. Calling await triggerWorkflows(...) inside it can double-trigger workflows and holds the DB txn open for external work. Move the trigger outside the transaction and flag within.

Apply this diff:

   const prisma = await getPrismaClientForTenancy(auth.tenancy);
-  const { user } = await retryTransaction(prisma, async (tx) => {
+  let shouldTriggerSignUp = false;
+  const { user } = await retryTransaction(prisma, async (tx) => {
@@
-      if (oldUser.isAnonymous && data.is_anonymous === false) {
-        // trigger onSignUp workflows
-        await triggerWorkflows(auth.tenancy, {
-          type: "sign-up",
-          userId: params.user_id,
-        });
-
-        // rename the personal team
+      if (oldUser.isAnonymous && data.is_anonymous === false) {
+        // mark for post-commit workflow trigger
+        shouldTriggerSignUp = true;
+        // rename the personal team
         await tx.team.updateMany({
           where: {
             tenancyId: auth.tenancy.id,
@@
       return {
         user,
       };
     });
 
+    if (shouldTriggerSignUp) {
+      // Dispatch asynchronously to avoid blocking the update path and duplicate side effects on retries
+      runAsynchronouslyAndWaitUntil(
+        triggerWorkflows(auth.tenancy, {
+          type: "sign-up",
+          userId: params.user_id,
+        })
+      );
+    }

Run to confirm the trigger type is valid and to surface other call sites:


🏁 Script executed:

#!/bin/bash
# Verify 'sign-up' is an allowed WorkflowTrigger type and find all usages
rg -nP --type=ts '\bWorkflowTrigger\b' -C2
rg -nP --type=ts "(\"|')sign-up(\"|')" -C2

Length of output: 8641


🏁 Script executed:

#!/bin/bash
# Show all retryTransaction usages in the users CRUD file with context
rg -nP --type=ts 'retryTransaction\(' -C4 apps/backend/src/app/api/latest/users/crud.tsx

Length of output: 1414


🏁 Script executed:

#!/bin/bash
# Locate definition of retryTransaction
rg -nP --type=ts "async function retryTransaction" -C3 .
rg -nP --type=ts "retryTransaction\\s*=" -C3 .

Length of output: 732


🏁 Script executed:

#!/bin/bash
sed -n '115,180p' apps/backend/src/prisma-client.tsx

Length of output: 3778


🏁 Script executed:

#!/bin/bash
# Extract create path around retryTransaction and subsequent triggerWorkflows
sed -n '630,700p' apps/backend/src/app/api/latest/users/crud.tsx

Length of output: 2300


Move triggerWorkflows call out of retryTransaction in onUpdate
The onUpdate handler invokes await triggerWorkflows(...) inside retryTransaction, which can double-trigger workflows on retries and blocks the transaction on external work. Mirror the onCreate pattern: set a flag inside the transaction, then after retryTransaction completes call

runAsynchronouslyAndWaitUntil(
  triggerWorkflows(auth.tenancy, { type: "sign-up", userId: params.user_id })
);
🤖 Prompt for AI Agents
In apps/backend/src/app/api/latest/users/crud.tsx around lines 960 to 968, the
code currently awaits triggerWorkflows(...) inside retryTransaction which can
cause double-triggering on retries and blocks the transaction; change this by
setting a local boolean flag inside the transaction when oldUser.isAnonymous &&
data.is_anonymous === false (do NOT call triggerWorkflows there), then after
retryTransaction completes check that flag and call
runAsynchronouslyAndWaitUntil(triggerWorkflows(auth.tenancy, { type: "sign-up",
userId: params.user_id })); remove the await triggerWorkflows from inside the
transaction so external work runs only once after the transaction succeeds.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/stack-shared/src/interface/server-interface.ts (1)

814-834: Align sendEmail to return void and remove unused res

Remove the unused res variable and let errors propagate by updating the implementation and signature:

-    const res = await this.sendServerRequest(
+    await this.sendServerRequest(
       "/emails/send-email",
       {
         method: "POST",
         headers: {
           "Content-Type": "application/json"
         },
         body: JSON.stringify({
           user_ids: options.userIds,
           theme_id: options.themeId,
           html: options.html,
           subject: options.subject,
           notification_category_name: options.notificationCategoryName,
           template_id: options.templateId,
           variables: options.variables,
         }),
       },
       null,
     );
-    return Result.ok(undefined);
+    return;

And update the signature to:

async sendEmail(options: {
  userIds: string[],
  themeId?: string | null | false,
  html?: string,
  subject?: string,
  notificationCategoryName?: string,
  templateId?: string,
  variables?: Record<string, any>,
}): Promise<void> // throws KnownErrors.RequiresCustomEmailServer | KnownErrors.SchemaError | KnownErrors.UserIdDoesNotExist
apps/backend/src/route-handlers/smart-request.tsx (1)

176-181: Require access-type when using workflow tokens.

If only x-stack-workflow-token is supplied (no keys), the current eitherKeyOrToken check won’t enforce x-stack-access-type. Include workflowToken to make header validation consistent.

Apply:

-  const eitherKeyOrToken = !!(publishableClientKey || secretServerKey || superSecretAdminKey || adminAccessToken);
+  const eitherKeyOrToken = !!(publishableClientKey || secretServerKey || superSecretAdminKey || adminAccessToken || workflowToken);
♻️ Duplicate comments (5)
apps/backend/src/lib/freestyle.tsx (1)

2-5: Nice: replaced raw JSON.parse with parseJson and Result flow

Prevents accidental throws in error paths and aligns with Result usage.

apps/backend/prisma/schema.prisma (1)

804-821: Enforce output XOR error; cascade on execution delete; add recent-by-execution index.

  • Add onDelete: Cascade on execution to avoid orphans.
  • Add DB CHECK to ensure exactly one of output or error is set.
  • Add index for recent triggers per execution.

Prisma schema and migration changes:

 model WorkflowTrigger {
@@
-  execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id])
+  execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id], onDelete: Cascade)
 
   @@id([tenancyId, executionId, id])
+  @@index([tenancyId, executionId, createdAt(sort: Desc)], name: "WorkflowTrigger_byExecution_recent")
 }

Migration SQL (add to the corresponding migration):

ALTER TABLE "WorkflowTrigger"
ADD CONSTRAINT "workflowtrigger_output_xor_error"
CHECK (
  (("output" IS NULL) <> ("error" IS NULL))
);
apps/backend/src/lib/workflows.tsx (3)

21-23: Type union missing "compile".

registeredTriggers can include "compile" (see tests). Expand the union to avoid unsafe casts.

Apply:

-type WorkflowRegisteredTriggerType = "sign-up";
+type WorkflowRegisteredTriggerType = "compile" | "sign-up";

340-384: Preserve trigger/execution IDs and return them to callers.

You generate triggerId/executionId but discard them. Return both for persistence and correlation.

Apply:

-async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> {
+async function triggerWorkflowRaw(
+  tenancy: Tenancy,
+  compiledWorkflowCode: string,
+  trigger: WorkflowTrigger,
+): Promise<Result<{ result: unknown, triggerId: string, executionId: string }, string>> {
@@
-    return Result.map(freestyleRes, (data) => data.result);
+    return Result.map(freestyleRes, (data) => ({
+      result: data.result,
+      triggerId,
+      executionId,
+    }));

386-416: Incorrect Execution association (bug) and missing ID persistence.

You connectOrCreate WorkflowExecution using compiledWorkflow.id as the execution ID — that’s wrong and collapses all runs into one execution. Use the IDs from triggerWorkflowRaw; persist them.

Apply:

   if (compiledWorkflow.compiledCode === null) {
     return Result.error(`Workflow ${compiledWorkflow.id} failed to compile: ${compiledWorkflow.compileError}`);
   }
-  const res = await triggerWorkflowRaw(tenancy, compiledWorkflow.compiledCode, trigger);
-  if (res.status === "error") {
-    console.log(`Compiled workflow failed to process trigger: ${res.error}`, { trigger, compiledWorkflowId: compiledWorkflow.id, res });
-  }
-  const prisma = await getPrismaClientForTenancy(tenancy);
-  await prisma.workflowTrigger.create({
-    data: {
-      triggerData: trigger,
-      ...(res.status === "ok" ? { output: (res.data ?? Prisma.JsonNull) as any } : { error: res.error }),
-      execution: {
-        connectOrCreate: {
-          where: {
-            tenancyId_id: {
-              tenancyId: tenancy.id,
-              id: compiledWorkflow.id,
-            },
-          },
-          create: {
-            tenancyId: tenancy.id,
-            compiledWorkflowId: compiledWorkflow.id,
-          },
-        },
-      },
-    },
-  });
+  const res = await triggerWorkflowRaw(tenancy, compiledWorkflow.compiledCode, trigger);
+  const prisma = await getPrismaClientForTenancy(tenancy);
+  if (res.status === "ok") {
+    await prisma.workflowTrigger.create({
+      data: {
+        tenancyId: tenancy.id,
+        id: res.data.triggerId,
+        triggerData: trigger,
+        output: (res.data.result ?? Prisma.JsonNull) as any,
+        execution: {
+          create: {
+            tenancyId: tenancy.id,
+            id: res.data.executionId,
+            compiledWorkflowId: compiledWorkflow.id,
+          },
+        },
+      },
+    });
+  } else {
+    console.log(`Compiled workflow failed to process trigger: ${res.error}`, { trigger, compiledWorkflowId: compiledWorkflow.id, res });
+    await prisma.workflowTrigger.create({
+      data: {
+        tenancyId: tenancy.id,
+        id: generateUuid(),
+        triggerData: trigger,
+        error: res.error,
+        execution: {
+          create: {
+            tenancyId: tenancy.id,
+            id: generateUuid(),
+            compiledWorkflowId: compiledWorkflow.id,
+          },
+        },
+      },
+    });
+  }
🧹 Nitpick comments (14)
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts (1)

52-53: No breaking call-site changes needed; add JSDoc for thrown errors
All usages simply await sendEmail(...) and none inspect a result wrapper. Update the interface to document thrown error types:

-    sendEmail(options: SendEmailOptions): Promise<void>,
+    /**
+     * Sends email to one or more users.
+     * Throws on failure (e.g. RequiresCustomEmailServer, SchemaError, UserIdDoesNotExist).
+     */
+    sendEmail(options: SendEmailOptions): Promise<void>,
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)

1119-1121: sendEmail now awaits and returns void — align consumer expectations and add JSDoc here too

Matches the interface change; ensure no callers rely on a Result. Consider adding a short JSDoc here mirroring the interface to list notable thrown errors.

Run the same repository-wide checks suggested in the interface comment to update call sites.

-  async sendEmail(options: SendEmailOptions): Promise<void> {
+  /**
+   * Throws on delivery/config/schema/user errors propagated by the backend.
+   */
+  async sendEmail(options: SendEmailOptions): Promise<void> {
     await this._interface.sendEmail(options);
   }
docker/dependencies/docker.compose.yaml (1)

178-184: Fix stale port in comment and keep config self-explanatory

The comment still points to 8119 while the mapping exposes 8080 on 8122. Suggest correcting to avoid confusion.

-      - "8122:8080"       # POST http://localhost:8119/execute/v1/script
+      - "8122:8080"       # POST http://localhost:8122/execute/v1/script
apps/e2e/tests/js/email.test.ts (1)

32-37: Use resolves.toBeUndefined() (or just await) instead of resolves.not.toThrow()

toThrow() is a matcher for functions, not resolved values; the current assertions pass vacuously. Assert resolution explicitly or simply await the call.

-  await expect(serverApp.sendEmail({
+  await expect(serverApp.sendEmail({
     userIds: [user.id],
     html: "<h1>Test Email</h1><p>This is a test email with HTML content.</p>",
     subject: "Test Subject",
-  })).resolves.not.toThrow();
+  })).resolves.toBeUndefined();
-  await expect(serverApp.sendEmail({
+  await expect(serverApp.sendEmail({
     userIds: [user.id],
     templateId: DEFAULT_TEMPLATE_IDS.sign_in_invitation,
     variables: {
       teamDisplayName: "Test Team",
       signInInvitationLink: "https://example.com",
     },
     subject: "Welcome!",
-  })).resolves.not.toThrow();
+  })).resolves.toBeUndefined();
-  await expect(serverApp.sendEmail({
+  await expect(serverApp.sendEmail({
     userIds: [user1.id, user2.id],
     html: "<p>Bulk email test</p>",
     subject: "Bulk Email Test",
-  })).resolves.not.toThrow();
+  })).resolves.toBeUndefined();
-  await expect(serverApp.sendEmail({
+  await expect(serverApp.sendEmail({
     userIds: [user.id],
     html: "<p>Themed email test</p>",
     subject: "Themed Email",
     themeId: DEFAULT_EMAIL_THEME_ID,
-  })).resolves.not.toThrow();
+  })).resolves.toBeUndefined();
-  await expect(serverApp.sendEmail({
+  await expect(serverApp.sendEmail({
     userIds: [user.id],
     html: "<p>Notification email test</p>",
     subject: "Notification Email",
     notificationCategoryName: "Transactional",
-  })).resolves.not.toThrow();
+  })).resolves.toBeUndefined();

Alternatively:

// simpler and reads well
await serverApp.sendEmail({...});

Also applies to: 48-57, 73-78, 89-95, 106-112

apps/e2e/tests/backend/workflows.test.ts (4)

5-28: Consider isolating email server config to a shared helper to reduce repetition and coupling to these tests

configureEmailAndWorkflow mixes workflow registration with SMTP setup; moving SMTP config to a project bootstrap helper will shorten tests and avoid accidental changes to SMTP defaults when only workflows are intended.


30-38: Make mailbox wait configurable and surface debug info for flakes

Hard-coded 10×1s retries can be tight under CI load. Add options for attempts/interval and include last message count in the error.

Apply this diff:

-async function waitForMailboxSubject(mailbox: Mailbox, subject: string) {
-  for (let i = 0; i < 10; i++) {
-    const messages = await mailbox.fetchMessages();
-    const message = messages.find((m) => m.subject === subject);
-    if (message) return;
-    await wait(1_000);
-  }
-  throw new Error(`Message with subject ${subject} not found after 10 tries`);
+async function waitForMailboxSubject(
+  mailbox: Mailbox,
+  subject: string,
+  { attempts = 20, intervalMs = 1_000 }: { attempts?: number; intervalMs?: number } = {},
+) {
+  let lastCount = 0;
+  for (let i = 0; i < attempts; i++) {
+    const messages = await mailbox.fetchMessages();
+    lastCount = messages.length;
+    if (messages.some((m) => m.subject === subject)) return;
+    await wait(intervalMs);
+  }
+  throw new Error(`Message with subject "${subject}" not found after ${attempts} tries (last mailbox size=${lastCount})`);
 }

137-137: Replace fixed sleeps with polling to reduce flakiness and cut test time

Static waits (12s) slow the suite and are brittle. Prefer polling until a condition is (not) met with a max timeout.

You can add this helper near the top of the file:

async function waitUntil(check: () => Promise<boolean>, { timeoutMs = 15_000, intervalMs = 500 } = {}) {
  const start = Date.now();
  for (;;) {
    if (await check()) return;
    if (Date.now() - start >= timeoutMs) throw new Error("waitUntil: timeout");
    await wait(intervalMs);
  }
}

Example replacement for Line 231:

await waitUntil(async () => {
  const me = await niceBackendFetch("/api/v1/users/me", { accessType: "server" });
  return me.body.is_anonymous === false && me.body.server_metadata?.[markerKey] === me.body.primary_email;
});

Also applies to: 223-223, 231-231, 257-257, 275-275


161-170: Tiny safety nit: keep the “bad compile” source minimally valid TS

Using return return is effective but can confuse tooling/highlighters. A shorter invalid snippet like @ or class { avoids any accidental lexing differences across TS versions.

apps/backend/prisma/migrations/20250903003239_workflow_triggers/migration.sql (1)

1-15: Add index for expiry-based cleanup and consider FK to tenancy for integrity

  • Create an index on (tenancyId, expiresAt) to speed up background deletions of expired tokens.
  • If your convention allows, add a foreign key to the tenancy table with ON DELETE CASCADE to prevent orphans. If FKs are intentionally avoided in this schema, ignore.

Suggested additions:

-- Speeds up pruning jobs
CREATE INDEX "WorkflowTriggerToken_tenancyId_expiresAt_idx"
  ON "WorkflowTriggerToken"("tenancyId", "expiresAt");

-- If you use FKs for tenancy (adjust table/column names to your schema)
-- ALTER TABLE "WorkflowTriggerToken"
--   ADD CONSTRAINT "WorkflowTriggerToken_tenancy_fkey"
--   FOREIGN KEY ("tenancyId") REFERENCES "Tenancy"("id") ON DELETE CASCADE;
apps/backend/src/route-handlers/smart-request.tsx (2)

278-280: Minor: fold “rare path” into bundled queries later.

As noted by the TODO, moving this to rawQueryAll would remove one extra roundtrip. Not urgent.


171-175: Optional: add telemetry around workflow-token auth.

A span or attributes (e.g., has-workflow-token: true, tenancyId) would help observability for these requests.

apps/backend/prisma/schema.prisma (1)

823-837: Execution relation hygiene and lookup speed.

  • Consider onDelete: Cascade from CompiledWorkflow to WorkflowExecution.
  • Add index on compiledWorkflowId for quick fetches per compiled workflow (e.g., dashboards).

Apply:

 model WorkflowExecution {
@@
-  compiledWorkflow   CompiledWorkflow @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id])
+  compiledWorkflow   CompiledWorkflow @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id], onDelete: Cascade)
@@
   @@id([tenancyId, id])
+  @@index([tenancyId, compiledWorkflowId], name: "WorkflowExecution_byCompiledWorkflow")
 }
apps/backend/src/lib/workflows.tsx (2)

17-19: Pin external package versions for reproducible builds.

'latest' is volatile. Use a pinned version (from lockfile/config), or inject via config.


324-335: Observability for compile timeouts/cleanup.

Consider logging retryInfo and adding metrics (count of deleted stale rows) to ease debugging intermittent compile stalls.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4c46d23 and 0f4ea9f.

📒 Files selected for processing (17)
  • apps/backend/prisma/migrations/20250903003239_workflow_triggers/migration.sql (1 hunks)
  • apps/backend/prisma/schema.prisma (2 hunks)
  • apps/backend/src/lib/email-rendering.tsx (1 hunks)
  • apps/backend/src/lib/freestyle.tsx (2 hunks)
  • apps/backend/src/lib/workflows.tsx (1 hunks)
  • apps/backend/src/middleware.tsx (1 hunks)
  • apps/backend/src/route-handlers/smart-request.tsx (5 hunks)
  • apps/backend/src/utils/vercel.tsx (1 hunks)
  • apps/e2e/tests/backend/workflows.test.ts (1 hunks)
  • apps/e2e/tests/js/email.test.ts (8 hunks)
  • docker/dependencies/docker.compose.yaml (1 hunks)
  • packages/stack-shared/src/interface/server-interface.ts (1 hunks)
  • packages/stack-shared/src/known-errors.tsx (2 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (2 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/common.ts (3 hunks)
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (3 hunks)
  • packages/template/src/lib/stack-app/apps/interfaces/server-app.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/backend/src/utils/vercel.tsx
  • packages/template/src/lib/stack-app/apps/implementations/common.ts
  • apps/backend/src/lib/email-rendering.tsx
🧰 Additional context used
📓 Path-based instructions (3)
packages/template/**

📄 CodeRabbit inference engine (AGENTS.md)

When modifying the SDK copies, make changes in packages/template (source of truth)

Files:

  • packages/template/src/lib/stack-app/apps/interfaces/server-app.ts
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • packages/template/src/lib/stack-app/apps/interfaces/server-app.ts
  • apps/backend/src/middleware.tsx
  • apps/e2e/tests/backend/workflows.test.ts
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
  • apps/backend/src/route-handlers/smart-request.tsx
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/stack-shared/src/known-errors.tsx
  • apps/e2e/tests/js/email.test.ts
  • apps/backend/src/lib/freestyle.tsx
  • apps/backend/src/lib/workflows.tsx
  • packages/stack-shared/src/interface/server-interface.ts
**/*.test.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

In tests, prefer .toMatchInlineSnapshot where possible; refer to snapshot-serializer.ts for snapshot formatting and handling of non-deterministic values

Files:

  • apps/e2e/tests/backend/workflows.test.ts
  • apps/e2e/tests/js/email.test.ts
🧬 Code graph analysis (9)
packages/template/src/lib/stack-app/apps/interfaces/server-app.ts (1)
packages/template/src/lib/stack-app/email/index.ts (1)
  • SendEmailOptions (10-22)
apps/e2e/tests/backend/workflows.test.ts (3)
apps/e2e/tests/helpers.ts (1)
  • Mailbox (199-224)
packages/stack-shared/src/utils/promises.tsx (1)
  • wait (260-268)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • bumpEmailAddress (172-180)
  • createMailbox (58-65)
  • niceBackendFetch (107-166)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (2)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
  • getDefaultExtraRequestHeaders (83-85)
packages/template/src/lib/stack-app/email/index.ts (1)
  • SendEmailOptions (10-22)
apps/backend/src/route-handlers/smart-request.tsx (4)
packages/stack-shared/src/known-errors.tsx (2)
  • KnownErrors (1570-1572)
  • KnownErrors (1574-1696)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
apps/backend/src/lib/workflows.tsx (1)
  • hashWorkflowTriggerToken (54-59)
apps/backend/src/prisma-client.tsx (1)
  • globalPrismaClient (31-31)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)
packages/template/src/lib/stack-app/apps/implementations/common.ts (1)
  • getDefaultExtraRequestHeaders (83-85)
packages/stack-shared/src/known-errors.tsx (1)
packages/stack-shared/src/index.ts (1)
  • KnownError (11-11)
apps/e2e/tests/js/email.test.ts (3)
packages/stack-shared/src/helpers/emails.ts (2)
  • DEFAULT_TEMPLATE_IDS (152-158)
  • DEFAULT_EMAIL_THEME_ID (105-105)
apps/e2e/tests/js/js-helpers.ts (1)
  • createApp (40-77)
packages/stack-shared/src/known-errors.tsx (2)
  • KnownErrors (1570-1572)
  • KnownErrors (1574-1696)
apps/backend/src/lib/freestyle.tsx (3)
packages/stack-shared/src/utils/env.tsx (2)
  • getEnvVariable (16-58)
  • getNodeEnvironment (65-67)
packages/stack-shared/src/utils/errors.tsx (1)
  • StackAssertionError (69-85)
packages/stack-shared/src/utils/json.tsx (1)
  • parseJson (72-74)
apps/backend/src/lib/workflows.tsx (12)
packages/stack-shared/src/utils/bytes.tsx (1)
  • encodeBase64 (114-116)
packages/stack-shared/src/utils/crypto.tsx (2)
  • hash (111-113)
  • generateSecureRandomString (20-26)
packages/stack-shared/src/utils/esbuild.tsx (1)
  • bundleJavaScript (33-128)
apps/backend/src/lib/tenancies.tsx (1)
  • Tenancy (47-47)
packages/stack-shared/src/utils/errors.tsx (4)
  • StackAssertionError (69-85)
  • captureError (126-134)
  • errorToNiceString (89-92)
  • throwErr (10-19)
packages/stack-shared/src/utils/promises.tsx (3)
  • timeout (390-396)
  • wait (260-268)
  • runAsynchronously (343-366)
packages/stack-shared/src/utils/arrays.tsx (1)
  • isStringArray (205-207)
apps/backend/src/prisma-client.tsx (3)
  • getPrismaClientForTenancy (51-53)
  • retryTransaction (124-193)
  • globalPrismaClient (31-31)
apps/backend/src/utils/vercel.tsx (2)
  • allPromisesAndWaitUntilEach (11-16)
  • runAsynchronouslyAndWaitUntil (5-9)
packages/stack-shared/src/utils/uuids.tsx (1)
  • generateUuid (3-8)
apps/backend/src/lib/freestyle.tsx (1)
  • Freestyle (8-48)
packages/stack-shared/src/utils/env.tsx (1)
  • getEnvVariable (16-58)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: build (22.x)
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: docker
  • GitHub Check: all-good
  • GitHub Check: setup-tests
  • GitHub Check: build (22.x)
  • GitHub Check: docker
  • GitHub Check: restart-dev-and-test
  • GitHub Check: Security Check
🔇 Additional comments (10)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)

35-36: Verify default extraRequestHeaders behavior across all app implementations
Confirmed usage of getDefaultExtraRequestHeaders in server-app-impl.ts (ln 269), admin-app-impl.ts (ln 79) and client-app-impl.ts (ln 320). After hardening common.ts, test each constructor with missing/invalid env vars.

apps/backend/src/middleware.tsx (1)

29-29: CORS: header addition looks good

Allowing x-stack-workflow-token for preflights is correct and aligns with the new auth flow.

apps/e2e/tests/js/email.test.ts (1)

114-127: Negative-path assertions look correct

rejects.toThrow on specific KnownErrors matches the new throwing semantics for sendEmail.

Also applies to: 134-139, 150-154, 165-171

packages/stack-shared/src/known-errors.tsx (1)

1550-1568: New workflow token errors: LGTM

Codes are unique, messages clear, and errors are wired into the export map.

Also applies to: 1694-1696

apps/backend/src/lib/freestyle.tsx (2)

14-19: Mock key guard looks good

Restricting the mock key to dev/test with a clear assertion is appropriate.


11-13: Make the Freestyle constructor read the env var safely

  • Replace the throwing getEnvVariable("STACK_FREESTYLE_API_KEY") call with a non-throwing lookup (e.g. process.env.STACK_FREESTYLE_API_KEY) or extend getEnvVariable to accept a default.
  • Ensure the two call sites in email-rendering.tsx and workflows.tsx (both new Freestyle()) no longer throw if neither options.apiKey nor the env var is set.
apps/backend/prisma/migrations/20250903003239_workflow_triggers/migration.sql (1)

6-8: Confirm updatedAt handling matches Prisma schema

updatedAt has no default here; ensure the Prisma model uses @updatedat so inserts/updates always set it. Otherwise, consider DEFAULT CURRENT_TIMESTAMP for inserts.

apps/backend/src/route-handlers/smart-request.tsx (2)

9-9: Import looks correct and scoped.

hashWorkflowTriggerToken is the right dependency here. No issues.


282-284: Deny admin with workflow tokens (good).

Explicitly throwing AdminAuthenticationRequired here is correct.

apps/backend/prisma/schema.prisma (1)

27-28: No-op change.

stripeAccountId whitespace-only; nothing to do.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0f4ea9f and e610ec9.

📒 Files selected for processing (3)
  • apps/backend/prisma/migrations/20250903062546_more_workflow_tokens/migration.sql (1 hunks)
  • apps/backend/prisma/schema.prisma (2 hunks)
  • apps/backend/src/lib/workflows.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/backend/src/lib/workflows.tsx
  • apps/backend/prisma/schema.prisma
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Security Check
🔇 Additional comments (2)
apps/backend/prisma/migrations/20250903062546_more_workflow_tokens/migration.sql (2)

9-10: Multi-tenant correctness: ensure cross-tenant integrity

If WorkflowTriggerToken and targets are tenant-scoped, enforce tenant alignment (e.g., via composite FKs or triggers) to prevent cross-tenant linkage.

Please verify the schema has a tenant/org/project key on all three tables and that linkage can’t cross tenants. If not, add appropriate constraints or a trigger.


1-7: Plan backfill and rollout order (expand/contract) to avoid downtime

  • Step 1 (this PR): add nullable columns + FKs + indexes.
  • Step 2: ship code that writes triggerId and, when available, executionId.
  • Step 3: backfill triggerId for existing rows; leave executionId null where no execution exists.
  • Step 4: enforce triggerId NOT NULL. Consider leaving executionId nullable long-term.

No matches were found when searching for FK or index creation on executionId/triggerId in other migrations—please manually verify that no existing migrations add these constraints or indexes.

@N2D4 N2D4 merged commit 40de0de into dev Sep 3, 2025
10 of 17 checks passed
@N2D4 N2D4 deleted the workflows branch September 3, 2025 08:22
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (5)
apps/backend/prisma/schema.prisma (4)

789-802: Index tokens by expiry for fast cleanup.
Add tenancy-scoped index on expiresAt to speed deletion of expired tokens during sweeps.

Apply:

 model WorkflowTriggerToken {
@@
   @@id([tenancyId, id])
   @@unique([tenancyId, tokenHash])
+  @@index([tenancyId, expiresAt], name: "WorkflowTriggerToken_expiry")
 }

846-858: Add index for stale compile cleanup.
Needed for deleteMany startedCompilingAt < now()-20s to avoid scans.

 model CurrentlyCompilingWorkflow {
@@
   @@id([tenancyId, workflowId, compilationVersion, sourceHash])
+  @@index([tenancyId, startedCompilingAt], name: "CurrentlyCompilingWorkflow_startedAt")
 }

804-829: Harden WorkflowTrigger integrity (FK action, state invariant, query index).

  • Add onDelete: Cascade on execution to avoid orphans.
  • Consider onDelete: SetNull on compiledWorkflow to allow pruning old compiles.
  • Enforce exactly one of [output, error] at DB level.
  • Add recent-by-execution index for common reads.

Apply:

 model WorkflowTrigger {
@@
-  compiledWorkflow   CompiledWorkflow? @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id])
+  compiledWorkflow   CompiledWorkflow? @relation(fields: [tenancyId, compiledWorkflowId], references: [tenancyId, id], onDelete: SetNull)
@@
-  execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id])
+  execution WorkflowExecution @relation(fields: [tenancyId, executionId], references: [tenancyId, id], onDelete: Cascade)
@@
-  @@id([tenancyId, id])
+  @@id([tenancyId, id])
+  @@index([tenancyId, executionId, createdAt(sort: Desc)], name: "WorkflowTrigger_byExecution_recent")
}

And add a migration CHECK:

ALTER TABLE "WorkflowTrigger"
ADD CONSTRAINT "workflowtrigger_output_xor_error"
CHECK (("output" IS NULL) <> ("error" IS NULL));

860-881: Enforce compiledCode XOR compileError.
Add DB CHECK so they’re mutually exclusive.

Migration example:

ALTER TABLE "CompiledWorkflow"
ADD CONSTRAINT "compiledworkflow_code_xor_error"
CHECK (("compiledCode" IS NULL) <> ("compileError" IS NULL));
apps/backend/src/lib/workflows.tsx (1)

134-167: Validate registeredTriggers at runtime.
Filter to known keys ("compile" | "callback" | "sign-up") to avoid garbage values from user code.

-      return Result.ok({
-        compiledCode: compiledCodeResult.data,
-        registeredTriggers: registeredTriggers,
-      });
+      const valid = (registeredTriggers as string[]).filter(t => t === "compile" || t === "callback" || t === "sign-up");
+      return Result.ok({ compiledCode: compiledCodeResult.data, registeredTriggers: valid });
🧹 Nitpick comments (9)
apps/backend/prisma/schema.prisma (1)

831-844: Optional: add read indexes for executions.
If you frequently fetch executions by workflowId or recency, add:

 model WorkflowExecution {
@@
   @@id([tenancyId, id])
+  @@index([tenancyId, workflowId, createdAt(sort: Desc)], name: "WorkflowExecution_byWorkflow_recent")
 }
apps/e2e/tests/backend/workflows.test.ts (1)

30-38: Reduce flakiness in mailbox polling.
Increase attempts or make timeout configurable; 10s can be tight in CI.

-async function waitForMailboxSubject(mailbox: Mailbox, subject: string) {
-  for (let i = 0; i < 10; i++) {
+async function waitForMailboxSubject(mailbox: Mailbox, subject: string, tries = 20, delayMs = 1000) {
+  for (let i = 0; i < tries; i++) {
     const messages = await mailbox.fetchMessages();
     const message = messages.find((m) => m.subject === subject);
     if (message) return;
-    await wait(1_000);
+    await wait(delayMs);
   }
   throw new Error(`Message with subject ${subject} not found after 10 tries`);
}
apps/backend/src/lib/workflows.tsx (2)

504-515: Handle per-workflow trigger errors without short-circuiting others.
Wrap each trigger in try/catch or use Promise.allSettled and log failures.

-    const promises = [...compiledWorkflows]
+    const promises = [...compiledWorkflows]
       .filter(([_, compiledWorkflow]) => compiledWorkflow.registeredTriggers.includes(trigger.type))
       .map(async ([workflowId, compiledWorkflow]) => {
         const dbTrigger = await createScheduledTrigger(tenancy, workflowId, trigger, new Date());
-        await triggerWorkflow(tenancy, compiledWorkflow, dbTrigger.id);
+        try {
+          await triggerWorkflow(tenancy, compiledWorkflow, dbTrigger.id);
+        } catch (e) {
+          captureError("workflow-trigger-exception", new StackAssertionError("Failed to trigger workflow", { cause: e, workflowId, trigger }));
+        }
       });
-    await Promise.all(promises);
+    await Promise.allSettled(promises);

500-502: Stubbed scheduler.
Implement polling of WorkflowTrigger where scheduledAt <= now and compiledWorkflowId is null, then dispatch via triggerWorkflow.

I can draft a minimal implementation that batches by tenancy and rate-limits; want me to open a follow-up PR?

apps/backend/prisma/migrations/20250903080405_workflows/migration.sql (5)

2-11: Add index for expirations and a simple validity check

Likely hot paths: sweeping expired tokens and validating TTLs. Add an index on (tenancyId, expiresAt) and a CHECK to ensure expiresAt > createdAt.

Apply:

 CREATE TABLE "WorkflowTriggerToken" (
   "tenancyId" UUID NOT NULL,
   "id" UUID NOT NULL,
   "tokenHash" TEXT NOT NULL,
   "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
   "updatedAt" TIMESTAMP(3) NOT NULL,
   "expiresAt" TIMESTAMP(3) NOT NULL,
 
   CONSTRAINT "WorkflowTriggerToken_pkey" PRIMARY KEY ("tenancyId","id")
 );
+
+-- CreateIndex: speed up expiry sweeps
+CREATE INDEX "WorkflowTriggerToken_tenancyId_expiresAt_idx"
+  ON "WorkflowTriggerToken"("tenancyId","expiresAt");
+
+-- Guardrail: expiresAt must be after createdAt
+ALTER TABLE "WorkflowTriggerToken"
+  ADD CONSTRAINT "WorkflowTriggerToken_expiresAt_after_createdAt_chk"
+  CHECK ("expiresAt" > "createdAt");

14-27: Index the scheduler and join keys; prevent double outcome

Scheduling/execution queries will scan by scheduledAt/executionId/compiledWorkflowId. Add targeted indexes and a CHECK so both output and error aren’t set simultaneously.

Apply:

 CREATE TABLE "WorkflowTrigger" (
   "tenancyId" UUID NOT NULL,
   "id" UUID NOT NULL,
   "executionId" UUID NOT NULL,
   "triggerData" JSONB NOT NULL,
   "scheduledAt" TIMESTAMP(3),
   "output" JSONB,
   "error" JSONB,
   "compiledWorkflowId" UUID,
   "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
   "updatedAt" TIMESTAMP(3) NOT NULL,
 
   CONSTRAINT "WorkflowTrigger_pkey" PRIMARY KEY ("tenancyId","id")
 );
+
+-- Due scheduling (only pending items)
+CREATE INDEX "WorkflowTrigger_tenancyId_scheduledAt_idx"
+  ON "WorkflowTrigger"("tenancyId","scheduledAt")
+  WHERE "scheduledAt" IS NOT NULL AND "output" IS NULL AND "error" IS NULL;
+
+-- Fast joins
+CREATE INDEX "WorkflowTrigger_tenancyId_executionId_idx"
+  ON "WorkflowTrigger"("tenancyId","executionId");
+CREATE INDEX "WorkflowTrigger_tenancyId_compiledWorkflowId_idx"
+  ON "WorkflowTrigger"("tenancyId","compiledWorkflowId")
+  WHERE "compiledWorkflowId" IS NOT NULL;
+
+-- Guardrail: not both output and error
+ALTER TABLE "WorkflowTrigger"
+  ADD CONSTRAINT "WorkflowTrigger_output_or_error_chk"
+  CHECK (("output" IS NULL) OR ("error" IS NULL));

55-69: Add a “latest per workflow” index

Fetching the latest compiled blob typically needs (tenant, workflowId, compilationVersion DESC). The existing uniqueness on (tenant, workflowId, compilationVersion, sourceHash) is great; add a covering index for recency queries.

Apply:

 CREATE TABLE "CompiledWorkflow" (
   "tenancyId" UUID NOT NULL,
   "id" UUID NOT NULL,
   "workflowId" TEXT NOT NULL,
   "compilationVersion" INTEGER NOT NULL,
   "sourceHash" TEXT NOT NULL,
   "compiledCode" TEXT,
   "compileError" TEXT,
   "compiledAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
   "registeredTriggers" TEXT[],
   "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
   "updatedAt" TIMESTAMP(3) NOT NULL,
 
   CONSTRAINT "CompiledWorkflow_pkey" PRIMARY KEY ("tenancyId","id")
 );
+
+CREATE INDEX "CompiledWorkflow_tenancyId_workflowId_compilationVersion_idx"
+  ON "CompiledWorkflow"("tenancyId","workflowId","compilationVersion" DESC);

6-7: Who updates “updatedAt”? Add DB-side default/trigger or ensure app discipline

updatedAt is NOT NULL without a default. Confirm your ORM sets it on every write; otherwise writes will fail. If you want DB enforcement, add a trigger to auto-touch updatedAt.

I can supply a Postgres trigger function/migration if you want DB-managed updatedAt.

Also applies to: 24-25, 36-37, 49-50, 66-67


64-65: Consider normalizing registeredTriggers

If you need queries by trigger value or to enforce referential integrity, move registeredTriggers TEXT[] into a join table. Keep as array only if write-once/read-all patterns dominate.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e610ec9 and 437bafa.

📒 Files selected for processing (4)
  • apps/backend/prisma/migrations/20250903080405_workflows/migration.sql (1 hunks)
  • apps/backend/prisma/schema.prisma (2 hunks)
  • apps/backend/src/lib/workflows.tsx (1 hunks)
  • apps/e2e/tests/backend/workflows.test.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Prefer ES6 Map over Record when representing key–value collections

Files:

  • apps/backend/src/lib/workflows.tsx
  • apps/e2e/tests/backend/workflows.test.ts
**/*.test.{ts,tsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

In tests, prefer .toMatchInlineSnapshot where possible; refer to snapshot-serializer.ts for snapshot formatting and handling of non-deterministic values

Files:

  • apps/e2e/tests/backend/workflows.test.ts
🧬 Code graph analysis (2)
apps/backend/src/lib/workflows.tsx (11)
packages/stack-shared/src/utils/bytes.tsx (1)
  • encodeBase64 (114-116)
packages/stack-shared/src/utils/crypto.tsx (2)
  • hash (111-113)
  • generateSecureRandomString (20-26)
packages/stack-shared/src/utils/esbuild.tsx (1)
  • bundleJavaScript (33-128)
packages/stack-shared/src/utils/errors.tsx (4)
  • StackAssertionError (69-85)
  • captureError (126-134)
  • errorToNiceString (89-92)
  • throwErr (10-19)
packages/stack-shared/src/utils/promises.tsx (3)
  • timeout (390-396)
  • wait (260-268)
  • runAsynchronously (343-366)
packages/stack-shared/src/utils/arrays.tsx (1)
  • isStringArray (205-207)
apps/backend/src/prisma-client.tsx (3)
  • getPrismaClientForTenancy (51-53)
  • retryTransaction (124-193)
  • globalPrismaClient (31-31)
apps/backend/src/utils/vercel.tsx (2)
  • allPromisesAndWaitUntilEach (11-16)
  • runAsynchronouslyAndWaitUntil (5-9)
apps/backend/src/lib/freestyle.tsx (1)
  • Freestyle (8-48)
packages/stack-shared/src/utils/env.tsx (1)
  • getEnvVariable (16-58)
packages/stack-shared/src/utils/uuids.tsx (1)
  • generateUuid (3-8)
apps/e2e/tests/backend/workflows.test.ts (3)
apps/e2e/tests/helpers.ts (1)
  • Mailbox (199-224)
packages/stack-shared/src/utils/promises.tsx (1)
  • wait (260-268)
apps/e2e/tests/backend/backend-helpers.ts (3)
  • bumpEmailAddress (172-180)
  • createMailbox (58-65)
  • niceBackendFetch (107-166)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: setup-tests
  • GitHub Check: docker
  • GitHub Check: restart-dev-and-test
  • GitHub Check: all-good
  • GitHub Check: lint_and_build (latest)
  • GitHub Check: docker
  • GitHub Check: build (22.x)
  • GitHub Check: build (22.x)
  • GitHub Check: Security Check
🔇 Additional comments (9)
apps/backend/prisma/schema.prisma (2)

27-27: LGTM: formatting-only change.


776-787: LGTM: DataVaultEntry looks correct.

  • Composite uniqueness and secondary index make sense for hot paths.
apps/e2e/tests/backend/workflows.test.ts (3)

5-28: Good test setup helper.
Concise config writer; enables per-test workflow source injection.


138-146: Nice negative test.
Wait-and-assert-only-verification mail ensures disabled workflows don’t send.


248-289: Versioning test is clear and effective.
Validates that subsequent compiles take effect without retroactive changes.

apps/backend/prisma/migrations/20250903080405_workflows/migration.sql (4)

2-11: Multi-tenant PK and unique hash look solid

Composite PK on (tenancyId, id) and unique (tenancyId, tokenHash) are appropriate for isolation and O(1) lookups by hash.


42-52: Clarify concurrency semantics for “CurrentlyCompilingWorkflow”

If you intend “at most one active compile per workflow per tenant,” consider a unique constraint on (tenancyId, workflowId). If parallel compiles are valid, keep as-is.

Optional index for retrieving the latest attempt:

CREATE INDEX "CurrentlyCompilingWorkflow_tenancyId_workflowId_version_idx"
  ON "CurrentlyCompilingWorkflow"("tenancyId","workflowId","compilationVersion" DESC);

71-76: Good: critical uniqueness covered

  • Unique (tenancyId, tokenHash) prevents duplicate tokens per tenant.
  • Unique (tenancyId, workflowId, compilationVersion, sourceHash) ensures no duplicate artifacts for the same build inputs.

78-81: Double-check ON DELETE policies

RESTRICT is safe, but verify operational needs:

  • Deleting a WorkflowExecution: should its triggers cascade? If yes, switch to ON DELETE CASCADE.
  • Deleting a CompiledWorkflow: if old artifacts can be pruned even when triggers exist, consider SET NULL on compiledWorkflowId (it’s nullable) to allow GC.

If desired, adjust:

-ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "WorkflowTrigger_tenancyId_compiledWorkflowId_fkey" FOREIGN KEY ("tenancyId", "compiledWorkflowId") REFERENCES "CompiledWorkflow"("tenancyId", "id") ON DELETE RESTRICT ON UPDATE CASCADE;
+ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "WorkflowTrigger_tenancyId_compiledWorkflowId_fkey" FOREIGN KEY ("tenancyId", "compiledWorkflowId") REFERENCES "CompiledWorkflow"("tenancyId", "id") ON DELETE SET NULL ON UPDATE CASCADE;

-ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "WorkflowTrigger_tenancyId_executionId_fkey" FOREIGN KEY ("tenancyId", "executionId") REFERENCES "WorkflowExecution"("tenancyId", "id") ON DELETE RESTRICT ON UPDATE CASCADE;
+ALTER TABLE "WorkflowTrigger" ADD CONSTRAINT "WorkflowTrigger_tenancyId_executionId_fkey" FOREIGN KEY ("tenancyId", "executionId") REFERENCES "WorkflowExecution"("tenancyId", "id") ON DELETE CASCADE ON UPDATE CASCADE;

Comment on lines +30 to +36
CREATE TABLE "WorkflowExecution" (
"tenancyId" UUID NOT NULL,
"id" UUID NOT NULL,
"workflowId" TEXT NOT NULL,
"triggerIds" TEXT[],
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove triggerIds array to avoid drift; rely on FK from WorkflowTrigger

WorkflowTrigger already has executionId -> WorkflowExecution. Keeping triggerIds TEXT[] duplicates the relationship without FK guarantees and can desynchronize, causing data integrity issues.

Apply (either edit this migration before apply, or follow-up migration to drop the column):

 CREATE TABLE "WorkflowExecution" (
   "tenancyId" UUID NOT NULL,
   "id" UUID NOT NULL,
   "workflowId" TEXT NOT NULL,
-  "triggerIds" TEXT[],
   "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
   "updatedAt" TIMESTAMP(3) NOT NULL,
 
   CONSTRAINT "WorkflowExecution_pkey" PRIMARY KEY ("tenancyId","id")
 );

Optional: add an index for common lookups:

CREATE INDEX "WorkflowExecution_tenancyId_workflowId_idx"
  ON "WorkflowExecution"("tenancyId","workflowId");
🤖 Prompt for AI Agents
In apps/backend/prisma/migrations/20250903080405_workflows/migration.sql around
lines 30-36, remove the redundant "triggerIds" TEXT[] column from the
WorkflowExecution CREATE TABLE statement (or if the migration has already been
applied, create a follow-up migration that drops the triggerIds column) so the
relationship is enforced only via the WorkflowTrigger.executionId FK;
additionally consider adding the suggested composite index on (tenancyId,
workflowId) for common lookups by creating an index named
WorkflowExecution_tenancyId_workflowId_idx.

Comment on lines +76 to +87
const registeredCallbacks = new Map();
globalThis.registerCallback = (callbackId, func) => {
registeredCallbacks.set(callbackId, func);
};
_registerTrigger("callback", ({ callbackId, data }) => {
const callbackFunc = registeredCallbacks.get(callbackId);
if (!callbackFunc) {
throw new Error(\`Callback \${callbackId} not found. Was it maybe deleted from the workflow?\`);
}
return callbackFunc(JSON.parse(data.dataJson));
});
let scheduledCallback = undefined;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: callback data is parsed incorrectly.
scheduleCallback stores opaque data; callback triggers receive it as-is. JSON.parse on data.dataJson will throw.

-        _registerTrigger("callback", ({ callbackId, data }) => {
+        _registerTrigger("callback", ({ callbackId, data }) => {
           const callbackFunc = registeredCallbacks.get(callbackId);
           if (!callbackFunc) {
             throw new Error(`Callback ${callbackId} not found. Was it maybe deleted from the workflow?`);
           }
-          return callbackFunc(JSON.parse(data.dataJson));
+          return callbackFunc(data);
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const registeredCallbacks = new Map();
globalThis.registerCallback = (callbackId, func) => {
registeredCallbacks.set(callbackId, func);
};
_registerTrigger("callback", ({ callbackId, data }) => {
const callbackFunc = registeredCallbacks.get(callbackId);
if (!callbackFunc) {
throw new Error(\`Callback \${callbackId} not found. Was it maybe deleted from the workflow?\`);
}
return callbackFunc(JSON.parse(data.dataJson));
});
let scheduledCallback = undefined;
const registeredCallbacks = new Map();
globalThis.registerCallback = (callbackId, func) => {
registeredCallbacks.set(callbackId, func);
};
_registerTrigger("callback", ({ callbackId, data }) => {
const callbackFunc = registeredCallbacks.get(callbackId);
if (!callbackFunc) {
throw new Error(`Callback ${callbackId} not found. Was it maybe deleted from the workflow?`);
}
return callbackFunc(data);
});
let scheduledCallback = undefined;
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 76 to 87, the callback
handler incorrectly does JSON.parse(data.dataJson) even though scheduled
callbacks store opaque data and may already be objects; this causes a runtime
throw. Replace the JSON.parse call with a safe pass-through: if data.dataJson is
a string then parse it, otherwise pass data.dataJson directly (or simply pass
data.dataJson without parsing if the system always stores raw values). Update
the return to call callbackFunc with the resolved value so callbacks get the
original opaque data unchanged.

Comment on lines +367 to +389
async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> {
const workflowToken = generateSecureRandomString();
const workflowTriggerToken = await globalPrismaClient.workflowTriggerToken.create({
data: {
expiresAt: new Date(Date.now() + 1000 * 35),
tenancyId: tenancy.id,
tokenHash: await hashWorkflowTriggerToken(workflowToken),
},
});

const tokenRefreshInterval = setInterval(() => {
runAsynchronously(async () => {
await globalPrismaClient.workflowTriggerToken.update({
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: workflowTriggerToken.id,
},
},
data: { expiresAt: new Date(Date.now() + 1000 * 35) },
});
});
}, 10_000);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use tenancy-scoped Prisma client for tokens.
Avoid mixing global and tenancy clients; keeps workflow tokens colocated with the tenancy’s source of truth.

-  const workflowTriggerToken = await globalPrismaClient.workflowTriggerToken.create({
+  const prisma = await getPrismaClientForTenancy(tenancy);
+  const workflowTriggerToken = await prisma.workflowTriggerToken.create({
@@
-      await globalPrismaClient.workflowTriggerToken.update({
+      await prisma.workflowTriggerToken.update({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function triggerWorkflowRaw(tenancy: Tenancy, compiledWorkflowCode: string, trigger: WorkflowTrigger): Promise<Result<unknown, string>> {
const workflowToken = generateSecureRandomString();
const workflowTriggerToken = await globalPrismaClient.workflowTriggerToken.create({
data: {
expiresAt: new Date(Date.now() + 1000 * 35),
tenancyId: tenancy.id,
tokenHash: await hashWorkflowTriggerToken(workflowToken),
},
});
const tokenRefreshInterval = setInterval(() => {
runAsynchronously(async () => {
await globalPrismaClient.workflowTriggerToken.update({
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: workflowTriggerToken.id,
},
},
data: { expiresAt: new Date(Date.now() + 1000 * 35) },
});
});
}, 10_000);
async function triggerWorkflowRaw(
tenancy: Tenancy,
compiledWorkflowCode: string,
trigger: WorkflowTrigger
): Promise<Result<unknown, string>> {
const workflowToken = generateSecureRandomString();
const prisma = await getPrismaClientForTenancy(tenancy);
const workflowTriggerToken = await prisma.workflowTriggerToken.create({
data: {
expiresAt: new Date(Date.now() + 1000 * 35),
tenancyId: tenancy.id,
tokenHash: await hashWorkflowTriggerToken(workflowToken),
},
});
const tokenRefreshInterval = setInterval(() => {
runAsynchronously(async () => {
await prisma.workflowTriggerToken.update({
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: workflowTriggerToken.id,
},
},
data: { expiresAt: new Date(Date.now() + 1000 * 35) },
});
});
}, 10_000);
// …rest of function…
}
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 367 to 389, the code is
creating and refreshing workflow trigger tokens using globalPrismaClient; switch
to the tenancy-scoped Prisma client so tokens are stored with the tenancy’s
data. Replace calls to globalPrismaClient.workflowTriggerToken.create/update
with the tenancy-scoped client (e.g., tenancy.prisma or tenancy.prismaClient—use
whatever property holds the per-tenancy Prisma instance in this codebase),
keeping the same create and update payloads and error handling, and ensure the
interval continuation also uses that tenancy client instead of
globalPrismaClient.

Comment on lines +410 to +431
async function createScheduledTrigger(tenancy: Tenancy, workflowId: string, trigger: WorkflowTrigger, scheduledAt: Date) {
const executionId = trigger.type === "callback" ? trigger.executionId : generateUuid();

const prisma = await getPrismaClientForTenancy(tenancy);
const dbTrigger = await prisma.workflowTrigger.create({
data: {
triggerData: trigger as any,
scheduledAt,
execution: {
connectOrCreate: {
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: executionId,
},
},
create: {
tenancyId: tenancy.id,
workflowId,
},
},
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

connectOrCreate bug: execution.id mismatch.
You generate executionId for connectOrCreate.where but don’t set it in create, causing a mismatch.

         connectOrCreate: {
           where: {
             tenancyId_id: {
               tenancyId: tenancy.id,
               id: executionId,
             },
           },
           create: {
             tenancyId: tenancy.id,
+            id: executionId,
             workflowId,
           },
         },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function createScheduledTrigger(tenancy: Tenancy, workflowId: string, trigger: WorkflowTrigger, scheduledAt: Date) {
const executionId = trigger.type === "callback" ? trigger.executionId : generateUuid();
const prisma = await getPrismaClientForTenancy(tenancy);
const dbTrigger = await prisma.workflowTrigger.create({
data: {
triggerData: trigger as any,
scheduledAt,
execution: {
connectOrCreate: {
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: executionId,
},
},
create: {
tenancyId: tenancy.id,
workflowId,
},
},
},
async function createScheduledTrigger(
tenancy: Tenancy,
workflowId: string,
trigger: WorkflowTrigger,
scheduledAt: Date
) {
const executionId =
trigger.type === "callback" ? trigger.executionId : generateUuid();
const prisma = await getPrismaClientForTenancy(tenancy);
const dbTrigger = await prisma.workflowTrigger.create({
data: {
triggerData: trigger as any,
scheduledAt,
execution: {
connectOrCreate: {
where: {
tenancyId_id: {
tenancyId: tenancy.id,
id: executionId,
},
},
create: {
tenancyId: tenancy.id,
id: executionId, // ← ensure the new execution uses the intended ID
workflowId,
},
},
},
},
});
// ...rest of the function
}
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 410 to 431, the
connectOrCreate uses executionId in the where clause but the create object
doesn't set the same id, causing a mismatch; fix by adding id: executionId to
the create payload (and ensure tenancyId and workflowId remain present) so the
connectOrCreate where and create objects reference the same execution id.

Comment on lines +469 to +479
compiledWorkflow.id,
{
type: "callback",
callbackId,
data: callbackData,
scheduledAtMillis: scheduleAt.getTime(),
callerTriggerId: triggerId,
executionId: trigger.executionId,
},
scheduleAt
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Wrong workflow identifier for scheduled callbacks.
Pass the logical workflowId, not the compiledWorkflow row id.

-        compiledWorkflow.id,
+        compiledWorkflow.workflowId,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
compiledWorkflow.id,
{
type: "callback",
callbackId,
data: callbackData,
scheduledAtMillis: scheduleAt.getTime(),
callerTriggerId: triggerId,
executionId: trigger.executionId,
},
scheduleAt
);
compiledWorkflow.workflowId,
{
type: "callback",
callbackId,
data: callbackData,
scheduledAtMillis: scheduleAt.getTime(),
callerTriggerId: triggerId,
executionId: trigger.executionId,
},
scheduleAt
);
🤖 Prompt for AI Agents
In apps/backend/src/lib/workflows.tsx around lines 469 to 479, the scheduled
callback is being created with compiledWorkflow.id (the compiled row id) but
should use the logical workflowId; replace the compiledWorkflow.id argument with
the logical workflowId variable used earlier in this scope (or the upstream
workflowId parameter) so the scheduled callback references the correct workflow
identifier when calling the scheduling function and ensure the value matches the
expected type/name in that function signature.

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