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
1 change: 1 addition & 0 deletions docs/deploy/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ All environment variables that Paperclip uses for server configuration.
| `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. |
| `PAPERCLIP_RUNTIME_API_URL` | (auto-derived) | Internal Paperclip API base URL for managed runtime bridges and agent processes. An explicit value takes precedence over the auto-derived URL. Set this when the server can reach a different URL than users or remote agents. For example, a container can use `http://127.0.0.1:3100` internally while `PAPERCLIP_API_URL` uses a public reverse-proxy URL. |

## Secrets

Expand Down
15 changes: 15 additions & 0 deletions server/src/__tests__/server-startup-feedback-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ describe("startServer PAPERCLIP_API_URL handling", () => {
loadConfigMock.mockReturnValue(buildTestConfig());
process.env.BETTER_AUTH_SECRET = "test-secret";
delete process.env.PAPERCLIP_API_URL;
delete process.env.PAPERCLIP_RUNTIME_API_URL;
});

afterEach(() => {
Expand Down Expand Up @@ -671,6 +672,20 @@ describe("startServer PAPERCLIP_API_URL handling", () => {
expect(JSON.parse(process.env.PAPERCLIP_RUNTIME_API_CANDIDATES_JSON ?? "[]")[0]).toBe("http://custom-api:3100");
});

it("preserves an explicit PAPERCLIP_RUNTIME_API_URL independently of the public API URL", async () => {
process.env.PAPERCLIP_API_URL = "https://paperclip.example";
process.env.PAPERCLIP_RUNTIME_API_URL = "http://127.0.0.1:3210";

const started = await startServer();

expect(started.apiUrl).toBe("https://paperclip.example");
expect(process.env.PAPERCLIP_API_URL).toBe("https://paperclip.example");
expect(process.env.PAPERCLIP_RUNTIME_API_URL).toBe("http://127.0.0.1:3210");
expect(JSON.parse(process.env.PAPERCLIP_RUNTIME_API_CANDIDATES_JSON ?? "[]")[0]).toBe(
"http://127.0.0.1:3210",
);
});

it("falls back to host-based URL when PAPERCLIP_API_URL is not set", async () => {
const started = await startServer();

Expand Down
16 changes: 9 additions & 7 deletions server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,15 +783,17 @@ export async function startServer(): Promise<StartedServer> {
}

const runtimeListenHost = config.host;
const runtimeApiUrl = choosePrimaryRuntimeApiUrl({
authPublicBaseUrl: config.authPublicBaseUrl ?? null,
allowedHostnames: config.allowedHostnames,
bindHost: runtimeListenHost,
port: listenPort,
});
const configuredRuntimeApiUrl = process.env.PAPERCLIP_RUNTIME_API_URL?.trim();
const runtimeApiUrl = configuredRuntimeApiUrl ||
choosePrimaryRuntimeApiUrl({
authPublicBaseUrl: config.authPublicBaseUrl ?? null,
allowedHostnames: config.allowedHostnames,
bindHost: runtimeListenHost,
port: listenPort,
});
const configuredApiUrl = process.env.PAPERCLIP_API_URL?.trim() || runtimeApiUrl;
const runtimeApiCandidates = buildRuntimeApiCandidateUrls({
preferredApiUrl: configuredApiUrl,
preferredApiUrl: configuredRuntimeApiUrl || configuredApiUrl,
authPublicBaseUrl: config.authPublicBaseUrl ?? null,
allowedHostnames: config.allowedHostnames,
bindHost: runtimeListenHost,
Expand Down
Loading