# Funnel vs Serve — when to use which

Both `tailscale serve` and `tailscale funnel` are layered on the same `tailscaled` listener. They share state but have different visibility.

## Visibility

| Command | URL format | Who can reach it |
|---|---|---|
| `tailscale serve --bg --set-path=/foo 3011` | `http://<node>.<tailnet>.ts.net/foo` (or `https://...` on 443) | Only devices on the user's tailnet (signed in to Tailscale) |
| `tailscale funnel --bg --set-path=/foo 3011` | `https://<node>.<tailnet>.ts.net/foo` | Anyone with the link, anywhere on the internet |

HTTPS is mandatory for Funnel because the public routing has no LAN context. Serve can run plaintext HTTP on the tailnet since it's already a VPN tunnel.

## Path semantics

- `--set-path=/foo` mounts at `/foo` and proxies to the local port. The path must start with `/`.
- Paths are **additive** within a listener. Adding `/foo` does NOT break `/bar` or `/`.
- Each node has its own listener; the same path on two different nodes won't collide.
- `tailscale serve reset` and `tailscale funnel reset` clear **everything** (nuclear option).

## Common mistakes

1. **`--funnel` is not a flag on `tailscale serve`.** The error is:
   ```
   flag provided but not defined: -funnel
   ```
   Use the `tailscale funnel` command instead. The two commands are siblings, not parent/child.

2. **Path with no leading slash.** `--set-path=foo` silently fails — the listener doesn't register it and requests return 404. Always use `--set-path=/foo`.

3. **Path with trailing slash mismatch.** If the local server returns 301 from `/foo` to `/foo/`, the public URL needs the trailing slash. Test with `curl https://<node>.<tailnet>.ts.net/foo/` to disambiguate.

4. **Confusing port semantics.** The final argument to `tailscale funnel` is the **local target port**, not a public port. The public port is fixed at 443 (and optionally 8443) by the Funnel listener.

5. **Public DNS propagation.** The Tailscale coordination server publishes the Funnel entry to the public DNS within a few seconds, but TTL / client caching can mean a fresh URL takes 30-60s to resolve from a non-tailnet client. Test from the same machine or from another tailnet device first.

6. **ACL blocks.** Tailscale node attributes must include `funnel` for the user's account. Run `tailscale debug` if you get 403 / connection refused. The user's current node has it enabled by default; new nodes added later may not.

## Path ownership

Common siblings on the user's `miopenclaw-vnic` node — do not collide:

| Public path | Local | Owner |
|---|---|---|
| `/` | 5000 | main root |
| `/led/` | 8999 | LED controller |
| `/hevy-api/` | 8420 | Hevy bridge |
| `:8443/` | 3017 | health-bridge public proxy |
| `:8443/hevy-api/` | 8420 | (duplicate entry on alternate port) |
| `/findings/` | 3010 | decision-support dashboard (tailnet-only, sensitive) |

Ad-hoc previews go in the 3010-3020 range. Use the `scripts/check-port.sh` helper.

## When the tailnet's HTTPS cert is broken

If `tailscale cert status` returns `500 Internal Server Error: invalid domain` (or similar), the HTTPS front-end of Tailscale serve will fail even though `tailscale serve` accepts the rule. Symptoms:

- `curl https://<node>.<tailnet>.ts.net/<path>/` SSL-handshake-fails
- `curl http://<node>.<tailnet>.ts.net/<path>/` returns 302 → `https://...` then fails
- Tailscale force-HTTPS-es at the node level even for `--http=80` rules

For **public** Funnels this is rare (HTTPS provisioning is more reliable for the well-known Funnel ports). For **tailnet-only** Serve, the cert provisioning is more often broken.

**Three options, in order of preference:**

1. **Skip Tailscale serve entirely — bind the local server to the Tailscale IP directly.** Most reliable. See Step 5 in the main SKILL.md. Works for tailnet-only access. URL: `http://100.87.116.90:<port>/` or `http://<node>:<port>/` via MagicDNS. Connection is encrypted at the WireGuard layer.

2. **Use `--http=80` to put the rule in the HTTP section.** May still 302 to HTTPS at the node level. See Step 6 in the main SKILL.md. Use only if Option 1 isn't viable.

3. **Fix the tailnet HTTPS config** (admin action). Tailscale admin console → DNS → "Enable HTTPS" or similar. After enabling, cert provisioning can take a few minutes. Verify with `tailscale cert status <hostname>`.
