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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/internal-api-origin-flag.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Operators can now route deployed runs' API traffic through an alternative origin on a per-organization basis, enabling gradual network migrations without redeploying tasks.
3 changes: 3 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ const EnvironmentSchema = z
LOGIN_RATE_LIMITS_ENABLED: BoolEnv.default(true),
APP_ORIGIN: z.string().default("http://localhost:3030"),
API_ORIGIN: z.string().optional(),
// Alternative API origin for deployed runs whose org has the
// internalApiOriginEnabled feature flag on. Unset = flag is a no-op.
INTERNAL_API_ORIGIN: z.string().optional(),
STREAM_ORIGIN: z.string().optional(),
ELECTRIC_ORIGIN: z.string().default("http://localhost:3060"),
// A comma separated list of electric origins to shard into different electric instances by environmentId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { env } from "~/env.server";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { deduplicateVariableArray } from "../deduplicateVariableArray.server";
import { removeBlacklistedVariables } from "../environmentVariableRules.server";
import { FEATURE_FLAG, resolveInternalApiOriginEnabled } from "../featureFlags";
import { globalFlagsRegistry } from "../globalFlagsRegistry.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import {
type CreateEnvironmentVariables,
Expand Down Expand Up @@ -1146,20 +1148,52 @@ async function resolveOverridableOtelDevVariables(
return result;
}

// Deployed runs normally get the public API origin. When INTERNAL_API_ORIGIN is
// set and the org's internalApiOriginEnabled flag resolves on (org override wins
// in both directions; the global default applies only when the org has not set
// it), they get the internal origin instead. Resolved at attempt start, so flag
// changes take effect on the next attempt without redeploys.
async function resolveProdApiOrigin(
runtimeEnvironment: RuntimeEnvironmentForEnvRepo
): Promise<string> {
const publicOrigin = env.API_ORIGIN ?? env.APP_ORIGIN;

if (!env.INTERNAL_API_ORIGIN) {
return publicOrigin;
}

const organization = await prisma.organization.findFirst({
where: { id: runtimeEnvironment.organizationId },
select: { featureFlags: true },
});

const enabled = resolveInternalApiOriginEnabled({
orgFeatureFlags: organization?.featureFlags,
// Cached global snapshot; a cold read fails safe to the public origin.
globalDefault: globalFlagsRegistry.current()?.[FEATURE_FLAG.internalApiOriginEnabled] ?? false,
});

return enabled ? env.INTERNAL_API_ORIGIN : publicOrigin;
}

async function resolveBuiltInProdVariables(
runtimeEnvironment: RuntimeEnvironmentForEnvRepo,
parentEnvironment?: RuntimeEnvironmentForEnvRepo
) {
const apiOrigin = await resolveProdApiOrigin(runtimeEnvironment);

let result: Array<EnvironmentVariable> = [
{
key: "TRIGGER_SECRET_KEY",
value: parentEnvironment?.apiKey ?? runtimeEnvironment.apiKey,
},
{
key: "TRIGGER_API_URL",
value: env.API_ORIGIN ?? env.APP_ORIGIN,
value: apiOrigin,
},
{
// Deliberately not switched by internalApiOriginEnabled: streams are
// long-lived connections served on their own path.
key: "TRIGGER_STREAM_URL",
value: env.STREAM_ORIGIN ?? env.API_ORIGIN ?? env.APP_ORIGIN,
},
Expand Down
35 changes: 35 additions & 0 deletions apps/webapp/app/v3/featureFlags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const FEATURE_FLAG = {
hasSso: "hasSso",
mollifierEnabled: "mollifierEnabled",
workerQueueScheduledSplitEnabled: "workerQueueScheduledSplitEnabled",
internalApiOriginEnabled: "internalApiOriginEnabled",
realtimeBackend: "realtimeBackend",
computeMigrationEnabled: "computeMigrationEnabled",
computeMigrationFreePercentage: "computeMigrationFreePercentage",
Expand All @@ -38,6 +39,11 @@ export const FeatureFlagCatalog = {
[FEATURE_FLAG.hasSso]: z.coerce.boolean(),
[FEATURE_FLAG.mollifierEnabled]: z.coerce.boolean(),
[FEATURE_FLAG.workerQueueScheduledSplitEnabled]: z.coerce.boolean(),
// Routes deployed runs' TRIGGER_API_URL to INTERNAL_API_ORIGIN. Controllable
// globally and per-org (org wins). No-op unless INTERNAL_API_ORIGIN is set.
// Strict z.boolean(): coercion turns the string "false" into true, which
// would silently enable the wrong orgs if written as a string.
[FEATURE_FLAG.internalApiOriginEnabled]: z.boolean(),
// Which backend serves the realtime run feed. Controllable
// globally and per-org (org wins). Defaults to "electric" when unset.
// "shadow" serves Electric but diffs the native path in the background.
Expand Down Expand Up @@ -104,6 +110,35 @@ export function validatePartialFeatureFlags(values: Record<string, unknown>) {
}

// Utility types for catalog-driven UI rendering
/**
* Resolve whether deployed runs should use the internal API origin, from the
* org's feature-flags JSON. Precedence: a per-org override wins in BOTH
* directions; the global default applies only when the org has not set the
* flag (or set it to something invalid).
*/
export function resolveInternalApiOriginEnabled({
orgFeatureFlags,
globalDefault,
}: {
orgFeatureFlags: unknown;
globalDefault: boolean;
}): boolean {
const override =
orgFeatureFlags && typeof orgFeatureFlags === "object" && !Array.isArray(orgFeatureFlags)
? (orgFeatureFlags as Record<string, unknown>)[FEATURE_FLAG.internalApiOriginEnabled]
: undefined;

if (override !== undefined) {
const parsed = FeatureFlagCatalog[FEATURE_FLAG.internalApiOriginEnabled].safeParse(override);

if (parsed.success) {
return parsed.data;
}
}

return globalDefault;
}

export type FlagControlType =
| { type: "boolean" }
| { type: "enum"; options: string[] }
Expand Down
65 changes: 65 additions & 0 deletions apps/webapp/test/internalApiOrigin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from "vitest";
import { resolveInternalApiOriginEnabled } from "~/v3/featureFlags";

describe("resolveInternalApiOriginEnabled", () => {
it("returns the global default when the org has no flags", () => {
expect(resolveInternalApiOriginEnabled({ orgFeatureFlags: null, globalDefault: false })).toBe(
false
);
expect(resolveInternalApiOriginEnabled({ orgFeatureFlags: null, globalDefault: true })).toBe(
true
);
expect(resolveInternalApiOriginEnabled({ orgFeatureFlags: {}, globalDefault: true })).toBe(
true
);
});

it("lets an org override win in both directions", () => {
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: true },
globalDefault: false,
})
).toBe(true);

expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: false },
globalDefault: true,
})
).toBe(false);
});

it("ignores invalid overrides and falls back to the global default", () => {
// Strict z.boolean(): the string "false" must not coerce to an enable.
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: "false" },
globalDefault: false,
})
).toBe(false);

expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: "true" },
globalDefault: false,
})
).toBe(false);

expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: 1 },
globalDefault: false,
})
).toBe(false);
});
Comment on lines +33 to +55

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Cover invalid overrides with an enabled global default.

All invalid cases use globalDefault: false, so they would pass if invalid values were incorrectly hardcoded to false. Assert an invalid override falls back to true when the global flag is enabled.

Proposed test
+    expect(
+      resolveInternalApiOriginEnabled({
+        orgFeatureFlags: { internalApiOriginEnabled: "false" },
+        globalDefault: true,
+      })
+    ).toBe(true);
📝 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
it("ignores invalid overrides and falls back to the global default", () => {
// Strict z.boolean(): the string "false" must not coerce to an enable.
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: "false" },
globalDefault: false,
})
).toBe(false);
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: "true" },
globalDefault: false,
})
).toBe(false);
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: 1 },
globalDefault: false,
})
).toBe(false);
});
it("ignores invalid overrides and falls back to the global default", () => {
// Strict z.boolean(): the string "false" must not coerce to an enable.
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: "false" },
globalDefault: false,
})
).toBe(false);
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: "false" },
globalDefault: true,
})
).toBe(true);
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: "true" },
globalDefault: false,
})
).toBe(false);
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: 1 },
globalDefault: false,
})
).toBe(false);
});


it("ignores non-object flag containers", () => {
expect(resolveInternalApiOriginEnabled({ orgFeatureFlags: [], globalDefault: true })).toBe(
true
);
expect(resolveInternalApiOriginEnabled({ orgFeatureFlags: "junk", globalDefault: false })).toBe(
false
);
});
});
Loading