Skip to content

Paperclip Desktop rejects valid authenticated/public remote because /api/health omits authReady #25

Description

@tobiasmuehl

Pre-submission checklist

  • I have searched existing open and closed issues and this is not a duplicate.
  • I am on the latest released version of Paperclip (or can reproduce on master).
  • I have confirmed the error originates in Paperclip itself — not in my agent adapter, API provider, or local configuration.

What happened?

Paperclip Desktop remote connection preflight reports that a remote Paperclip server does not expose the health endpoint / is not a Paperclip host when connecting to an authenticated public Paperclip server whose /api/health response omits authReady.

The health endpoint is publicly reachable and returns HTTP 200 JSON, but the Desktop preflight parser rejects the response because it contains deploymentExposure: "public" while omitting authReady.

This creates a confusing user-facing error: the endpoint is reachable, but Desktop says the health endpoint is not accessible or the host does not appear to expose it.

Original failing health response

Before the workaround, the public health endpoint was reachable and returned HTTP 200:

curl -i https://paperclip.example.com/api/health

Response body:

{
  "status": "ok",
  "deploymentMode": "authenticated",
  "deploymentExposure": "public",
  "bootstrapStatus": "ready",
  "bootstrapInviteActive": false
}

Important detail: authReady is missing.

Session probe response

The auth/session probe returns the expected signed-out response:

curl -i https://paperclip.example.com/api/auth/get-session

Response:

HTTP/2 401
content-type: application/json; charset=utf-8
{"error":"Board authentication required"}

This appears to be expected and is handled by Desktop as signed_out when the health response passes validation.

Expected behavior

Desktop should accept this host as a valid authenticated Paperclip remote, or the server should include authReady in its public authenticated health response.

A valid authenticated/public health response should look like:

{
  "status": "ok",
  "deploymentMode": "authenticated",
  "deploymentExposure": "public",
  "authReady": true,
  "bootstrapStatus": "ready",
  "bootstrapInviteActive": false
}

Steps to reproduce

Follow the VPS install guide in the docs

Paperclip version or commit

2026.609.0

Deployment mode

Self-hosted server

Installation method

npm / pnpm global install

Agent adapter(s) involved

  • Claude Code
  • Codex
  • Cursor
  • Droid
  • Hermes
  • Custom / external plugin adapter
  • Not adapter-specific (core bug)

Database mode

None

Access context

None

Node.js version

No response

Operating system

No response

Relevant logs or output

Relevant config (if applicable)

Additional context

Actual behavior

Paperclip Desktop rejects the remote during preflight with a message equivalent to:

  • This host does not appear to expose the Paperclip health endpoint.
  • The health endpoint is not publicly accessible.

However, /api/health is publicly accessible and returns HTTP 200 JSON.

Root cause observed

In Paperclip Desktop src/connection/preflight.ts, the health parser accepts either:

  1. Full health shape:
    • deploymentExposure !== null
    • authReady !== null

or:

  1. Redacted authenticated shape:
    • status === "ok"
    • deploymentMode === "authenticated"
    • bootstrapStatus !== null
    • bootstrapInviteActive !== null
    • deploymentExposure === null
    • authReady === null

The server response is neither shape:

  • deploymentExposure is present: "public"
  • authReady is absent/null

So parseHealthPayload() returns null, and Desktop reports not_paperclip / health endpoint unavailable even though the endpoint is reachable.

Pseudo-simulation of the failing shape:

const body = {
  status: "ok",
  deploymentMode: "authenticated",
  deploymentExposure: "public",
  bootstrapStatus: "ready",
  bootstrapInviteActive: false,
};

const authReady = typeof body.authReady === "boolean" ? body.authReady : null;
const deploymentExposure = body.deploymentExposure === "private" || body.deploymentExposure === "public"
  ? body.deploymentExposure
  : null;

const hasFullHealthShape = deploymentExposure !== null && authReady !== null; // false
const hasRedactedAuthenticatedShape =
  body.status === "ok" &&
  body.deploymentMode === "authenticated" &&
  body.bootstrapStatus !== null &&
  body.bootstrapInviteActive !== null &&
  deploymentExposure === null &&
  authReady === null; // false because deploymentExposure is present

Relevant config (sanitized)

/home/paperclip/paperclip.env:

PAPERCLIP_DEPLOYMENT_MODE=authenticated
PAPERCLIP_DEPLOYMENT_EXPOSURE=public
PAPERCLIP_AUTH_PUBLIC_BASE_URL=https://paperclip.example.com
PAPERCLIP_ALLOWED_HOSTNAMES=paperclip.example.com
DATABASE_URL=postgresql://paperclip:REDACTED@127.0.0.1:5432/paperclip

Relevant parts of /home/paperclip/.paperclip/instances/default/config.json:

{
  "database": {
    "mode": "postgres",
    "connectionString": "REDACTED"
  },
  "server": {
    "deploymentMode": "authenticated",
    "exposure": "public",
    "bind": "loopback",
    "host": "127.0.0.1",
    "port": 3100,
    "allowedHostnames": [
      "paperclip.example.com"
    ],
    "serveUi": true
  },
  "auth": {
    "baseUrlMode": "explicit",
    "disableSignUp": false,
    "publicBaseUrl": "https://paperclip.example.com"
  },
  "storage": {
    "provider": "local_disk"
  }
}

Relevant Nginx reverse proxy shape before workaround:

location / {
    proxy_pass         http://127.0.0.1:3100;
    proxy_http_version 1.1;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $paperclip_forwarded_proto;
    proxy_set_header   Upgrade           $http_upgrade;
    proxy_set_header   Connection        "upgrade";
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

Workaround used successfully

I applied a narrow Nginx workaround for only /api/health to inject "authReady": true into the JSON response.

Nginx location block:

location = /api/health {
    proxy_pass         http://127.0.0.1:3100;
    proxy_http_version 1.1;
    proxy_set_header   Host              $host;
    proxy_set_header   X-Real-IP         $remote_addr;
    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header   X-Forwarded-Proto $paperclip_forwarded_proto;
    proxy_set_header   Accept-Encoding   "";
    sub_filter_types   application/json;
    sub_filter_once    on;
    sub_filter         '"deploymentExposure":"public","bootstrapStatus"' '"deploymentExposure":"public","authReady":true,"bootstrapStatus"';
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

After reloading Nginx, the public health endpoint returns:

{
  "status": "ok",
  "deploymentMode": "authenticated",
  "deploymentExposure": "public",
  "authReady": true,
  "bootstrapStatus": "ready",
  "bootstrapInviteActive": false
}

Desktop then accepts the remote.

Verification after workaround

curl -s https://paperclip.example.com/api/health

returns:

{"status":"ok","deploymentMode":"authenticated","deploymentExposure":"public","authReady":true,"bootstrapStatus":"ready","bootstrapInviteActive":false}

Desktop parser simulation result:

{
  "accepted": true,
  "status": "ok",
  "deploymentMode": "authenticated",
  "deploymentExposure": "public",
  "authReady": true,
  "bootstrapStatus": "ready",
  "bootstrapInviteActive": false
}

Suggested fixes

Possible server-side fix:

  • Ensure Paperclip server 2026.609.0 includes authReady in /api/health whenever it includes deploymentExposure for authenticated deployments.

Possible Desktop-side fix:

  • Accept the current server response shape for authenticated deployments where:
    • status === "ok"
    • deploymentMode === "authenticated"
    • deploymentExposure is "public" or "private"
    • bootstrapStatus and bootstrapInviteActive are present
    • authReady is absent
  • Or show a more precise error such as: Health endpoint is reachable, but response is missing authReady.

Why this matters

The current Desktop error suggests a networking/public accessibility problem. That sends users toward DNS, Cloudflare, TLS, and firewall debugging, even though the actual issue is a schema mismatch between the server's /api/health response and Desktop's parser.

Server/origin:

  • Paperclip CLI/server package: paperclipai@2026.609.0
  • paperclipai --version: 2026.609.0
  • @paperclipai/server: 2026.609.0 via global paperclipai package
  • Latest npm paperclipai at time of testing: 2026.609.0
  • Node.js: v22.22.1
  • npm: 9.2.0
  • OS: Ubuntu 26.04 LTS x86_64
  • Nginx: nginx/1.28.3 (Ubuntu)
  • Deployment mode: authenticated
  • Deployment exposure: public
  • Paperclip server bound to: 127.0.0.1:3100
  • Reverse proxy: Nginx, public HTTPS through Cloudflare

Desktop:

  • Paperclip Desktop latest release observed: v3.2.9
  • Repository: https://github.com/aronprins/paperclip-desktop
  • Desktop remote preflight code appears to probe:
    • /api/health
    • /api/auth/get-session

Privacy checklist

  • I have reviewed all pasted output for PII (usernames, file paths, API keys, tokens, company names) and redacted where necessary.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions