{"id":45191397,"url":"https://github.com/nonchan7720/webhook-over-websocket","last_synced_at":"2026-04-01T19:36:20.377Z","repository":{"id":339547921,"uuid":"1162264733","full_name":"nonchan7720/webhook-over-websocket","owner":"nonchan7720","description":null,"archived":false,"fork":false,"pushed_at":"2026-03-20T07:18:18.000Z","size":128,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-20T21:51:21.727Z","etag":null,"topics":["cloudflared-alternative","docker","golang","ngrok-alternative","traefik","tunnel","tunnel-client","tunnel-server","tunneling","websocket"],"latest_commit_sha":null,"homepage":"https://nonchan7720.github.io/webhook-over-websocket/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nonchan7720.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-20T03:44:20.000Z","updated_at":"2026-03-20T07:18:10.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/nonchan7720/webhook-over-websocket","commit_stats":null,"previous_names":["nonchan7720/webhook-over-websocket"],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/nonchan7720/webhook-over-websocket","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nonchan7720%2Fwebhook-over-websocket","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nonchan7720%2Fwebhook-over-websocket/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nonchan7720%2Fwebhook-over-websocket/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nonchan7720%2Fwebhook-over-websocket/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nonchan7720","download_url":"https://codeload.github.com/nonchan7720/webhook-over-websocket/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nonchan7720%2Fwebhook-over-websocket/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291151,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cloudflared-alternative","docker","golang","ngrok-alternative","traefik","tunnel","tunnel-client","tunnel-server","tunneling","websocket"],"created_at":"2026-02-20T12:03:54.580Z","updated_at":"2026-04-01T19:36:20.351Z","avatar_url":"https://github.com/nonchan7720.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# webhook-over-websocket\n\n[![Release](https://img.shields.io/github/v/release/nonchan7720/webhook-over-websocket)](https://github.com/nonchan7720/webhook-over-websocket/releases)\n[![Go Version](https://img.shields.io/github/go-mod/go-version/nonchan7720/webhook-over-websocket)](go.mod)\n[![License](https://img.shields.io/github/license/nonchan7720/webhook-over-websocket)](LICENSE)\n\n[日本語](README_ja.md)\n\nA tunnel tool that forwards external webhook requests to a local development server via WebSocket.\n\n## Overview\n\n`webhook-over-websocket` allows you to receive webhooks from external services (e.g. GitHub, Stripe, Slack) on your local development machine without exposing it to the internet directly. It works by establishing a persistent WebSocket connection between a publicly accessible server and the client running locally.\n\n```mermaid\nflowchart TD\n    A[External Service] -- HTTP --\u003e B[\"Server\u003cbr\u003e/webhook/{channel_id}\"]\n    B \u003c--\u003e|WebSocket| C[\"Client\u003cbr\u003e(local machine)\"]\n    C -- HTTP --\u003e D[\"Local application\u003cbr\u003e(e.g. http://localhost:3000)\"]\n```\n\n## Architecture\n\n| Component  | Role                                                                                                                                                                                                  |\n| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| **Server** | Publicly accessible HTTP server. Receives webhooks and forwards them over WebSocket to the connected client. Also exposes a Traefik HTTP Provider endpoint for dynamic routing when running at scale. |\n| **Client** | Runs on the local machine. Connects to the server via WebSocket, receives webhook payloads, and forwards them to the local application.                                                               |\n\n### Server Endpoints\n\n| Endpoint                           | Description                                                                           |\n| ---------------------------------- | ------------------------------------------------------------------------------------- |\n| `GET /new`                         | Issues a new `channel_id` (UUID) for a client to use                                  |\n| `GET /traefik-config`              | Returns dynamic Traefik routing configuration (HTTP Provider)                         |\n| `GET /internal/channels`           | Returns active channel list (used for peer-to-peer sync in multi-replica deployments) |\n| `GET /ws/{channel_id}`             | WebSocket upgrade endpoint for client connections                                     |\n| `POST /webhook/{channel_id}[/...]` | Receives external webhook requests and tunnels them to the client                     |\n| `GET /auth/login`                 | *(auth mode only)* Redirects to the GitHub OAuth consent page                         |\n| `GET /auth/callback`               | *(auth mode only)* GitHub OAuth callback; returns a session JWT                       |\n\n## Installation\n\n### Docker\n\n```bash\ndocker pull ghcr.io/nonchan7720/webhook-over-websocket:latest\n```\n\n### Go install\n\n```bash\ngo install github.com/nonchan7720/webhook-over-websocket@latest\n```\n\n### Binary download\n\nDownload the latest binary for your platform from the [Releases](https://github.com/nonchan7720/webhook-over-websocket/releases) page.\n\n## Usage\n\n### 1. Start the server\n\nRun the server on a publicly accessible host:\n\n```bash\nwebhook-over-websocket server --port 8080\n```\n\nOr with Docker:\n\n```bash\ndocker run --rm -p 8080:8080 ghcr.io/nonchan7720/webhook-over-websocket:latest server --port 8080\n```\n\n**Server flags:**\n\n| Flag                         | Default   | Description                                                            |\n| ---------------------------- | --------- | ---------------------------------------------------------------------- |\n| `--port`, `-p`               | `8080`    | Port to listen on                                                      |\n| `--peer-domain`              | *(empty)* | Peer domain name for memberlist cluster discovery                      |\n| `--cleanup-duration`         | `5m`      | Interval for cleaning up inactive channel sessions                     |\n| `--memberlist-port`          | `7946`    | Port for memberlist gossip protocol                                    |\n| `--memberlist-sync-duration` | `5s`      | Interval for memberlist cluster synchronization                        |\n| `--github-client-id`         | *(empty)* | GitHub OAuth App client ID — **enables authentication when set**       |\n| `--github-client-secret`     | *(empty)* | GitHub OAuth App client secret (required when `--github-client-id` is set) |\n| `--github-org`               | *(empty)* | Required GitHub organization — only members are allowed access         |\n| `--jwt-signing-key`               | *(empty)* | Secret key for signing JWT tokens (required when `--github-client-id` is set) |\n\n### 2. Start the client\n\nRun the client on your local machine, pointing it at the server and your local application:\n\n```bash\nwebhook-over-websocket client \\\n  --server-url http://your-server.example.com \\\n  --target-url http://localhost:3000\n```\n\nOn startup, the client prints the webhook URL to configure in the external service:\n\n```\nIssued Channel ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\nPlease set the webhook destination as follows: http://your-server.example.com/webhook/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\nA tunnel to the server has been established.\n```\n\n**Client flags:**\n\n| Flag              | Default                 | Description                                                          |\n| ----------------- | ----------------------- | -------------------------------------------------------------------- |\n| `--server-url`    | *(required)*            | URL of the webhook-over-websocket server                             |\n| `--target-url`    | `http://localhost:3000` | URL of the local application to forward webhook requests to          |\n| `--token`         | *(empty)*               | Session JWT token for authentication (required when server auth is enabled) |\n| `--insecure`      | `false`                 | Skip TLS certificate verification                                    |\n\n### 3. Configure the external service\n\nSet the webhook URL in the external service (e.g. GitHub, Stripe) to:\n\n```\nhttp://your-server.example.com/webhook/\u003cchannel_id\u003e\n```\n\nAny path suffix after the channel ID is preserved and forwarded to your local application as-is.\n\n## Authentication\n\nWhen the server is configured with a GitHub OAuth App, only authenticated users can obtain `channel_id`s and connect via WebSocket.  Two levels of access control are supported:\n\n1. **Organization check** – only members of a specific GitHub organization may authenticate (enabled via `--github-org`).\n2. **Per-channel token** – every `channel_id` is bound to a signed JWT whose `sub` (subject) claim equals the `channel_id`, so a token issued for one channel cannot be used for another.\n\n### Setup\n\n1. [Create a GitHub OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) and set the callback URL to `http://your-server.example.com/auth/callback`.\n2. Start the server with the auth flags:\n\n```bash\nwebhook-over-websocket server \\\n  --port 8080 \\\n  --github-client-id  \u003cYOUR_CLIENT_ID\u003e \\\n  --github-client-secret \u003cYOUR_CLIENT_SECRET\u003e \\\n  --github-org my-organization \\\n  --jwt-secret  \u003cA_LONG_RANDOM_STRING\u003e\n```\n\n### New server endpoints (auth mode only)\n\n| Endpoint            | Description                                                          |\n| ------------------- | -------------------------------------------------------------------- |\n| `GET /auth/github`  | Redirects the user to the GitHub OAuth consent page                  |\n| `GET /auth/callback`| GitHub calls this after the user approves; returns the session JWT   |\n\n\n\n| Variable | Description                                                                                                                      |\n| -------- | -------------------------------------------------------------------------------------------------------------------------------- |\n| `POD_IP` | Pod IP address used as the server's own IP (Kubernetes). When set to a valid IPv4 address, it is used instead of auto-detection. |\n\n## Clustering and High Availability\n\n### Traefik Integration with Memberlist\n\nFor production deployments with multiple server replicas (e.g. in Kubernetes), Traefik is used as a load balancer with dynamic routing so that webhook requests are always forwarded to the replica that holds the correct WebSocket connection.\n\n**Challenge:** Traefik's [HTTP Provider](https://doc.traefik.io/traefik/providers/http/) can only poll a single endpoint URL for configuration updates. In a multi-replica deployment, this creates a problem: how can a single endpoint return routing information for channels connected to different replicas?\n\n**Solution:** [HashiCorp Memberlist](https://github.com/hashicorp/memberlist) enables cluster coordination via a gossip-based membership protocol. When Traefik polls any single replica's `/traefik-config` endpoint, that replica automatically aggregates channel information from all cluster members and returns the complete routing configuration.\n\n**How it works:**\n\n1. Each server instance joins the memberlist cluster using the `--peer-domain` flag for DNS-based peer discovery\n2. Servers periodically exchange information about their active channels via the gossip protocol  \n3. When Traefik polls `/traefik-config` on any replica, that replica:\n   - Collects its own active channels\n   - Queries all other alive cluster members via `/internal/channels`\n   - Aggregates all channel information and generates the complete Traefik routing configuration\n4. Inactive or failed nodes are automatically detected and removed from the cluster\n\n**Configuration example:**\n\nServer:\n```bash\nwebhook-over-websocket server \\\n  --port 8080 \\\n  --peer-domain webhook-service.default.svc.cluster.local \\\n  --memberlist-port 7946 \\\n  --memberlist-sync-duration 5s\n```\n\nTraefik static configuration:\n```yaml\nproviders:\n  http:\n    endpoint: \"http://webhook-over-websocket-service/traefik-config\"\n    pollInterval: \"5s\"\n```\n\nWith this setup, Traefik can query any single replica (via the Kubernetes service), and that replica will return routing information for all channels across the entire cluster.\n\n## Development\n\nThe repository includes a Docker Compose file for local development:\n\n```bash\ndocker compose up -d\n```\n\nThis mounts the repository source into the container so you can edit files locally.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnonchan7720%2Fwebhook-over-websocket","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnonchan7720%2Fwebhook-over-websocket","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnonchan7720%2Fwebhook-over-websocket/lists"}