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

Self-hosted instances can now serve deployed runs' API traffic from a different origin than the public one, per organization, via the `INTERNAL_API_ORIGIN` environment variable and a feature flag.
5 changes: 5 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ 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(),
// Global default for internalApiOriginEnabled when an org hasn't set it.
INTERNAL_API_ORIGIN_ENABLED: z.string().default("0"),
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
3 changes: 3 additions & 0 deletions apps/webapp/app/routes/api.v1.projects.$projectRef.envvars.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type LoaderFunctionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { resolveVariablesForEnvironment } from "~/v3/environmentVariables/environmentVariablesRepository.server";

Expand Down Expand Up @@ -44,6 +45,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
},
include: {
parentEnvironment: true,
// Feeds resolveProdApiOrigin; only loaded when internal-origin routing is possible.
...(env.INTERNAL_API_ORIGIN ? { organization: { select: { featureFlags: true } } } : {}),
},
});

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 @@ -934,7 +936,7 @@ export type RuntimeEnvironmentForEnvRepo = Pick<
| "organizationId"
| "branchName"
| "builtInEnvironmentVariableOverrides"
>;
> & { organization?: { featureFlags: unknown } | null };

export const environmentVariablesRepository = new EnvironmentVariablesRepository();

Expand Down Expand Up @@ -1146,20 +1148,47 @@ 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; INTERNAL_API_ORIGIN_ENABLED is the global default applied
// only when the org has not set it), they get the internal origin instead. The
// global default is the cached DB flag with INTERNAL_API_ORIGIN_ENABLED as the
// fallback; org flags are read in-memory, so a flip applies on the next attempt.
function resolveProdApiOrigin(runtimeEnvironment: RuntimeEnvironmentForEnvRepo): string {
const publicOrigin = env.API_ORIGIN ?? env.APP_ORIGIN;

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

const enabled = resolveInternalApiOriginEnabled({
orgFeatureFlags: runtimeEnvironment.organization?.featureFlags,
globalDefault:
globalFlagsRegistry.current()?.[FEATURE_FLAG.internalApiOriginEnabled] ??
env.INTERNAL_API_ORIGIN_ENABLED === "1",
});
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

return enabled ? env.INTERNAL_API_ORIGIN : publicOrigin;
}

async function resolveBuiltInProdVariables(
runtimeEnvironment: RuntimeEnvironmentForEnvRepo,
parentEnvironment?: RuntimeEnvironmentForEnvRepo
) {
const apiOrigin = 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
36 changes: 36 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,12 @@ 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. Per-org, with
// INTERNAL_API_ORIGIN_ENABLED as the global default (org wins). No-op unless
// INTERNAL_API_ORIGIN is set.
// Strict z.boolean(): coercion turns the string "false" into true, which

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I can see you aren't using Eric'c comment killer plugin 😝

// 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 +111,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
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,8 @@ export class AuthenticatedWorkerInstance extends WithRunEngine {
},
include: {
parentEnvironment: true,
// Feeds resolveProdApiOrigin; only loaded when internal-origin routing is possible.
...(env.INTERNAL_API_ORIGIN ? { organization: { select: { featureFlags: true } } } : {}),
},
});

Expand Down
73 changes: 73 additions & 0 deletions apps/webapp/test/internalApiOrigin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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);

// The fallback must follow the global default, not hardcode false.
expect(
resolveInternalApiOriginEnabled({
orgFeatureFlags: { internalApiOriginEnabled: "false" },
globalDefault: true,
})
).toBe(true);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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