Skip to content

Commit 52b7ade

Browse files
committed
feat(webapp): let the deployment S2 endpoint be overridden
The S2 SDK honours only endpoints passed to its constructor. S2Environment.parse(), which reads the endpoint variables, is an opt-in helper the webapp never called, so deployment event logs always went to hosted S2 and could not be pointed at the local s2 container in docker compose. Realtime streams already had this knob. S2_DEPLOYMENT_ENDPOINT is a single value covering both the account and basin hosts. Two separate variables would let a half-set config send the access token to the hosted service while the operator believed the client was entirely local. Unset means the SDK hosted defaults, so nothing changes for existing deployments.
1 parent fe94700 commit 52b7ade

5 files changed

Lines changed: 79 additions & 4 deletions

File tree

apps/webapp/app/env.server.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ const S2EnvSchema = z.preprocess(
7979
S2_ACCESS_TOKEN: z.string(),
8080
S2_DEPLOYMENT_LOGS_BASIN_NAME: z.string(),
8181
S2_DEPLOYMENT_STREAMS_LOCAL: z.string().default("0"),
82+
// Points deployment event logs at an S2 service other than the hosted one, e.g. the
83+
// local s2-lite in docker compose. One value covers both the account and basin
84+
// endpoints: splitting them lets a half-set config send the access token to the
85+
// hosted service while the operator believes they are entirely local.
86+
S2_DEPLOYMENT_ENDPOINT: z
87+
.string()
88+
.url()
89+
.optional()
90+
.or(z.literal("").transform(() => undefined)),
8291
}),
8392
z.object({
8493
S2_ENABLED: z.literal("0"),

apps/webapp/app/presenters/v3/DeploymentPresenter.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { type User } from "~/models/user.server";
1313
import { getUsername } from "~/utils/username";
1414
import { processGitMetadata } from "./BranchesPresenter.server";
1515
import { VercelProjectIntegrationDataSchema } from "~/v3/vercel/vercelProjectIntegrationSchema";
16-
import { S2 } from "@s2-dev/streamstore";
16+
import { createDeploymentS2Client } from "~/v3/s2Client.server";
1717
import { env } from "~/env.server";
1818
import { createRedisClient } from "~/redis.server";
1919
import { tryCatch } from "@trigger.dev/core";
@@ -30,7 +30,7 @@ const s2TokenRedis = createRedisClient("s2-token-cache", {
3030
clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1",
3131
});
3232

33-
const s2 = env.S2_ENABLED === "1" ? new S2({ accessToken: env.S2_ACCESS_TOKEN }) : undefined;
33+
const s2 = createDeploymentS2Client();
3434

3535
export type ErrorData = {
3636
name: string;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { S2 } from "@s2-dev/streamstore";
2+
import { env } from "~/env.server";
3+
4+
export type DeploymentS2Config = {
5+
accessToken: string;
6+
endpoint?: string;
7+
};
8+
9+
// One endpoint drives both the account and basin hosts. The SDK honours only endpoints passed
10+
// here (`S2Environment.parse()` is opt-in and we never call it), and overriding just one of the
11+
// two would send the access token to the hosted service while the other half went elsewhere.
12+
export function buildDeploymentS2Client({ accessToken, endpoint }: DeploymentS2Config): S2 {
13+
return new S2({
14+
accessToken,
15+
endpoints: { account: endpoint, basin: endpoint },
16+
});
17+
}
18+
19+
export function createDeploymentS2Client(): S2 | undefined {
20+
if (env.S2_ENABLED !== "1") {
21+
return undefined;
22+
}
23+
24+
return buildDeploymentS2Client({
25+
accessToken: env.S2_ACCESS_TOKEN,
26+
endpoint: env.S2_DEPLOYMENT_ENDPOINT,
27+
});
28+
}

apps/webapp/app/v3/services/deployment.server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
import { FEATURE_FLAG, type FeatureFlagKey } from "../featureFlags";
2424
import { flags } from "../featureFlags.server";
2525
import { globalFlagsRegistry } from "../globalFlagsRegistry.server";
26-
import { AppendInput, AppendRecord, S2 } from "@s2-dev/streamstore";
26+
import { AppendInput, AppendRecord } from "@s2-dev/streamstore";
27+
import { createDeploymentS2Client } from "~/v3/s2Client.server";
2728
import { createRedisClient } from "~/redis.server";
2829

2930
const S2_TOKEN_KEY_PREFIX = "s2-token:read:deployment-event-stream:project:";
@@ -35,7 +36,7 @@ const s2TokenRedis = createRedisClient("s2-token-cache", {
3536
tlsDisabled: env.CACHE_REDIS_TLS_DISABLED === "true",
3637
clusterMode: env.CACHE_REDIS_CLUSTER_MODE_ENABLED === "1",
3738
});
38-
const s2 = env.S2_ENABLED === "1" ? new S2({ accessToken: env.S2_ACCESS_TOKEN }) : undefined;
39+
const s2 = createDeploymentS2Client();
3940

4041
const DEPLOY_BUILD_PATH_ENV_FLAG: Partial<Record<RuntimeEnvironmentType, FeatureFlagKey>> = {
4142
PREVIEW: FEATURE_FLAG.deployBuildPathPreview,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, expect, it } from "vitest";
2+
import { buildDeploymentS2Client } from "~/v3/s2Client.server";
3+
4+
const BASIN = "trigger-local";
5+
6+
describe("buildDeploymentS2Client", () => {
7+
it("uses the SDK's hosted defaults when no endpoint is configured", () => {
8+
const client = buildDeploymentS2Client({ accessToken: "token" });
9+
10+
expect(client.endpoints.accountBaseUrl()).toBe("https://a.s2.dev/v1");
11+
expect(client.endpoints.basinBaseUrl(BASIN)).toBe(`https://${BASIN}.b.s2.dev/v1`);
12+
expect(client.endpoints.includeBasinHeader).toBe(false);
13+
});
14+
15+
it("points both the account and basin hosts at a configured endpoint", () => {
16+
const client = buildDeploymentS2Client({
17+
accessToken: "token",
18+
endpoint: "http://localhost:4566",
19+
});
20+
21+
expect(client.endpoints.accountBaseUrl()).toBe("http://localhost:4566/v1");
22+
expect(client.endpoints.basinBaseUrl(BASIN)).toBe("http://localhost:4566/v1");
23+
expect(client.endpoints.includeBasinHeader).toBe(true);
24+
});
25+
26+
// A split configuration would send the access token to the hosted service while the operator
27+
// believed the client was entirely local, so one value has to drive both hosts.
28+
it("never leaves one host hosted while the other is overridden", () => {
29+
const client = buildDeploymentS2Client({
30+
accessToken: "token",
31+
endpoint: "http://localhost:4566",
32+
});
33+
34+
expect(client.endpoints.accountBaseUrl()).not.toContain("s2.dev");
35+
expect(client.endpoints.basinBaseUrl(BASIN)).not.toContain("s2.dev");
36+
});
37+
});

0 commit comments

Comments
 (0)