Skip to content

Network model (Tailscale)

Carabase Host is designed around zero public ports. The desktop client reaches the host across a private mesh network that only your devices can see — by default, that mesh is Tailscale.

This page covers:

  1. Why Tailscale (and what happens if you skip it)
  2. Installing it on the machine running the host
  3. Connecting the desktop client
  4. The localhost-only fallback for local-loop development

The security model assumes the host is never reachable from the public internet. The HTTP listener doesn’t speak TLS, doesn’t require auth on /api/v1/health, and isn’t designed to fend off the kind of background scanning every IPv4 address gets. Putting the host directly on a public IP is not a supported configuration — credentials would leak within hours.

Tailscale gives us a WireGuard-based overlay network where:

  • Every device joins the same “tailnet” with a single login (Google/GitHub/Microsoft/email)
  • Devices get stable hostnames (MagicDNS) and IPs that don’t change
  • Traffic is end-to-end encrypted between devices, not just to a relay
  • No port-forwarding, no DDNS, no public DNS records

It’s free for personal use (1 user, 100 devices) and runs as a small daemon.

On macOS:

Terminal window
brew install --cask tailscale
open -a Tailscale

The Tailscale app prompts you to sign in — pick whichever identity provider you want this tailnet to live under. After login, the menubar icon turns blue.

In a terminal:

Terminal window
tailscale status

You’ll see a line like:

100.123.45.67 my-mac-mini tailnet-name@ macOS -

The my-mac-mini part is your MagicDNS hostname. Combined with your tailnet’s domain (shown in the Tailscale admin console), it becomes a fully-qualified name like my-mac-mini.tailnet-name.ts.net.

That’s the address the desktop client uses. Either form works:

  • http://my-mac-mini:3000 — short MagicDNS name (works between devices on the same tailnet)
  • http://my-mac-mini.tailnet-name.ts.net:3000 — fully qualified (more explicit)
  • http://100.123.45.67:3000 — raw tailscale IP (works but breaks if the host’s IP changes)

The host defaults to HOST=:: in every .env.<env>.example, which is dual-stack (IPv4 + IPv6) and accepts connections on every interface — including the Tailscale virtual interface. Don’t change this to 127.0.0.1 unless you genuinely want a local-loop-only host (see the fallback section below).

Verify the host is listening on the Tailscale interface:

Terminal window
lsof -i :3000 | grep LISTEN
# Look for entries on the tailscale0 interface or a wildcard (*) bind

On the device that will run the desktop client:

  1. Install Tailscale and sign in with the same identity you used on the host
  2. tailscale status should now list both devices
  3. Open the desktop app → Settings → Host Connection → enter the host URL (e.g. http://my-mac-mini:3000)
  4. The connection indicator should go green within a few seconds

If you see a timeout: try the raw Tailscale IP instead of MagicDNS — MagicDNS resolution can take a moment after a fresh install.

Tailscale supports ACLs (access control lists) at the tailnet level. For a single-user setup with one host + a few clients, the default “anyone in this tailnet can reach anyone else” is fine. If you start sharing the tailnet with collaborators or family, write an ACL that restricts port 3000 on the host to specific tagged users.

See the Tailscale ACL docs — out of scope here.

If you don’t want Tailscale, you can run Carabase in local-loop-only mode where the host is only reachable from the same machine:

Terminal window
# In .env.dev:
HOST=127.0.0.1
PORT=3000

This works for the Admin SPA at http://localhost:3000/admin/ and for any local development.

It does not work for the desktop client — the desktop is a separate macOS process that needs to reach the host across a network boundary. With localhost-only binding, the only way the desktop can reach the host is if it’s running on the same machine, which defeats most of the point.

If you go this route:

  • The “no public ports” guarantee is satisfied trivially (the listener is unreachable from anywhere except 127.0.0.1)
  • The Admin SPA is still your full configuration surface — every connector, OAuth app, and sync rule is reachable
  • Background workers (harvest, sync, dreams, curation) run normally — they’re in-process, not network-bound

This is a perfectly valid mode for headless installations, server deployments where you SSH-tunnel into the box, or developers who never use the desktop client.

You probably don’t, but: put a reverse proxy (Caddy, nginx, Cloudflare Tunnel) in front of the host with TLS + a separate auth layer. Carabase’s own routes do not implement public-internet authentication — they assume the network boundary is the auth layer. Adding TLS termination is necessary but not sufficient.

This is unsupported. The single-tenant assumption (one workspace per host, one user per host) doesn’t translate to a public deployment, and there’s no rate limiting, no per-user authentication, and no audit logging on the routes themselves.

If you’re trying to share access with another person, the right answer is to add their device to your tailnet — not to expose the host publicly.