A tailnet line from your phone to every live office.
Each office already has a private control API, but its loopback port and bearer token rotate at every boot. floorgate is the stable front door: it discovers projects under ~/.theboringfloor/projects/, finds the live offices, and proxies the right action. Your phone needs one gateway URL and one long-lived token, not a changing address for every project.
Start one stable front door for this machine.
Run floorgate on the machine where your offices run. It listens on 127.0.0.1:8787 by default. Use --bind or THEFLOOR_GATE_BIND when the gateway must listen on another address.
# default: local machine only
floorgate
# bind to a Tailscale address for the Android app
floorgate --bind <tailnet-ip>:8787
A non-loopback bind is supported, and floorgate prints a warning to stderr when you use one: the bearer token is then the only protection at the gateway.
Mint once, then paste it into the app.
On its first run, floorgate mints a long-lived token at ~/.theboringfloor/configs/gateway.json. The file is mode 0600. Print the token when you need to paste it into the Android app.
floorgate --print-token
Put both devices on one encrypted tailnet.
Install Tailscale on the computer running floorgate and on the Android phone. Sign in and join both devices to the same tailnet. On the computer, find its tailnet IPv4 address, then bind the gateway to that address.
# on the computer running the offices
tailscale ip -4
# replace <tailnet-ip> with that output
floorgate --bind <tailnet-ip>:8787
In the app's Settings screen, use http://<tailnet-ip>:8787 as the gateway URL and paste the token from floorgate --print-token. Tailscale encrypts transport over WireGuard inside your tailnet. The gateway has no TLS of its own. Binding it to a public interface without a tunnel is not supported.
One authenticated API for every discovered project.
Every route requires Authorization: Bearer <token>. Errors are {"error":"..."}. A project id is the SHA-1 of its absolute path, so it remains stable across reboots. A Project is {"id","dir","name","live","backend","primaryId","port","version","savedAt","chatCount"}. Transcript at values are Unix milliseconds.
| Method path | What it does | Response |
|---|---|---|
| GET /api/v1/health | Checks that the gateway is running. | {"ok":true,"version":"..."} |
| GET /api/v1/projects | Lists every discovered project on this machine. | {"projects":[Project,...]} |
| GET /api/v1/projects/{id} | Reads one discovered project. | Project |
| GET /api/v1/projects/{id}/status | Reads live office status and transcript counts. | {"dir","backend","primaryId","planDraftLen","planApprovedLen","chatCount"} |
| GET /api/v1/projects/{id}/busy | Reads whether the office is working or waiting. | {"busy","pendingBoss","thinking","delegating","questionParked"} |
| GET /api/v1/projects/{id}/transcript?limit=N | Reads recent transcript messages; limit must be 0–500. | {"messages":[{"id","from","kind","text","at"}],"truncated":bool} |
| POST /api/v1/projects/{id}/message {"text":""} | Sends text to the live office. | {"ok":true} |
| POST /api/v1/projects/{id}/stop | Stops the current work in the live office. | {"ok":true} |
| POST /api/v1/projects/{id}/new | Starts a new session in the live office. | {"ok":true} |
Error codes
| Status | Meaning |
|---|---|
| 400 | Bad request. |
| 401 | Unauthenticated. |
| 404 | Unknown project. |
| 409 | Project is known, but no office is running. |
| 502 | The office returned an error. |
| 504 | The office timed out. |
Copy and paste examples
curl -H "Authorization: Bearer <token>" http://<tailnet-ip>:8787/api/v1/projects
{"projects":[{"id":"7f4a...","dir":"/Users/me/code/atlas","name":"atlas","live":true,"backend":"opencode","primaryId":"ses_123","port":43721,"version":"0.4.0","savedAt":1740000000000,"chatCount":42}]}
curl -H "Authorization: Bearer <token>" "http://<tailnet-ip>:8787/api/v1/projects/7f4a.../transcript?limit=20"
{"messages":[{"id":"msg_1","from":"member","kind":"chat","text":"Check the failing test.","at":1740000000123}],"truncated":false}
curl -X POST -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d '{"text":"Run the focused test again."}' http://<tailnet-ip>:8787/api/v1/projects/7f4a.../message
{"ok":true}
curl -X POST -H "Authorization: Bearer <token>" http://<tailnet-ip>:8787/api/v1/projects/7f4a.../stop
{"ok":true}
Three screens, scoped to the live office.
Projects list. Shows the discovered projects and refreshes every 5 seconds.
Session detail. Shows the transcript, a message composer, and Stop and New controls. The transcript polls every 3 seconds. A remote stop leaves remote: stopped current work in your terminal transcript; a remote new session leaves remote: started a new session.
Settings. Stores the gateway URL and token and offers a test-connection action.
Download the release APK, then install it on your phone.
The Android app ships as a signed release APK attached to the GitHub release, not through an app store. To install without a cable, open that release page on the phone and download the APK there.
Android will warn about installing from an unknown source. Allow the browser or file manager you used to download the APK under Settings → Apps → Special app access → Install unknown apps, then install the downloaded file.
With the phone plugged in and Android Debug Bridge available, install an upgrade from the computer instead.
adb install -r <path-to-apk>
A stable self-signed key keeps upgrades in place.
The APK is signed with a self-signed certificate, not a Play Store key. Android cannot vouch for the publisher, which is why it warns about the install.
Before installing, compare the certificate fingerprint of the APK with the published fingerprint on the release page. For v0.4.0 the SHA-256 is 3d0c9f22464659ccaac8f2a28d7c91ed7e53570ad84dd7e38caa19ea591eb3cb.
The signing key is stable, so a new APK installs over the previous version. If the key ever changes, Android requires you to uninstall the old app first. Uninstalling clears the saved gateway URL and token.
The gateway has no TLS on its own. Use it over a Tailscale tailnet, where WireGuard encrypts transport. Binding it to a public interface without a tunnel is not supported.
The APK is self-signed. Android cannot vouch for the publisher, and there is no Play Store, F-Droid, or iOS distribution.
There is no auto-update. Download and install each new APK release.
The app stores the gateway URL and bearer token unencrypted in local app preferences.
The app polls for projects and transcript updates. There is no push.
Remote transcript reads are capped at the last 200 messages because that is what the office persists.
Answering a permission prompt or a member question from the phone is not supported in v1. Those still require the terminal.
