Local HTTP development has two halves. Sometimes the internet needs to reach your laptop — a webhook, an OAuth callback, a mobile build, a partner poking at your machine for ten minutes — and you need to see exactly what arrived. Other times you are the client, and you need to compose a request, authenticate properly, send it, and keep it somewhere your team can find it next month.
Tap is two products, one for each half:
| 🔌 | Tap Tunnel + Inspector | Give localhost a real public URL through Cloudflare Tunnel or Tailscale, and capture every request, response, SSE event, and WebSocket frame that flows through it. Runs as the tap CLI or as .NET Aspire resources. |
| 🧪 | Tap Studio | An HTTP workbench: compose requests, run real authentication flows, execute, chain them into flows and test sets that also run in CI, and keep the whole workspace in your git repo as Markdown. Ships as a desktop app, with an AI assistant built in. |
They share a philosophy more than they share code:
- Local-first. Everything runs on your machine. No account, no cloud workspace, no telemetry. Quick tunnels cost nothing; stable hostnames just need a domain you already own.
- Plain text, in your repo. Studio's workspace is Markdown with YAML frontmatter — no proprietary export, no sync service, ordinary git diffs.
- Secrets stay out of files. The Inspector never persists credentials; Studio resolves secret references at execute time and keeps tokens in your OS state folder, never in the workspace.
- Explicit boundaries. Tunnels are private by default where the provider allows it, and public exposure is something you opt into with your eyes open.
inbound Internet ─▶ Cloudflare Tunnel / Tailscale ─▶ Tap capture proxy ─▶ your service
│
▼
Inspector UI
requests · SSE · WS · replay
outbound request.req.md + auth profile + environment ─▶ Tap Studio ─▶ any API
└──────── Markdown, in your repo ─────────┘ executor
Use either on its own. Used together, Studio composes the call and the Inspector shows you what your service actually received.
For the moment when localhost needs to behave like a real internet endpoint, but you still want full visibility into every request. Mobile app hooks, webhook deliveries, third-party OAuth redirects, partner integrations, and "can you hit my laptop for a minute?" demos all need the same two things: a tunnel that is quick to bring up, and a request log that tells you what actually happened.
📖 Full reference: docs/inspector.md
Warning
Public tunnels are scanned within minutes. As soon as a public hostname's TLS certificate
hits a CT log — immediately, when Cloudflare Tunnel or Tailscale Funnel comes up —
opportunistic scanners start probing for admin endpoints and known-CVE banners. Always pair
public tunnels with Tap's auth options (header / CIDR / country / OIDC) or edge controls like
Cloudflare Access and WAF rules. For Tailscale, prefer WithTailscaleServe(...)
(tailnet-only) over WithTailscaleFunnel(...) (public) unless you actually need internet
exposure.
| Tunnels without ceremony | Free TryCloudflare URLs, dashboard connector tokens, API-managed Cloudflare tunnels + DNS, or Tailscale Serve/Funnel when your tailnet is the right boundary. |
| Captures every hop | Method, host, path, headers, status, timing, request and response bodies, and image previews — recorded before forwarding to your upstream. |
| Live streaming protocols | text/event-stream responses and WebSocket connections proxy through the same port and render as live, direction-tagged timelines in dedicated SSE and WS tabs. |
| Replay and QR | Replay any captured request; scan the public URL straight onto a phone from the QR tab. |
| Aspire-native | Model inspectors and tunnels in your AppHost. Allocated ports and generated hostnames resolve at startup and show up in the dashboard. |
| Auth on the public path | Header, CIDR, country, and OIDC checks gate the proxy branch before traffic reaches your upstream. The UI port stays local. |
All three routes install the same tap CLI.
dotnet tool install -g Tap # .NET 10 SDK on PATH
curl -fsSL https://raw.githubusercontent.com/philbir/tap/main/install.sh | sh # Linux/macOS, self-contained
irm https://raw.githubusercontent.com/philbir/tap/main/install.ps1 | iex # WindowsMake sure ~/.dotnet/tools (Linux/macOS) or %USERPROFILE%\.dotnet\tools (Windows) is on your
PATH. Pin a version with TAP_VERSION=0.1.0 (or $env:TAP_VERSION on Windows). Archives are
also on the Releases page as
tap-<version>-<rid>.tar.gz with a SHA256SUMS alongside.
Cloudflare features need cloudflared
on PATH — brew install cloudflared, winget install Cloudflare.cloudflared, or
tap install-cloudflared. Tailscale host modes need the
tailscale CLI; Docker mode doesn't.
tap run http://localhost:3000Proxy on http://localhost:4444, Inspector UI on http://localhost:4445.
# throwaway public URL, no account needed
tap run http://localhost:3000 --quick
# your own hostname, via a tunnel you created in the Cloudflare dashboard
tap run http://localhost:3000 --token "$CLOUDFLARE_TUNNEL_TOKEN" --hostname api-local.example.com
# tailnet-only (the safe default)
tap run http://localhost:3000 --tailscale
# public Tailscale Funnel — pair it with auth
tap run http://localhost:3000 --tailscale --tailscale-public --auth-header "X-Tap-Key=$TAP_KEY"Every flag, every environment variable, and the tap.config file format:
docs/inspector.md.
using Aspire.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var api = builder.AddProject<Projects.Sample_Api>("api");
var tap = builder.AddTap<Projects.Tap_Server>();
api.WithTap(tap);
builder.Build().Run();Inspector UI on http://localhost:5198; traffic through http://localhost:5199 is recorded
before it reaches api. Add .WithQuickTunnel(), .WithTunnel(...), .WithTailscaleServe(...),
or .WithTailscaleFunnel(...) to put a tunnel in front — the
Aspire recipes cover each mode.
| Package | Purpose |
|---|---|
Tap.Hosting |
Aspire AppHost extensions: AddTap, AddTapContainer, WithTap, WithTunnel, WithQuickTunnel, WithTailscaleServe (tailnet-only, default), WithTailscaleFunnel (public, opt-in), WithExistingTunnel, WithApiManagedTunnel, WithDynamicHostname, WithSystemDaemon / WithEphemeralDaemon / WithFunnelPort. |
Tap.Server |
ASP.NET Core capture server: YARP reverse proxy, capture middleware, WebSocket-terminating proxy, SSE event parser, REST API, /api/stream push channel, and the bundled React Inspector UI. |
Tap.Cli |
Local command host that reuses the same server code. |
Both entry points run the same Tap.Server host: the CLI builds TapInspectorOptions from
flags, environment variables, and tap.config; Aspire writes the same options as project
environment variables.
The other direction: you are the client. Studio is a full HTTP request workbench — compose, authenticate, execute, document — with a workspace that lives in your repository as plain Markdown.
📖 Full reference: docs/studio.md · 📄 On-disk format: docs/workspace-format.md
| Full request composition | Method, URL, query params, headers, and bodies as None / Form / Multipart / Raw / Binary / GraphQL — with JSON/XML formatting, multi-file uploads, and a GraphQL editor backed by the live schema. |
| Real responses | Status, duration, size, syntax-highlighted body with image and binary previews, plus Headers, the exact Request that went on the wire, the auth/variable Flow, and which Secrets were resolved. |
| Streaming | SSE responses stream in live; requests marked protocol: websocket open a real socket and append frames as they arrive. |
| Many authentication flows | OAuth 2.0 / OIDC (authorization code + PKCE, client credentials, ROPC, device code), Microsoft Entra, Azure CLI (direct + on-behalf-of), GitHub (PAT / gh CLI / GitHub App / OAuth App), AWS SigV4, signed JWT, bearer, basic, API key, and custom headers. |
| Flows and test sets | A flow (*.flow.md) runs requests in order and carries values out of one response into the next; a test set (*.test.md) groups checks that each run one request or one whole flow. The Testing tab authors both and streams every result as it lands. |
| The same verdict in CI | The tap-studio .NET tool runs those flows and test sets headlessly — JUnit, TRX, JSON, or Markdown reports, and exit codes a pipeline can branch on. Same engine as the UI, so a pull-request check and the Testing tab are one computation. |
| AI assistance | Hand the request to GitHub Copilot CLI or Claude Code — running locally, with your existing CLI login — and get a proposed edit you review before saving. |
| Git-native workspace | Requests, collections, auth profiles, environments, flows, and test sets are Markdown files. Built-in branch, diff, stage, and commit. |
| Variables and secrets | A six-level cascade (workspace → collection → stage → environment → request → per-run) over pluggable providers: process env with allowlists, an encrypted workspace file, Azure Key Vault, and machine-local system variables. |
Creating an auth profile starts from a template catalog; the wizard then asks only for the fields that flow actually needs, and shows you what it will write before it writes it.
Tokens never touch the workspace — they live in ~/.tap/auth-tokens.json, keyed by workspace
and profile, and refresh automatically. The redirect URI is owned by the runtime and shown
read-only so you know exactly what to register with your identity provider; the desktop app
uses the stable tap-studio://callback deep link instead of an ephemeral loopback port. You
can also pick which browser and profile handles an interactive sign-in, so a work tenant
doesn't land in your personal session.
Details and the full grant matrix: docs/studio.md.
Studio spawns an AI coding CLI you already have installed — no bundled SDK, no extra credentials. The assistant is handed the request you're editing plus the collection's base URL, default auth, and shared headers, the available auth profiles, the environment names, and the variable catalog, so it edits your workspace instead of inventing endpoints and tokens.
It never writes files. It proposes a structured request that the UI applies to the editor as an
unsaved change, together with Markdown documentation for the request — you review the diff and
decide whether to keep it. Secrets are always referenced as {{variables}}, never inlined.
.tap/
├── tap.md ← workspace: name, providers, default env
├── auth/corp-entra.auth.md ← auth profile shared by every collection
├── environments/local.env.md ← named variable set
├── tests/
│ ├── checkout.flow.md ← requests in order, values carried across
│ └── billing.test.md ← a set of checks over requests and flows
└── collections/billing/
├── _collection.md ← baseUrl, stages, default auth/headers
├── billing-oauth.auth.md ← auth profile scoped to this collection
└── create-customer.req.md ← one request, as a fenced http block
Because a request is a couple of lines of Markdown, review, blame, cherry-pick, and revert all work the way they do for code. Studio is the only thing that writes the YAML — editors PUT a typed spec and the server re-emits the file — so what lands in your diff is predictable.
Assertions answer whether one response looked right. Flows and test sets answer the two questions above that: does this multi-step exchange still work end to end, and do these requests still pass?
kind: flow
name: Checkout
steps:
- name: Create the order
request: ../collections/demo/create-order.req.md
extract:
- var: orderId # bind it out of the response…
jsonpath: $.order.id
- name: Read it back
request: ../collections/demo/get-order.req.md
vars:
id: '{{orderId}}' # …and the next step reads itThat is the whole mechanism. Extract from a JSONPath, an XPath, a header, the status, the duration, the whole body, or a regex capture group; a value that doesn't turn up fails the step rather than quietly binding nothing. Neither request knows it is in a flow — they are the same files the Requests tab sends, carrying the same assertions.
A test set lists tests that each run one request or one whole flow, plus variables that apply to the entire run. Results stream into the Testing tab as they land, each row expanding to the request that ran, every assertion verdict, and the values a step bound.
The same runs go headless through a separate .NET tool:
dotnet tool install --global Tap.Studio.Cli
tap-studio test "Demo API smoke" # a test set, a flow, by name or path
tap-studio test --tag smoke --output junit # everything carrying a tag
tap-studio send "Create customer" # one request + its assertions
tap-studio lint # what doesn't parse
tap-studio vars --env ci # the resolved cascade, secrets maskedtap-studio is a different package from the tap tunnel CLI — different product, different
command, both installable side by side. Both it and the Studio's API call Tap.Execution, so
a verdict from a pipeline and a verdict from the Testing tab are the same computation over the
same files. --output takes junit, trx, json, or markdown; exit 1 means a test
failed, while 2/3/4 mean usage, workspace, and auth problems — so a red build caused by
an API is distinguishable from one caused by a broken runner. A selection matching nothing is
an error, never a green run over zero tests.
This repo runs its own sample set that way on every push — see
.github/workflows/workspace-tests.yml. Full guide:
docs/studio.md ·
CLI reference.
Studio ships as a native desktop app (Tauri 2 wrapping the self-contained Tap.Studio
sidecar). Grab the .dmg, .msi/.exe, or .deb from
Releases; it self-updates from there.
From source, the whole dev loop is one command:
cd samples
aspire runThat brings up demo-api (an upstream exercising every verb, content type, SSE, WebSockets,
GraphQL, and a real OAuth2/OIDC server), studio-api, and the Vite UI on port 5297. Point it
at your own repo with STUDIO_WORKSPACE=/path/to/your/repo aspire run, and add
RunDesktop=true to open the native window too.
dotnet restore Tap.slnx
dotnet build Tap.slnx
dotnet run --project samples/Sample.AppHost # tunnels + inspector scenarios
cd samples && aspire run # Tap Studio
dotnet test src/backend/Tap.Tests/Tap.Tests.csproj -p:SkipStudioUiBuild=trueThe SDK is pinned in global.json to .NET 10 and TreatWarningsAsErrors is on globally, so
warnings break the build. The tests cover the workspace parser/emitter round-trips (requests,
assertions, flows, test sets), the assertion evaluator, the response-value extractor, and the
CLI's variable inputs, target resolution, and report writers — all pure functions, no AppHost
needed. The skip flag avoids a full Vite build on every run.
Two independent UIs, both yarn 4 (Berry):
cd src/ui-inspector && yarn && yarn dev # Inspector — port 5197
cd src/ui-studio && yarn && yarn dev # Studio — port 5297
cd docs-site && yarn && yarn build # landing page + docssrc/ui-inspector is built into src/backend/Tap.Server/wwwroot/ on every server build, and
src/ui-studio into Tap.Studio's wwwroot. Skip those with -p:SkipTapUiBuild=true and
-p:SkipStudioUiBuild=true when iterating on C# only. The generated wwwroot directories are
gitignored — never hand-edit them.
assets/ Logo, hero art, and documentation screenshots
docs/ Reference documentation
docs-site/ Vite landing page + docs, published to GitHub Pages
src/backend/Tap.Core/ Shared auth and Cloudflare/cloudflared primitives
src/backend/Tap.Hosting/ Aspire integration and lifecycle hooks
src/backend/Tap.Server/ Capture server, YARP proxy, SSE/WS API, bundled Inspector UI
src/backend/Tap.Cli/ CLI host for the inspector server
src/backend/Tap.Studio/ Studio backend (REST + SSE, auth runner, AI, git)
src/backend/Tap.Studio.Cli/ `tap-studio` dotnet tool — headless test runs for terminals and CI
src/backend/Tap.Execution/ Execution engine: send, auth, run flows and test sets
src/backend/Tap.Workspace/ Workspace parsing, variable providers, and rendering
src/backend/Tap.Tests/ xunit v3 tests for the parsers, evaluators, and the CLI
src/ui-inspector/ Vite + React Inspector UI
src/ui-studio/ Vite + React Studio UI
src/desktop/ Tauri desktop shell for Studio
samples/ Sample AppHosts, the demo API, and a sample workspace
| Document | Contents |
|---|---|
| docs/inspector.md | Tunnel modes, full CLI reference, Cloudflare and Tailscale setup, proxy auth, Aspire recipes, configuration. |
| docs/studio.md | Workspace model, request composer, authentication flows, variables and secrets, AI assistant, git, desktop app. |
| docs/workspace-format.md | The authoritative on-disk format spec for Studio workspaces. |
| docs/ARCHITECTURE.md | Deep technical background on the capture path and tunnel providers. |
| src/backend/Tap.Studio.Cli/README.md | The tap-studio CLI: commands, selection, report formats, exit codes, headless auth. |
| src/desktop/README.md | Desktop shell internals, build, signing, and release pipeline. |
| docs/release-notes/ | Per-release notes. |
TBD.





