hostc
Expose a local development server through a self-hosted Cloudflare tunnel endpoint.
hostc is a self-hosted ngrok alternative built on Cloudflare (Durable Objects, Workers). Free tier eligible within limits. Inspect the source and license in the linked repository.
Source & license
Upstream license: Apache-2.0
License TL;DR
You can use, change and sell it, including in closed-source products. When sharing copies, include the license, keep required notices and mark changed files. It includes a contributor patent grant with conditions, but no trademark permission or warranty.
Explain Apache 2.0 in plain English →Summary of the main license. Separate packages and assets can have different terms.
Inspect repository ↗Read this project’s actual license ↗Repository owner
See the upstream repository for the original creator and contributors.
Maintain this project? Maintainer verification →Cloudflare hosting
Free tier eligible within limits
The documented hostc deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs.
Hosting requirements
- Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits.
- Use the configured SQLite Durable Object classes within 100,000 requests/day, 13,000 GB-s duration/day, 5 million SQL rows read/day, 100,000 written/day and 5 GB storage; active sockets consume duration.
- Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Sources checked 01/10/2026
Repository snapshot: 8a840da. Hosting eligibility reflects the deployment documentation and listed assumptions.
- ngrok ↗
- **Nothing to set up.** No sign-up, no auth token, no binary to download. If you have Node.js, you have hostc. - **Visitors see your app.** No warning page to click through before they reach it. - **Hot reload works.** WebSockets pass straight through, so Vite, Next.js and friends update every open device the moment you save. Server-Sent Events stream as they are produced. - **Dev servers accept it as is.** Requests arrive addressed to localhost and redirects point back to the public URL, so there is no allowed-hosts setting to change. - **The link survives.** Network drops and server restarts don't change the URL; hostc reconnects on its own. - **Free and open source.** The server is a Cloudflare Worker you can also run on your own domain.
- workers ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "hostc-tunnel", "main": "src/index.ts", "compatibility_date": "2026-09-20", // Served only on the routes passed at deploy time. "workers_dev": false, // Local development defaults. Production values are passed by scripts/deploy.ts. "vars": { "TUNNEL_DOMAIN": "localhost:8787", }, "durable_objects": { "bindings": [{ "name": "TUNNEL", "class_name": "Tunnel" }],
- durable-objects ↗
routes passed at deploy time. "workers_dev": false, // Local development defaults. Production values are passed by scripts/deploy.ts. "vars": { "TUNNEL_DOMAIN": "localhost:8787", }, "durable_objects": { "bindings": [{ "name": "TUNNEL", "class_name": "Tunnel" }], }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Tunnel"] }], "ratelimits": [ { "name": "CREATE_LIMIT", "namespace_id": "1001", "simple": { "limit": 20, "period": 60 }, }, ], "observability": { "enabled": tru
- free-tier-eligible ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "hostc-tunnel", "main": "src/index.ts", "compatibility_date": "2026-09-20", // Served only on the routes passed at deploy time. "workers_dev": false, // Local development defaults. Production values are passed by scripts/deploy.ts. "vars": { "TUNNEL_DOMAIN": "localhost:8787", }, "durable_objects": { "bindings": [{ "name": "TUNNEL", "class_name": "Tunnel" }],
- free-tier-eligible ↗
routes passed at deploy time. "workers_dev": false, // Local development defaults. Production values are passed by scripts/deploy.ts. "vars": { "TUNNEL_DOMAIN": "localhost:8787", }, "durable_objects": { "bindings": [{ "name": "TUNNEL", "class_name": "Tunnel" }], }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Tunnel"] }], "ratelimits": [ { "name": "CREATE_LIMIT", "namespace_id": "1001", "simple": { "limit": 20, "period": 60 }, }, ], "observability": { "enabled": tru
- free-tier-eligible ↗
up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co
- free-tier-eligible ↗
jects are available both on Workers Free and Workers Paid plans. - **Workers Free plan**: Only Durable Objects with [SQLite storage backend](https://developers.cloudflare.com/durable-objects/best-practices/access-durable-objects-storage/#create-sqlite-backed-durable-object-class) are available. - **Workers Paid plan**: Durable Objects with the SQLite storage backend are available. The [key-value storage backend](https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/#storage-backends) is only available to accounts that already have a key-value-backed namespace. If you wish to downgrade from a Workers Paid plan to a Workers Free plan, you must first ensure that you have deleted all Durable Object namespaces with the key-value storage backend. On Workers Free plan: - If you exceed any one of the free tier limits, further operations of that type will fail with an error. - Daily free limits reset at 00:00 UTC. ## Compute billing Durable Objects are billed for compute duration (wall-clock time) while the Durable Object is actively running or is idle in memory but unable to [hibernate](https://developers.cloudflare.com/durable-objects/concepts/durable-object-lifecycle/). Durable Objects that are idle and eligible for hibernation are not billed for duration, even before the runtime has hibernated them. Requests to a D
- free-tier-eligible ↗
billed accordingly. | | Free plan | Paid plan | | --- | --- | --- | | Requests | 100,000 / day | 1 million / month, + $0.15/million<br> Includes HTTP requests, RPC sessions<sup>1</sup>, WebSocket messages<sup>2</sup>, and alarm invocations | | Duration<sup>3</sup> | 13,000 GB-s / day | 400,000 GB-s / month, + $12.50/million GB-s<sup>4,5</sup> | <details> <summary> Footnotes </summary> <sup>1</sup> Each <a href="https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/">RPC session</a> is billed as one request to your Durable Object. Every <a href="https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/">RPC method call</a> on a <a href="https://developers.cloudflare.com/durable-objects/">Durable Objects stub</a> is its own RPC session and therefore a single billed request. RPC method calls can return objects (stubs) extending <a href="https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/#lifetimes-memory-and-resource-management"><code>RpcTarget</code></a> and invo
- free-tier-eligible ↗
/). | | Workers Free plan | Workers Paid plan | | --- | --- | --- | | Rows reads <sup>1,2</sup> | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written <sup>1,2,3,4</sup> | 100,000 / day | First 50 million / month included + $1.00 / million rows | | SQL Stored data <sup>5</sup> | 5 GB (total) | 5 GB-month, + $0.20/ GB-month | <details> <summary> Footnotes </summary> <sup>1</sup> Rows read and rows written included limits and rates match <a href="https://developers.cloudflare.com/d1/platform/pricing/">D1 pricing</a>, Cloudflare's serverless SQL database. <sup>2</sup> Key-value methods like <code>get()</code>, <code>put()</code>, <code>delete()</code>, or <code>list(
- Apache-2.0 ↗
Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to ca
- architecture ↗
{ "$schema": "node_modules/wrangler/config-schema.json", "name": "hostc-tunnel", "main": "src/index.ts", "compatibility_date": "2026-09-20", // Served only on the routes passed at deploy time. "workers_dev": false, // Local development defaults. Production values are passed by scripts/deploy.ts. "vars": { "TUNNEL_DOMAIN": "localhost:8787", }, "durable_objects": { "bindings": [{ "name": "TUNNEL", "class_name": "Tunnel" }],
- architecture ↗
routes passed at deploy time. "workers_dev": false, // Local development defaults. Production values are passed by scripts/deploy.ts. "vars": { "TUNNEL_DOMAIN": "localhost:8787", }, "durable_objects": { "bindings": [{ "name": "TUNNEL", "class_name": "Tunnel" }], }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Tunnel"] }], "ratelimits": [ { "name": "CREATE_LIMIT", "namespace_id": "1001", "simple": { "limit": 20, "period": 60 }, }, ], "observability": { "enabled": tru
What it can replace
Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.
Public HTTP/WebSocket access to a local development server through a self-hosted tunnel endpoint; complete managed identity, logging and enterprise tunnel policies are excluded.
See supporting source ↗How it works
The shape of hostc on Cloudflare, and how it stacks up against the rented tools it replaces.
Architecture
Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.
View upstream source ↗Configuration and workflow sources
Reviewed commit 8a840dac9220. Files were read as data; upstream applications and CI jobs were not executed.
Deployment configuration · 2 files
Cloudflare Workers · compatibility 2026-09-20
hostc-tunnel · default
Entrypoint: src/index.ts
TUNNEL→ Durable Objects · class Tunnel
Cloudflare Workers · compatibility 2026-09-20
hostc-web · default
Static assets: public · 404-page
Static assets→ Static assets
Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.
Runtime source · handlers, binding usage and workflow steps
Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.
- L18 · fetch handler exported · references TUNNEL_DOMAIN · calls tunnelIdFromHost, handleTunnelRequest, Response.json, url.pathname.startsWith, checkProtocol, createTunnel, jsonError, url.pathname.match, connectTunnel
- L51 · tunnelIdFromHost calls (conditional paths may differ): tunnelDomain.toLowerCase, host.toLowerCase, normalized.endsWith, normalized.slice, isTunnelId
- L61 · handleTunnelRequest calls (conditional paths may differ): errorResponse, fetch, env.TUNNEL.getByName
- L68 · createTunnel calls (conditional paths may differ): request.headers.get, env.CREATE_LIMIT.limit, jsonError, randomTunnelId, init, env.TUNNEL.getByName, connectUrl.toString, signConnectToken, Response.json
- L89 · connectTunnel calls (conditional paths may differ): isTunnelId, jsonError, request.headers.get, verifyConnectToken, fetch, env.TUNNEL.getByName
- L103 · checkProtocol calls (conditional paths may differ): request.headers.get, String, jsonError
- L110 · randomTunnelId calls (conditional paths may differ): crypto.getRandomValues, join, Array.from
- L116 · jsonError calls (conditional paths may differ): Response.json
Environment references: env.TUNNEL_DOMAIN · env.TUNNEL · env.CREATE_LIMIT · env.TOKEN_SECRET
- L10 · errorResponse calls (conditional paths may differ): wantsHtml, Response.json, html, escape
- L54 · wantsHtml calls (conditional paths may differ): includes, request.headers.get
- L58 · html calls (conditional paths may differ): escape
- L83 · escape calls (conditional paths may differ): value.replace, char.charCodeAt
- L11 · signConnectToken calls (conditional paths may differ): crypto.subtle.sign, importKey, message, toBase64Url
- L16 · verifyConnectToken calls (conditional paths may differ): fromBase64Url, crypto.subtle.verify, importKey, message
- L24 · message calls (conditional paths may differ): encoder.encode
- L28 · importKey calls (conditional paths may differ): encoder.encode, keys.get, crypto.subtle.importKey, keys.set
- L43 · toBase64Url calls (conditional paths may differ): replace, replaceAll, btoa, String.fromCharCode
- L50 · fromBase64Url calls (conditional paths may differ): test, atob, replaceAll, value.replaceAll, Uint8Array.from, char.charCodeAt
- L667 · forwardedRequestHeaders calls (conditional paths may differ): name.startsWith, list.push, stripHopByHop
- L681 · publicResponseHeaders calls (conditional paths may differ): stripHopByHop, name.toLowerCase, headers.append, withoutCookieDomain
- L694 · withoutCookieDomain calls (conditional paths may differ): cookie.split, attributes.filter, attribute.split, join
- L700 · attachmentOf calls (conditional paths may differ): ws.deserializeAttachment
- L704 · isStale calls (conditional paths may differ): attachmentOf, ctx.getWebSocketAutoResponseTimestamp, Date.now
- L711 · safeClose calls (conditional paths may differ): ws.close, sendableCloseCode, sendableCloseReason
- L719 · withTimeout calls (conditional paths may differ): setTimeout, resolve, promise.then, clearTimeout, reject
Build and deployment pipeline · 2 GitHub Actions workflows
Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.
Triggers: push, pull_request
check · no job dependencies declared
- actions/checkout@v7
actions/checkout@v7 - pnpm/action-setup@v6
pnpm/action-setup@v6 - actions/setup-node@v7
actions/setup-node@v7 - Shell command
pnpm install --frozen-lockfile - Shell command
pnpm check - Shell command
cp apps/server/.dev.vars.example apps/server/.dev.vars - Shell command
pnpm test:e2e
Triggers: workflow_dispatch
publish · no job dependencies declared
Condition: github.ref == 'refs/heads/main'
- actions/checkout@v7
actions/checkout@v7 - pnpm/action-setup@v6
pnpm/action-setup@v6 - actions/setup-node@v7
actions/setup-node@v7 - Shell command
pnpm install --frozen-lockfile - Shell command
pnpm check - Shell command
pnpm -F hostc pack --pack-destination "$RUNNER_TEMP" - Shell command
npm publish "$RUNNER_TEMP"/hostc-*.tgz --tag "${{ inputs.dist-tag }}"
build: pnpm -F hostc builddeploy:server: pnpm -F @hostc/server run deploydeploy:web: pnpm -F web run deploy
build: tsdown
deploy: node scripts/deploy.ts
deploy: node scripts/deploy.ts
Repository README
View original on GitHub ↗Full upstream document by @akazwz · README.md · snapshot 8a840da
hostc
Localhost, anywhere.
One command gives your dev server a public HTTPS URL. WebSockets and hot reload included.
Free, open source, no account.
Quick start
Start your app, then point hostc at its port:
npx hostc@latest 3000
https://k7m2xq9pa4dn.hostc.app → http://localhost:3000
Anyone with this URL can reach your local server. Press Ctrl+C to stop.
Open the URL on any device. Every request shows up in your terminal as it arrives.
hostc is free and open source. If it helps you, a star on GitHub helps other developers find it, and it's the best encouragement for the project.
Why hostc
- Nothing to set up. No sign-up, no auth token, no binary to download. If you have Node.js, you have hostc.
- Visitors see your app. No warning page to click through before they reach it.
- Hot reload works. WebSockets pass straight through, so Vite, Next.js and friends update every open device the moment you save. Server-Sent Events stream as they are produced.
- Dev servers accept it as is. Requests arrive addressed to localhost and redirects point back to the public URL, so there is no allowed-hosts setting to change.
- The link survives. Network drops and server restarts don't change the URL; hostc reconnects on its own.
- Free and open source. The server is a Cloudflare Worker you can also run on your own domain.
Use it to
- share something you built with an AI coding tool before you deploy it anywhere,
- show work in progress to a teammate or a client,
- test webhooks from Stripe, GitHub or Slack against the code on your machine,
- try your site on a real phone, with hot reload,
- let a coding agent share what it built. Point the agent at hostc.dev/llms.txt and it knows how to run hostc and read the URL.
Usage
hostc <target> [options]
3000 http://localhost:3000
127.0.0.1:8080 http://127.0.0.1:8080
https://localhost:5173 any http(s) origin
--server <url> tunnel server (env HOSTC_SERVER)
--qr print a QR code of the public URL
-h, --help show this help
-v, --version show the version
Press Ctrl+C to stop; the URL is released immediately. Restarting hostc gives a new URL.
The URL is public: anyone who has it reaches your server. Only expose what you mean to share.
Always run @latest
hostc is free and moves fast, so server updates can be incompatible with older CLIs. Run it with
npx hostc@latest and you always get the matching version.
Don't install it globally (npm i -g hostc) or add it to a project: an installed copy stays on its
version and stops working when the server moves on. An outdated CLI stops at startup and tells
you to upgrade.
How it works
browser ──▶ Worker ──▶ Durable Object (one per tunnel) ◀── WebSocket ── hostc ──▶ localhost
hostc opens a single outgoing WebSocket, so nothing on your machine has to be reachable from the internet. Each public request or WebSocket becomes a stream on that connection, with flow control for bodies. Idle tunnels sleep on the server, which is what keeps hostc free. The wire format is in docs/protocol.md.
Self-hosting
Add your domains to Cloudflare: one for the API and one for tunnels, for example
example.comandexample.app. They can be the same domain, but a separate tunnel domain keeps tunnel cookies and abuse reports away from your main site. Add a proxied wildcard DNS record on the tunnel domain (*→192.0.2.1); Cloudflare's Universal SSL covers*.example.app.Set the token secret once:
pnpm -F @hostc/server exec wrangler secret put TOKEN_SECRET(at least 32 random bytes, e.g.openssl rand -base64 48).Deploy:
API_DOMAIN=example.com TUNNEL_DOMAIN=example.app pnpm deploy:serverUse it:
npx hostc@latest 3000 --server https://example.com, or build the CLI withHOSTC_DEFAULT_SERVER=https://example.com pnpm build.
Contributing
Requires Node.js 22.22+ and pnpm.
pnpm install
cp apps/server/.dev.vars.example apps/server/.dev.vars
pnpm dev # tunnel server on http://localhost:8787
pnpm build # build the CLI
node apps/cli/dist/hostc.mjs 3000 --server http://localhost:8787
Tunnels are served on http://<id>.localhost:8787; Chrome and Firefox resolve *.localhost to your machine.
pnpm check # format check, lint, typecheck, unit and integration tests
pnpm test:e2e # wrangler dev + CLI + a local origin, end to end
| Path | What |
|---|---|
packages/protocol |
frame format, messages and constants shared by both sides |
packages/client |
Node.js client: connection, streams, reconnects |
apps/server |
Cloudflare Worker + Durable Object tunnel server |
apps/cli |
the hostc command |
apps/web |
hostc.dev website (one static page) and llms.txt |
Issues and pull requests are welcome.
If hostc is useful to you, star it on GitHub.
License
Frequently asked about hostc
What is hostc?+
hostc is a self-hosted ngrok alternative built on the Cloudflare developer platform. Expose a local development server through a self-hosted Cloudflare tunnel endpoint.
What does hostc replace?+
hostc is listed as an alternative to ngrok. Compare the features and tradeoffs before migrating.
What Cloudflare primitives does hostc use?+
hostc is built on Durable Objects, Workers.
How much does hostc cost to run?+
The documented hostc deployment can use Cloudflare Free allowances for a small workload under the request, CPU and service-specific quotas below. This is conditional eligibility, not a measured zero-cost deployment; optional features, domains and external providers can add costs. Workers Free allows 100,000 requests per day shared across the account and 10 ms CPU per invocation; measure CPU-heavy authentication, parsing and rendering before assuming it fits. Use the configured SQLite Durable Object classes within 100,000 requests/day, 13,000 GB-s duration/day, 5 million SQL rows read/day, 100,000 written/day and 5 GB storage; active sockets consume duration. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.
Is hostc open source?+
The upstream repository declares the Apache-2.0 license. Read its terms at https://raw.githubusercontent.com/akazwz/hostc/8a840dac9220662a51c57eda1d21a9f221caf6fa/LICENSE. Source code and contributor credit are available at https://github.com/akazwz/hostc.

Discussion · 0
sign in to comment →