Skip to content

feat(listen): support headless mode for Docker/CI#8

Open
jamierpond wants to merge 12 commits intopolarsource:mainfrom
jamierpond:feat/headless-listen
Open

feat(listen): support headless mode for Docker/CI#8
jamierpond wants to merge 12 commits intopolarsource:mainfrom
jamierpond:feat/headless-listen

Conversation

@jamierpond
Copy link
Copy Markdown

@jamierpond jamierpond commented Apr 7, 2026

Why

polar listen requires interactive prompts (environment selection, OAuth browser login, organization selection), making it unusable in Docker containers, CI pipelines, or any headless context.

Additionally, the prebuilt binary only ships for linux/amd64 and darwin. On Apple Silicon Macs running Docker (linux/arm64), the binary crashes under Rosetta (SIGILL / exit code 132). Building from source solves the arch problem, but the interactive prompts still block headless use.

What

Three new optional flags for polar listen (each with an env var fallback):

  • --access-token / POLAR_ACCESS_TOKEN — Use a personal access token directly, skipping the OAuth browser login flow
  • --env sandbox|production / POLAR_ENVIRONMENT — Select the environment without the interactive prompt
  • --org <slug-or-id> — Select an organization by slug or ID. Auto-selects when only one org exists

One new optional flag for webhook relay integrity:

  • --webhook-secret / POLAR_WEBHOOK_SECRET — Re-sign forwarded payloads using the standardwebhooks format (HMAC-SHA256), so the receiving app's validateEvent / Webhooks() signature verification passes without needing to disable it in development

A multi-stage Dockerfile is included so the CLI can be built from source and run as a lightweight container — useful for local dev docker-compose setups where you need a webhook relay sidecar alongside your app, database, etc. The final image only contains the built output and runtime dependencies.

All existing interactive behavior is preserved when flags/env vars are not provided.

How

  • resolveEnvironment() checks --env flag, then POLAR_ENVIRONMENT env var, then falls back to the interactive prompt
  • resolveAccessToken() checks --access-token flag, then POLAR_ACCESS_TOKEN env var, then falls back to the OAuth login flow
  • resolveOrganization() fetches orgs via the API when using a personal access token (bypassing the OAuth-dependent Polar service), auto-selects when only one org exists, and matches by slug or ID when --org is provided
  • signPayload() generates standardwebhooks-compatible headers (webhook-id, webhook-timestamp, webhook-signature) using HMAC-SHA256 with the raw UTF-8 bytes of the secret — matching the key derivation in @polar-sh/sdk's validateEvent

What's not in scope

  • Changes to polar login or other commands
  • Publishing linux/arm64 prebuilt binaries (separate infrastructure change)
  • npm package publishing for @polar-sh/cli
  • Changes to the OAuth flow itself

Examples

# Fully headless with webhook re-signing (Docker, CI)
POLAR_ACCESS_TOKEN=... POLAR_ENVIRONMENT=sandbox POLAR_WEBHOOK_SECRET=... \
  polar listen http://localhost:3000/api/polar/webhook

# Explicit flags
polar listen \
  --access-token pat_xxx \
  --env sandbox \
  --webhook-secret polar_whs_xxx \
  http://localhost:3000/api/polar/webhook

docker-compose sidecar

services:
  polar-relay:
    build: https://github.com/polarsource/cli.git
    environment:
      - POLAR_ACCESS_TOKEN=${POLAR_ACCESS_TOKEN}
      - POLAR_ENVIRONMENT=sandbox
      - POLAR_WEBHOOK_SECRET=${POLAR_WEBHOOK_SECRET}
    command: ["listen", "http://web:3000/api/polar/webhook"]
    depends_on:
      web:
        condition: service_healthy

  web:
    build: .
    ports:
      - "3000:3000"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/"]
      interval: 5s
      timeout: 10s
      retries: 5

Test plan

  • --access-token + --env sandbox connects without any prompts
  • POLAR_ACCESS_TOKEN + POLAR_ENVIRONMENT env vars work identically
  • Auto-selects org when only one exists
  • --org <slug> selects the correct org when multiple exist
  • --webhook-secret re-signs payloads; receiving app returns 200 (was 403 without signing)
  • Without any flags, existing interactive flow is unchanged
  • polar listen --help shows all new options with descriptions
  • signPayload signatures verified against standardwebhooks (unit tests)

@jamierpond jamierpond force-pushed the feat/headless-listen branch from 5b62333 to 3a4e190 Compare April 7, 2026 04:45
Add --access-token, --env, and --org flags (with env var fallbacks)
to `polar listen` so it can run without interactive prompts.

This enables usage in Docker containers, CI pipelines, and scripts
where no TTY or browser is available for OAuth login.
@jamierpond jamierpond force-pushed the feat/headless-listen branch from 3a4e190 to 0360186 Compare April 7, 2026 04:58
@jamierpond
Copy link
Copy Markdown
Author

@emilwidlund you got a moment to check this out?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant