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
33 changes: 33 additions & 0 deletions src/cli/lib/localDeployment/run.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { afterEach, describe, expect, it } from "vitest";
import type { Context } from "../../../bundler/context.js";
import { localBackendInterface } from "./run.js";

function fakeCtx(): Context {
return {
crash: async ({ printedMessage }: any): Promise<never> => {
throw new Error(`crashed: ${printedMessage}`);
},
} as unknown as Context;
}

describe("localBackendInterface", () => {
afterEach(() => {
delete process.env.CONVEX_LOCAL_BACKEND_INTERFACE;
});

it("defaults to loopback so local backends are not reachable from the network (convex/convex-js#200)", async () => {
await expect(localBackendInterface(fakeCtx())).resolves.toBe("127.0.0.1");
});

it("honors an explicit interface for machines that deliberately serve a local deployment", async () => {
process.env.CONVEX_LOCAL_BACKEND_INTERFACE = "0.0.0.0";
await expect(localBackendInterface(fakeCtx())).resolves.toBe("0.0.0.0");
});

it("rejects a blank interface instead of silently binding to every interface", async () => {
process.env.CONVEX_LOCAL_BACKEND_INTERFACE = " ";
await expect(localBackendInterface(fakeCtx())).rejects.toThrow(
"Invalid CONVEX_LOCAL_BACKEND_INTERFACE",
);
});
});
25 changes: 25 additions & 0 deletions src/cli/lib/localDeployment/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { DeploymentType, DetailedDeploymentCredentials } from "../api.js";
import { productionProvisionHost, provisionHost } from "../config.js";

const DEFAULT_STARTUP_TIMEOUT_SECS = 30;
const DEFAULT_LOCAL_BACKEND_INTERFACE = "127.0.0.1";
const FIRST_CONTROL_PLANE_AUTH_BACKEND_VERSION =
"precompiled-2026-09-15-4e9b83a";

Expand Down Expand Up @@ -52,6 +53,27 @@ async function parseStartupTimeoutSecs(ctx: Context): Promise<number> {
return parsed;
}

/**
* Which host interface the local backend binds to. Defaults to loopback so a
* local deployment is not reachable from the network; override with
* CONVEX_LOCAL_BACKEND_INTERFACE for machines that deliberately serve a
* local deployment to other devices (convex/convex-js#200).
*/
export async function localBackendInterface(ctx: Context): Promise<string> {
const raw = process.env.CONVEX_LOCAL_BACKEND_INTERFACE;
if (raw === undefined) {
return DEFAULT_LOCAL_BACKEND_INTERFACE;
}
if (raw.trim() === "") {
return await ctx.crash({
exitCode: 1,
errorType: "fatal",
printedMessage: `Invalid CONVEX_LOCAL_BACKEND_INTERFACE=${JSON.stringify(raw)}: expected a host interface such as "127.0.0.1".`,
});
}
return raw;
}

export async function runLocalBackend(
ctx: Context,
args: {
Expand Down Expand Up @@ -99,6 +121,9 @@ export async function runLocalBackend(
if (args.isLatestVersion) {
// CLI args that were added in later versions of backend go here instead of above
// since the CLI may run older versions of backend (e.g. when upgrading).
// Bind to loopback by default so local deployments are not reachable from
// the network (convex/convex-js#200).
commandArgs.push("--interface", await localBackendInterface(ctx));
if (args.deploymentKind === "anonymous") {
const uuid = loadUuidForAnonymousUser(ctx);
if (uuid !== null) {
Expand Down