diff --git a/docs/deploy/environment-variables.md b/docs/deploy/environment-variables.md index 5ebef728..b5b6486d 100644 --- a/docs/deploy/environment-variables.md +++ b/docs/deploy/environment-variables.md @@ -18,6 +18,7 @@ All environment variables that Paperclip uses for server configuration. | `PAPERCLIP_INSTANCE_ID` | `default` | Instance identifier (for multiple local instances) | | `PAPERCLIP_DEPLOYMENT_MODE` | `local_trusted` | Runtime mode override | | `PAPERCLIP_DEPLOYMENT_EXPOSURE` | `private` | Exposure policy when deployment mode is `authenticated` | +| `PAPERCLIP_API_URL` | (auto-derived) | Paperclip API base URL. When set externally (e.g., via Kubernetes ConfigMap, load balancer, or reverse proxy), the server preserves the value instead of deriving it from the listen host and port. Useful for deployments where the public-facing URL differs from the local bind address. | ## Secrets @@ -35,7 +36,7 @@ These are set automatically by the server when invoking agents: |----------|-------------| | `PAPERCLIP_AGENT_ID` | Agent's unique ID | | `PAPERCLIP_COMPANY_ID` | Company ID | -| `PAPERCLIP_API_URL` | Paperclip API base URL | +| `PAPERCLIP_API_URL` | Paperclip API base URL (inherits the server-level value; see Server Configuration above) | | `PAPERCLIP_API_KEY` | Short-lived JWT for API auth | | `PAPERCLIP_RUN_ID` | Current heartbeat run ID | | `PAPERCLIP_TASK_ID` | Issue that triggered this wake | diff --git a/server/src/__tests__/server-startup-feedback-export.test.ts b/server/src/__tests__/server-startup-feedback-export.test.ts index f8589544..c950aa6e 100644 --- a/server/src/__tests__/server-startup-feedback-export.test.ts +++ b/server/src/__tests__/server-startup-feedback-export.test.ts @@ -180,3 +180,27 @@ describe("startServer feedback export wiring", () => { }); }); }); + +describe("startServer PAPERCLIP_API_URL handling", () => { + beforeEach(() => { + vi.clearAllMocks(); + process.env.BETTER_AUTH_SECRET = "test-secret"; + delete process.env.PAPERCLIP_API_URL; + }); + + it("uses the externally set PAPERCLIP_API_URL when provided", async () => { + process.env.PAPERCLIP_API_URL = "http://custom-api:3100"; + + const started = await startServer(); + + expect(started.apiUrl).toBe("http://custom-api:3100"); + expect(process.env.PAPERCLIP_API_URL).toBe("http://custom-api:3100"); + }); + + it("falls back to host-based URL when PAPERCLIP_API_URL is not set", async () => { + const started = await startServer(); + + expect(started.apiUrl).toBe("http://127.0.0.1:3210"); + expect(process.env.PAPERCLIP_API_URL).toBe("http://127.0.0.1:3210"); + }); +}); diff --git a/server/src/index.ts b/server/src/index.ts index 74199e36..eb6ad4e4 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -554,7 +554,9 @@ export async function startServer(): Promise { : runtimeListenHost; process.env.PAPERCLIP_LISTEN_HOST = runtimeListenHost; process.env.PAPERCLIP_LISTEN_PORT = String(listenPort); - process.env.PAPERCLIP_API_URL = `http://${runtimeApiHost}:${listenPort}`; + if (!process.env.PAPERCLIP_API_URL) { + process.env.PAPERCLIP_API_URL = `http://${runtimeApiHost}:${listenPort}`; + } setupLiveEventsWebSocketServer(server, db as any, { deploymentMode: config.deploymentMode, @@ -792,7 +794,7 @@ export async function startServer(): Promise { server, host: config.host, listenPort, - apiUrl: process.env.PAPERCLIP_API_URL ?? `http://${runtimeApiHost}:${listenPort}`, + apiUrl: process.env.PAPERCLIP_API_URL!, databaseUrl: activeDatabaseConnectionString, }; }