localhost behave like the laptop were the boxes you are
looking at.
tesser dev, use, and review start it in the background when it is not
running, and tesser daemon install makes it a login service (see
the daemon). It listens on port 3000 for its own
panel and status page, and on every port any of your boxes declares or
depends on. A port means that port everywhere: nothing is ever redirected to
some other port of some other box.
Every box has an address
tesser dev prints, with the port the service listens on
(3000 for a typical Next.js app). Browsers resolve *.localhost to your own
machine without any configuration, so two tabs on two box ids show two
branches side by side. Every port a box declares is a door —
<box_id>.localhost:9229 for a debugger port, say. A port the box does not
declare answers 404.
A box’s dependency ports are doors as well. If a frontend box’s manifest says
5000 = "api", then <box_id>.localhost:5000 is the api that frontend is
talking to: the box you wired it to, or the org’s shared instance, whichever
its routing table says right now. When that backend is one of your own boxes
the request rides the tunnel the laptop already holds to it; otherwise the
daemon opens one through the frontend box’s own loopback port. A dep with
nothing behind it answers 503 and names the tesser wire command that fixes
it.
From a box
The same address works on the boxes themselves. Code running on any of your boxes can openhttp://<box_id>.localhost:<port> for any other box in the
org and reach that box’s declared port, whether or not the two are wired
together. It works for a browser or curl on the box too, which never ask the
resolver about *.localhost and dial plain loopback with the name in Host:
on a port some other box declares, that connection is routed by the name, the
way the laptop does it. Nothing is bound on the box for this, and anything
that listens on the box wins its port, whether it started before or after:
the box’s own ports, its dependency ports, a database a test brought up. So
from a browser on a box, a peer’s copy of a port something on this box is
listening on reaches this box, not the peer. The other way round, a port some
other box declares accepts a connection on this box’s loopback even when
nothing local is listening, so a raw TCP probe (nc -z, wait-on tcp:)
cannot tell that port is free; probe over HTTP instead. A port the other box
does not declare is refused immediately. This includes the
shared instances of main: a shared box can call back into a dev box that
handed it its address, which is how a dev box registers for webhooks. The
run command gets that address as TESSER_BOX_URL (and the id as
TESSER_BOX_ID), so a run script does not need to work it out.
A name that follows the shared instance
A box id names one box for as long as it lives. On a box,<service>.localhost:<port> names whichever box is currently the org’s
shared instance of that service, so it survives a promote: the name moves to the new box and the superseded
one drains. Use it when the other side is meant to be the shared instance
rather than a particular box, such as a webhook subscription that should
keep working across deploys. In-flight connections are not moved; a new
connection after the promote lands on the new box.
Bare localhost is the focused boxes, port by port
Selecting a box selects its entire recursive dependency graph. Each box brings its declared ports and dependency ports. Shared dependencies are included even when you do not have a local target or SSH key for them: the daemon tunnels through one of your registered boxes using its access to the box mesh. For a dashboard wired to a server, where the server uses hp and a worker:
The last explicit selection wins for every port in its subtree. Unrelated
ports remain where they were. Remove a selection to reveal the previous
selection underneath it. A missing binding fails with an explanation; it
does not silently fall back to an older selection. Shared nodes and cycles
are visited once per selected root.
This changes your laptop’s routes. Dashboard A’s remote server process still
calls the backend wired to A. To browser-test a backend-only branch B, run a
personal dashboard B and wire it to backend B, even if its frontend code is
unchanged. See Wiring a dependency.
Databases and other TCP
Anything that speaks on a port works. A dependency that is a database is reachable from laptop tooling on barelocalhost:5432 just as the box
reaches it: HTTP on a door is routed by name, everything else is passed
through as bytes.
Cookies and OAuth callbacks
Moving the focus does not change the bare address, so an OAuth callback registered forlocalhost:3000 keeps working as you move between branches.
Cookies stay with the address too, exactly as they would for two local copies
of your app on one port: if each box keeps sessions in its own database, the
next box will ask you to sign in again. A box’s own address has its own
cookies, so a tab opened there keeps a sign-in per box. When the focus moves,
the proxy closes the old box’s connections on the ports that moved, so a
hot-reload socket that was talking to one branch does not keep feeding the
page after you have moved to another; pages on ports that did not move are
not touched.
The panel
The square in the bottom-right corner opens the panel (⌘⇧U), which lists
the branches serving this port; hover it to see which branch you are on.
“Hide until tesserd restarts” at the bottom of the panel takes the square off
every page; ⌘⇧U still opens the panel, and expired AWS credentials still
show.
Choose a branch to select the service serving this page and its complete
dependency graph. Selecting the current branch again restores its graph after
overrides.
A tab keeps the kind of address you opened it on. On a box’s address,
choosing a branch moves the tab to that branch’s box,
http://<box_id>.localhost:<port>, on the same path; every box keeps its own
cookies there, so a sign-in on one branch survives a visit to another. On
bare localhost the address stays and the page reloads onto the chosen
branch.
A service whose manifest sets box_address = true never stays on bare
localhost: a page you open there moves to the box that port shows,
http://<box_id>.localhost:<port>, on the same path. Only page loads move:
API calls and sockets to bare localhost go where they always did.
Ports that fail to connect are listed under the branches.
Bare localhost follows the choice either way, so a URL your app has baked
in (http://localhost:8080 for its API, say) reaches the branch you are
looking at.
To a browser, a page on a box’s address and an API on bare localhost are
different sites, and it will not send that API’s cookies. If your frontend
calls its API at a hardcoded http://localhost:<port> and signs in with
cookies, point it at http://$TESSER_BOX_ID.localhost:<port> instead and set
box_address = true, or serve the API through the dev server’s proxy so the
page only talks to its own origin.
From a terminal
From a terminal:tesser dev joins the selection at the bottom, taking unclaimed ports.
Re-running an existing box preserves its priority. Starting a service does
not rewire its dependencies or launch other services.
Over Tailscale
The daemon answers requests from your own machine and from Tailscale peers, so a phone or a second laptop on your tailnet can open the proxy’s address and see the same focus. It refuses requests from the rest of your LAN unless you start it with--allow-lan.