Skip to content

feat(webapp): Org level feature flags for Private Links#3287

Open
0ski wants to merge 1 commit intomainfrom
oskar/feat-private-links-ff
Open

feat(webapp): Org level feature flags for Private Links#3287
0ski wants to merge 1 commit intomainfrom
oskar/feat-private-links-ff

Conversation

@0ski
Copy link
Copy Markdown
Collaborator

@0ski 0ski commented Mar 28, 2026

No description provided.

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 28, 2026

⚠️ No Changeset found

Latest commit: cdb1ba7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

Walkthrough

This PR introduces a new hasPrivateConnections feature flag and refactors its application across the codebase. A new async function canAccessPrivateConnections() evaluates organization-specific flag values and is used by route loaders for access control. The feature flag catalog is extended with the new key, validation rules, and improved logic for handling falsy overrides and null database states. The sidebar menu component now uses the feature flags hook, while the organizations presenter supplies environment-derived defaults when fetching global flags.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is completely empty and does not follow the required template structure with Testing, Changelog, and other sections. Add a comprehensive description following the template: include the issue number, testing steps, changelog entry, and any relevant screenshots or context.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: introducing organization-level feature flags for Private Connections functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch oskar/feat-private-links-ff

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@0ski 0ski added the ready label Mar 28, 2026
@0ski 0ski marked this pull request as ready for review March 28, 2026 14:55
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +22 to +26
return flag({
key: FEATURE_FLAG.hasPrivateConnections,
defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1",
overrides: (org?.featureFlags as Record<string, unknown>) ?? {},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 Managed cloud requirement intentionally dropped for private connections

The old hasPrivateConnections in features.server.ts:18-23 required BOTH PRIVATE_CONNECTIONS_ENABLED === "1" AND the host being managed cloud (isManagedCloud(host)). The new implementation in canAccessPrivateConnections.server.ts:24 and OrganizationsPresenter.server.ts:161 only checks the env var (or feature flag override), dropping the managed cloud gate. This means self-hosted instances with PRIVATE_CONNECTIONS_ENABLED=1 will now see Private Connections in the UI and be able to access the routes, which was previously blocked. This appears intentional (moving to per-org flag control), but is worth confirming since it broadens the feature surface.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const flagSchema = FeatureFlagCatalog[opts.key];

if (opts.overrides?.[opts.key]) {
if (opts.overrides?.[opts.key] !== undefined) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 makeFlag override fix changes behavior for falsy org flag values across all callers

The change from if (opts.overrides?.[opts.key]) (truthy check) to if (opts.overrides?.[opts.key] !== undefined) at featureFlags.server.ts:52 is a correctness fix, but it subtly changes behavior for ALL existing callers of makeFlag, not just the new hasPrivateConnections code. Specifically, if any organization has a boolean flag like hasQueryAccess: false or hasAiAccess: false stored in their featureFlags JSON column, the old code would silently ignore this false override and fall through to the DB global value. The new code correctly respects the false override. I verified all existing callers (canAccessQuery, canAccessAi, canAccessAiModels, eventRepository, workerGroupService, runsRepository) — for non-boolean flags this has no effect, and for boolean flags the fix is the correct behavior. However, if any orgs have false flags that were previously being bypassed, they will now take effect.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx`:
- Around line 12-13: The OrganizationSettingsSideMenu is currently hiding
sidebar items using feature-flag hooks (useFeatureFlags, useFeatures) at the nav
level; remove that gating so menu entries are always rendered and let
route/loader authorization handle access instead: update the
OrganizationSettingsSideMenu component to stop conditionally rendering the menu
item(s) based on useFeatureFlags/useFeatures (including the blocks around the
link(s) referenced in the component where features are checked) so the links are
always present, but keep any existing route-level guards intact.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.settings.private-connections._index/route.tsx:
- Around line 47-49: The delete action is not currently protected by the same
org-level check as the loader; call canAccessPrivateConnections({
organizationSlug, userId }) at the start of the action handler (the export const
action or the function handling the delete flow around the deletion logic
starting at line ~71) and, if it returns false, short-circuit by returning a
redirect to organizationPath({ slug: organizationSlug }) (or an appropriate
unauthorized response) before performing any deletion or state mutation; ensure
you reference the same organizationSlug and userId used in the loader so the
guard logic is identical.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.settings.private-connections.new/route.tsx:
- Around line 59-62: The POST/action handler must enforce the same access gate
as the loader: call canAccessPrivateConnections({ organizationSlug, userId }) at
the start of the action (before performing the create mutation), and if it
returns false perform the same redirect to organizationPath({ slug:
organizationSlug }) (or return an appropriate unauthorized response) to prevent
direct form POST bypass; update the action function in this file to reuse the
canAccessPrivateConnections check and short-circuit before running the mutation
logic that currently lives after line 95.

In `@apps/webapp/app/v3/canAccessPrivateConnections.server.ts`:
- Around line 11-26: The org lookup can be null which should short-circuit
access; change the flow in the code that calls prisma.organization.findFirst
(variable org) so that if org is falsy you immediately return false instead of
evaluating the feature flag; keep using makeFlag and
FEATURE_FLAG.hasPrivateConnections only when org exists and pass overrides from
org.featureFlags, but do not fall back to env.PRIVATE_CONNECTIONS_ENABLED for
non-members or missing orgs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 09e14c70-dfc4-4e63-836d-a048a93c6607

📥 Commits

Reviewing files that changed from the base of the PR and between 2366b21 and cdb1ba7.

📒 Files selected for processing (6)
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
📜 Review details
⏰ 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). (28)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (5, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (6, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (7, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (2, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (5, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (8, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (3, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (3, 8)
  • GitHub Check: units / webapp / 🧪 Unit Tests: Webapp (1, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (6, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (4, 8)
  • GitHub Check: units / internal / 🧪 Unit Tests: Internal (2, 8)
  • GitHub Check: units / packages / 🧪 Unit Tests: Packages (1, 1)
  • GitHub Check: sdk-compat / Bun Runtime
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - npm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - npm)
  • GitHub Check: sdk-compat / Node.js 20.20 (ubuntu-latest)
  • GitHub Check: typecheck / typecheck
  • GitHub Check: e2e / 🧪 CLI v3 tests (windows-latest - pnpm)
  • GitHub Check: e2e / 🧪 CLI v3 tests (ubuntu-latest - pnpm)
  • GitHub Check: sdk-compat / Deno Runtime
  • GitHub Check: sdk-compat / Node.js 22.12 (ubuntu-latest)
  • GitHub Check: sdk-compat / Cloudflare Workers
  • GitHub Check: Analyze (javascript-typescript)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{ts,tsx}: Use types over interfaces for TypeScript
Avoid using enums; prefer string unions or const objects instead

**/*.{ts,tsx}: For apps and internal packages (apps/*, internal-packages/*), use pnpm run typecheck --filter <package> for verification, never use build as it proves almost nothing about correctness
Use testcontainers helpers (redisTest, postgresTest, containerTest from @internal/testcontainers) for integration tests with Redis and PostgreSQL instead of mocking
When writing Trigger.dev tasks, always import from @trigger.dev/sdk - never use @trigger.dev/sdk/v3 or deprecated client.defineJob

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
{packages/core,apps/webapp}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use zod for validation in packages/core and apps/webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use function declarations instead of default exports

**/*.{ts,tsx,js,jsx}: Use pnpm for package management in this monorepo (version 10.23.0) with Turborepo for orchestration - run commands from root with pnpm run
Add crumbs as you write code for debug tracing using // @Crumbs comments or `// `#region` `@crumbs blocks - they stay on the branch throughout development and are stripped via agentcrumbs strip before merge

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
apps/webapp/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
apps/webapp/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/webapp.mdc)

apps/webapp/**/*.{ts,tsx}: When importing from @trigger.dev/core in the webapp, use subpath exports from the package.json instead of importing from the root path
Follow the Remix 2.1.0 and Express server conventions when updating the main trigger.dev webapp

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
**/*.{js,ts,jsx,tsx,json,md,yaml,yml}

📄 CodeRabbit inference engine (AGENTS.md)

Format code using Prettier before committing

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
apps/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

When modifying only server components (apps/webapp/, apps/supervisor/, etc.) with no package changes, add a .server-changes/ file instead of a changeset

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
apps/webapp/app/**/*.{ts,tsx,server.ts}

📄 CodeRabbit inference engine (apps/webapp/CLAUDE.md)

Access environment variables via env export from app/env.server.ts. Never use process.env directly

Files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/otel-metrics.mdc)

**/*.ts: When creating or editing OTEL metrics (counters, histograms, gauges), ensure metric attributes have low cardinality by using only enums, booleans, bounded error codes, or bounded shard IDs
Do not use high-cardinality attributes in OTEL metrics such as UUIDs/IDs (envId, userId, runId, projectId, organizationId), unbounded integers (itemCount, batchSize, retryCount), timestamps (createdAt, startTime), or free-form strings (errorMessage, taskName, queueName)
When exporting OTEL metrics via OTLP to Prometheus, be aware that the exporter automatically adds unit suffixes to metric names (e.g., 'my_duration_ms' becomes 'my_duration_ms_milliseconds', 'my_counter' becomes 'my_counter_total'). Account for these transformations when writing Grafana dashboards or Prometheus queries

Files:

  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
apps/webapp/app/v3/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

In the webapp v3 directory, only modify V2 code paths when encountering V1/V2 branching in services - all new work uses Run Engine 2.0 (@internal/run-engine) and redis-worker, not legacy V1 engine code

Files:

  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
🧠 Learnings (14)
📓 Common learnings
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 3264
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx:176-176
Timestamp: 2026-03-26T17:27:07.048Z
Learning: In `apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx`, the variable `hasPrivateNetworking` is intentionally hardcoded to `true` as a placeholder. Plan-level gating will be wired to actual billing data (e.g., `plan?.v3Subscription?.plan?.limits?.hasPrivateNetworking`) once the billing integration is complete. The route is already guarded at the feature-flag level via `hasPrivateConnections` in the loader. Do not flag this hardcoded value as dead code or a bug until the billing integration is in place.
Learnt from: nicktrn
Repo: triggerdotdev/trigger.dev PR: 3200
File: docs/config/config-file.mdx:353-368
Timestamp: 2026-03-10T12:44:19.869Z
Learning: In the triggerdotdev/trigger.dev repository, docs PRs are often written as companions to implementation PRs (e.g., PR `#3200` documents features being added in PR `#3196`). When reviewing docs PRs, the documented features may exist in a companion/companion PR branch rather than main. Always check companion PRs referenced in the PR description before flagging missing implementations.
📚 Learning: 2026-03-26T17:27:07.048Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 3264
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx:176-176
Timestamp: 2026-03-26T17:27:07.048Z
Learning: In `apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx`, the variable `hasPrivateNetworking` is intentionally hardcoded to `true` as a placeholder. Plan-level gating will be wired to actual billing data (e.g., `plan?.v3Subscription?.plan?.limits?.hasPrivateNetworking`) once the billing integration is complete. The route is already guarded at the feature-flag level via `hasPrivateConnections` in the loader. Do not flag this hardcoded value as dead code or a bug until the billing integration is in place.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
📚 Learning: 2026-02-11T16:50:14.167Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx:126-131
Timestamp: 2026-02-11T16:50:14.167Z
Learning: In apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboards.$dashboardId.widgets.tsx, MetricsDashboard entities are intentionally scoped to the organization level, not the project level. The dashboard lookup should filter by organizationId only (not projectId), allowing dashboards to be accessed across projects within the same organization. The optional projectId field on MetricsDashboard serves other purposes and should not be used as an authorization constraint.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
📚 Learning: 2026-03-10T17:56:26.581Z
Learnt from: samejr
Repo: triggerdotdev/trigger.dev PR: 3201
File: apps/webapp/app/v3/services/setSeatsAddOn.server.ts:25-29
Timestamp: 2026-03-10T17:56:26.581Z
Learning: In the `triggerdotdev/trigger.dev` webapp, service classes such as `SetSeatsAddOnService` and `SetBranchesAddOnService` do NOT need to perform their own userId-to-organizationId authorization checks. Auth is enforced at the route layer: `requireUserId(request)` authenticates the user, and the `_app.orgs.$organizationSlug` layout route enforces that the authenticated user is a member of the org. Any `userId` and `organizationId` reaching these services from org-scoped routes are already validated. This is the consistent pattern used across all org-scoped services in the codebase.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
📚 Learning: 2026-02-04T16:34:48.876Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/vercel.connect.tsx:13-27
Timestamp: 2026-02-04T16:34:48.876Z
Learning: In apps/webapp/app/routes/vercel.connect.tsx, configurationId may be absent for "dashboard" flows but must be present for "marketplace" flows. Enforce this with a Zod superRefine and pass installationId to repository methods only when configurationId is defined (omit the field otherwise).

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
📚 Learning: 2026-03-22T13:45:36.346Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/navigation/SideMenu.tsx:460-489
Timestamp: 2026-03-22T13:45:36.346Z
Learning: In triggerdotdev/trigger.dev, sidebar navigation items (SideMenu.tsx) are intentionally NOT gated behind feature-flag or permission checks at the nav level. Authorization is enforced at the route/loader level instead. Hiding nav items based on access checks is considered confusing UX. This applies to items like "AI Metrics" (v3BuiltInDashboardPath) and other dashboard links — they are always rendered in the sidebar regardless of hasQueryAccess or similar flags.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
📚 Learning: 2025-11-27T16:26:37.432Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T16:26:37.432Z
Learning: Applies to internal-packages/database/**/*.{ts,tsx} : Use Prisma for database interactions in internal-packages/database with PostgreSQL

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
📚 Learning: 2026-02-03T18:27:40.429Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:40.429Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (e.g., Edit with PencilSquareIcon) in the TableCellMenu are intentionally icon-only with no text labels as a compact UI pattern. This is a deliberate design choice for this route; preserve the icon-only behavior for consistency in this file.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
📚 Learning: 2026-02-11T16:37:32.429Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3019
File: apps/webapp/app/components/primitives/charts/Card.tsx:26-30
Timestamp: 2026-02-11T16:37:32.429Z
Learning: In projects using react-grid-layout, avoid relying on drag-handle class to imply draggability. Ensure drag-handle elements only affect dragging when the parent grid item is configured draggable in the layout; conditionally apply cursor styles based on the draggable prop. This improves correctness and accessibility.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
📚 Learning: 2026-03-22T13:26:12.060Z
Learnt from: ericallam
Repo: triggerdotdev/trigger.dev PR: 3244
File: apps/webapp/app/components/code/TextEditor.tsx:81-86
Timestamp: 2026-03-22T13:26:12.060Z
Learning: In the triggerdotdev/trigger.dev codebase, do not flag `navigator.clipboard.writeText(...)` calls for `missing-await`/`unhandled-promise` issues. These clipboard writes are intentionally invoked without `await` and without `catch` handlers across the project; keep that behavior consistent when reviewing TypeScript/TSX files (e.g., usages like in `apps/webapp/app/components/code/TextEditor.tsx`).

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
📚 Learning: 2026-03-22T19:24:14.403Z
Learnt from: matt-aitken
Repo: triggerdotdev/trigger.dev PR: 3187
File: apps/webapp/app/v3/services/alerts/deliverErrorGroupAlert.server.ts:200-204
Timestamp: 2026-03-22T19:24:14.403Z
Learning: In the triggerdotdev/trigger.dev codebase, webhook URLs are not expected to contain embedded credentials/secrets (e.g., fields like `ProjectAlertWebhookProperties` should only hold credential-free webhook endpoints). During code review, if you see logging or inclusion of raw webhook URLs in error messages, do not automatically treat it as a credential-leak/secrets-in-logs issue by default—first verify the URL does not contain embedded credentials (for example, no username/password in the URL, no obvious secret/token query params or fragments). If the URL is credential-free per this project’s conventions, allow the logging.

Applied to files:

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx
  • apps/webapp/app/v3/canAccessPrivateConnections.server.ts
  • apps/webapp/app/v3/featureFlags.server.ts
📚 Learning: 2026-02-03T18:27:49.039Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 2994
File: apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx:553-555
Timestamp: 2026-02-03T18:27:49.039Z
Learning: In apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx, the menu buttons (like the Edit button with PencilSquareIcon) intentionally have no text labels - only icons are shown in the TableCellMenu. This is a deliberate UI design pattern for compact icon-only menu items.

Applied to files:

  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
📚 Learning: 2026-03-25T15:29:25.853Z
Learnt from: CR
Repo: triggerdotdev/trigger.dev PR: 0
File: .cursor/rules/writing-tasks.mdc:0-0
Timestamp: 2026-03-25T15:29:25.853Z
Learning: Use `trigger.dev/react-hooks` for React components that need to display real-time task status and results

Applied to files:

  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
📚 Learning: 2026-03-26T10:02:22.373Z
Learnt from: 0ski
Repo: triggerdotdev/trigger.dev PR: 3254
File: apps/webapp/app/services/platformNotifications.server.ts:363-385
Timestamp: 2026-03-26T10:02:22.373Z
Learning: In `triggerdotdev/trigger.dev`, the `getNextCliNotification` fallback in `apps/webapp/app/services/platformNotifications.server.ts` intentionally uses `prisma.orgMember.findFirst` (single org) when no `projectRef` is provided. This is acceptable for v1 because the CLI (`dev` and `login` commands) always passes `projectRef` in normal usage, making the fallback a rare edge case. Do not flag the single-org fallback as a multi-org correctness bug in this file.

Applied to files:

  • apps/webapp/app/presenters/OrganizationsPresenter.server.ts
🔇 Additional comments (5)
apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections._index/route.tsx (1)

16-49: Good move to org-scoped access checks in the loader.

Line 47 correctly switches from request-host feature detection to org/user-aware canAccessPrivateConnections, which aligns with org-level flag rollout.

apps/webapp/app/presenters/OrganizationsPresenter.server.ts (1)

13-14: LGTM: env-based global default is wired correctly.

Line 159-163 cleanly injects hasPrivateConnections default from env.PRIVATE_CONNECTIONS_ENABLED, while preserving org override precedence later in the merge.

Also applies to: 158-163

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.private-connections.new/route.tsx (1)

27-28: Loader access check update looks good.

Line 59 now uses org/user-aware feature access and is consistent with the index route guard pattern.

Also applies to: 59-62

apps/webapp/app/v3/featureFlags.server.ts (2)

12-24: hasPrivateConnections catalog wiring looks correct.

The key + zod schema addition is consistent with the existing flag catalog pattern.


52-68: Nice fix for falsy override and null-row fallback behavior.

Using !== undefined at Line 52 ensures explicit false overrides are respected, and Line 68 cleanly falls back to defaultValue.

Comment on lines +12 to 13
import { useFeatureFlags } from "~/hooks/useFeatureFlags";
import { useFeatures } from "~/hooks/useFeatures";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t gate this sidebar item by feature flag at nav level.

This hides the menu entry and diverges from the project’s established pattern where sidebar items are always shown and authorization is enforced in route loaders.

🔧 Suggested fix
-import { useFeatureFlags } from "~/hooks/useFeatureFlags";
 import { useFeatures } from "~/hooks/useFeatures";
@@
   const { isManagedCloud } = useFeatures();
-  const featureFlags = useFeatureFlags();
@@
-          {featureFlags.hasPrivateConnections && (
-            <SideMenuItem
-              name="Private Connections"
-              icon={LockClosedIcon}
-              activeIconColor="text-purple-500"
-              to={v3PrivateConnectionsPath(organization)}
-              data-action="private-connections"
-            />
-          )}
+          <SideMenuItem
+            name="Private Connections"
+            icon={LockClosedIcon}
+            activeIconColor="text-purple-500"
+            to={v3PrivateConnectionsPath(organization)}
+            data-action="private-connections"
+          />
Based on learnings: In triggerdotdev/trigger.dev, sidebar navigation items are intentionally NOT gated behind feature-flag or permission checks at the nav level; authorization is enforced at the route/loader level.

Also applies to: 52-53, 110-118

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx`
around lines 12 - 13, The OrganizationSettingsSideMenu is currently hiding
sidebar items using feature-flag hooks (useFeatureFlags, useFeatures) at the nav
level; remove that gating so menu entries are always rendered and let
route/loader authorization handle access instead: update the
OrganizationSettingsSideMenu component to stop conditionally rendering the menu
item(s) based on useFeatureFlags/useFeatures (including the blocks around the
link(s) referenced in the component where features are checked) so the links are
always present, but keep any existing route-level guards intact.

Comment on lines +47 to 49
const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
if (!canAccess) {
return redirect(organizationPath({ slug: organizationSlug }));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard the action with the same canAccessPrivateConnections check.

Line 47 gates only the loader. The delete action (Line 71 onward) is still callable directly and can mutate state without feature access, which weakens enforcement of the org-level flag.

🔧 Suggested fix
 export const action = async ({ request, params }: ActionFunctionArgs) => {
   const userId = await requireUserId(request);
   const { organizationSlug } = OrganizationParamsSchema.parse(params);
+  const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
+
+  if (!canAccess) {
+    return redirect(organizationPath({ slug: organizationSlug }));
+  }
 
   if (request.method !== "DELETE" && request.method !== "POST") {
     return json({ error: "Method not allowed" }, { status: 405 });
   }

Also applies to: 71-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.settings.private-connections._index/route.tsx
around lines 47 - 49, The delete action is not currently protected by the same
org-level check as the loader; call canAccessPrivateConnections({
organizationSlug, userId }) at the start of the action handler (the export const
action or the function handling the delete flow around the deletion logic
starting at line ~71) and, if it returns false, short-circuit by returning a
redirect to organizationPath({ slug: organizationSlug }) (or an appropriate
unauthorized response) before performing any deletion or state mutation; ensure
you reference the same organizationSlug and userId used in the loader so the
guard logic is identical.

Comment on lines +59 to 62
const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
if (!canAccess) {
return redirect(organizationPath({ slug: organizationSlug }));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Apply the same access gate in action to prevent direct POST bypass.

The loader is protected, but create mutation in Line 95+ is still reachable via direct form POST without feature access validation.

🔧 Suggested fix
 export const action: ActionFunction = async ({ request, params }) => {
   const userId = await requireUserId(request);
   const { organizationSlug } = OrganizationParamsSchema.parse(params);
+  const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
+
+  if (!canAccess) {
+    return redirect(organizationPath({ slug: organizationSlug }));
+  }
 
   const formData = await request.formData();
   const submission = parse(formData, { schema });

Also applies to: 95-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.settings.private-connections.new/route.tsx
around lines 59 - 62, The POST/action handler must enforce the same access gate
as the loader: call canAccessPrivateConnections({ organizationSlug, userId }) at
the start of the action (before performing the create mutation), and if it
returns false perform the same redirect to organizationPath({ slug:
organizationSlug }) (or return an appropriate unauthorized response) to prevent
direct form POST bypass; update the action function in this file to reuse the
canAccessPrivateConnections check and short-circuit before running the mutation
logic that currently lives after line 95.

Comment on lines +11 to +26
const org = await prisma.organization.findFirst({
where: {
slug: organizationSlug,
members: { some: { userId } },
},
select: {
featureFlags: true,
},
});

const flag = makeFlag();
return flag({
key: FEATURE_FLAG.hasPrivateConnections,
defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1",
overrides: (org?.featureFlags as Record<string, unknown>) ?? {},
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Return false when org lookup fails before evaluating the flag.

As written, this can return true from env default even when the user is not a member or the org slug doesn’t exist. That makes the helper semantically unsafe as an access primitive.

🔧 Suggested fix
   const org = await prisma.organization.findFirst({
@@
   });
 
+  if (!org) {
+    return false;
+  }
+
   const flag = makeFlag();
   return flag({
     key: FEATURE_FLAG.hasPrivateConnections,
     defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1",
-    overrides: (org?.featureFlags as Record<string, unknown>) ?? {},
+    overrides: (org.featureFlags as Record<string, unknown>) ?? {},
   });
📝 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 org = await prisma.organization.findFirst({
where: {
slug: organizationSlug,
members: { some: { userId } },
},
select: {
featureFlags: true,
},
});
const flag = makeFlag();
return flag({
key: FEATURE_FLAG.hasPrivateConnections,
defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1",
overrides: (org?.featureFlags as Record<string, unknown>) ?? {},
});
const org = await prisma.organization.findFirst({
where: {
slug: organizationSlug,
members: { some: { userId } },
},
select: {
featureFlags: true,
},
});
if (!org) {
return false;
}
const flag = makeFlag();
return flag({
key: FEATURE_FLAG.hasPrivateConnections,
defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1",
overrides: (org.featureFlags as Record<string, unknown>) ?? {},
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/canAccessPrivateConnections.server.ts` around lines 11 -
26, The org lookup can be null which should short-circuit access; change the
flow in the code that calls prisma.organization.findFirst (variable org) so that
if org is falsy you immediately return false instead of evaluating the feature
flag; keep using makeFlag and FEATURE_FLAG.hasPrivateConnections only when org
exists and pass overrides from org.featureFlags, but do not fall back to
env.PRIVATE_CONNECTIONS_ENABLED for non-members or missing orgs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants