dev.site puts links and private TCP services on one developer profile.
Services stay on the machines that own them. An approved user maps a service onto a local
loopback port with the devsite CLI.
The control plane handles identity, profiles, sharing, and short-lived capabilities. It never carries service traffic. Iroh establishes direct or relayed connectivity between devices, so there is no public application listener or inbound TCP port to configure. Services are protocol-agnostic TCP byte streams; they are not assumed to be websites.
macOS with Homebrew:
brew install FelineStateMachine/tap/devsiteLinux x86-64/ARM64, Windows x86-64, and Apple Silicon archives are attached to the
latest GitHub release.
Each release includes SHA-256 checksums and GitHub build-provenance attestations. The CLI
is one binary; devsite daemon run is its long-running host mode.
Sign in at dev.site, create a single-use machine ticket on the dashboard, and use it to bind this machine's Ed25519 identity:
devsite login dmt_...Host a named local service and keep the daemon running:
devsite service host 5432 --name postgres
devsite daemon runAccept a share on the dashboard, open its profile entry, and choose Get ticket. On the connecting machine:
devsite connect dst_...
# connected to postgres (https://dev.site/s/res_...)
# listening on 127.0.0.1:43127local application
│ TCP to 127.0.0.1:ephemeral
▼
devsite connect
│ capability-gated Iroh QUIC stream
▼
owner's devsite daemon
│ TCP to 127.0.0.1:PORT
▼
private service
dev.site control plane ── identity, profiles, approved shares, capability signing
The control plane never carries service bytes and never learns the local port. It stores resource metadata and permissions, mints browser-requested connection tickets, then signs one short-lived capability per connection. The daemon verifies the signature, daemon audience, client endpoint key, resource id, expiry, and one-use nonce before opening the configured loopback target.
The transport ALPN is devsite/tcp/1.
Concept guides explain the security boundaries and lifecycle in detail:
Services default to private:
devsite service host 3000
# hosting port 3000 → 127.0.0.1:3000 (private)
# https://dev.site/s/res_...Names and folders are presentation. Hosted ports enter the Services folder unless a
different folder is named:
devsite service host 5432 --name postgres --folder databasesSharing remains per service. A recipient must approve the invitation on their dashboard:
devsite service host 6379 --name redis --folder databases --share @bobAn approved user opens the service's profile entry on dev.site, clicks Get ticket, and passes the single-use ticket to the CLI. The connecting machine does not need a saved login:
devsite connect dst_...
# connected to postgres (https://dev.site/s/res_...)
# listening on 127.0.0.1:43127Choose a stable local port when an application expects one:
devsite connect dst_... --listen 127.0.0.1:15432
psql -h 127.0.0.1 -p 15432The connector refuses non-loopback listeners. Stop hosting a service with:
devsite service remove postgresLinks default to private, can be shared for recipient approval, or made public:
devsite link set --name runbook --url https://example.com/runbook
devsite link set --name staging --url https://staging.example.com --share @bob
devsite link set --name klot.ski --url https://klot.ski --public --folder GamesSetting an existing link or hosting an existing service prints the visibility, recipient, destination, and folder changes before applying the upsert. Changing a shared link's URL requires its recipients to approve the new destination again.
devsite daemon run is the portable foreground service entrypoint. A service manager only
needs to keep that command alive:
devsite daemon runEvery command accepts --json for automation. Finite commands emit one success or error
object; connect and daemon run emit newline-delimited lifecycle events. JSON mode never
prompts. Command-specific --help is available as structured JSON, and failures include a
recovery suggestion. The complete stdout and exit-code contract is in
docs/json-output.md.
Inspect remote resources together with this machine's hosting state, validate a mutation without applying it, or diagnose configuration and control-plane drift:
devsite resources list --json
devsite service host 5432 --name postgres --share @bob --plan --json
devsite doctor --json--dry-run is an alias for --plan. Plans make no remote or local changes. A portable
agent skill that teaches these intent-based workflows through the CLI's JSON contract lives
at skills/devsite-cli/SKILL.md; it has no dependency on a
particular model or agent harness.
A sandboxed worker can request temporary service access without receiving a logged-in machine's credential or endpoint key:
# Requester: keep requester.key private; send only request.json to the granting party.
devsite access request postgres --request request.json --key requester.key --json
# Granting party: requires a machine ticket enrolled with service_grants:issue.
devsite access resolve postgres --json
devsite access grant --request request.json --plan --json
devsite access grant --request request.json --approved-plan dsp_... --json
# Requester: use the server and dss_ grant returned by the granting party.
devsite --server https://dev.site access connect dss_... --key requester.key --jsonThe request signs the service keyword, requester endpoint, request id, and expiry. The
granting party resolves that intent only among services its account may access, signs the
chosen resource and expiry, and receives a short-lived session bound to the requester's
endpoint. Requests expire within 10 minutes; sessions within 15 minutes. Replaying an
issued request id is rejected, while the resulting session can authorize multiple TCP
connections during its lifetime. Apply requires the server-signed dsp_… ticket returned
by the exact reviewed plan, so changing the request, resource, endpoint, or expiry after
approval is rejected. The dashboard exposes grant authority as an explicit
service_grants:issue scope on the single-use machine enrollment ticket, and revoking that
machine credential removes the grant sessions it issued.
The optional plugins/devsite-access bundle packages the same
harness-neutral skill with a local MCP stdio adapter. The adapter implements MCP
2026-07-28 discovery and request-scoped metadata: there is no initialize exchange,
negotiated session, or call-order state. Any compatible MCP client can use the adapter; the
plugin manifest is only a packaging layer. See delegated access for
the trust model and integration contract.
Homebrew installs can supervise it at login on macOS or Linux:
brew services start devsiteNative Linux packages may install packaging/systemd/devsite.service as a user unit:
systemctl --user enable --now devsite.serviceVersion 0.6.0 uses devsite-endpoint.key and devsite-endpoint.pub. It does not read or
move legacy identity.key and identity.pub files. Start version 0.5.1 once before the
upgrade if these files remain. See Keys and endpoint identities
for the migration rules. The daemon registers that public endpoint id with the control
plane at startup. It publishes its address through Iroh and reloads authorizations every
two seconds.
Adding or removing a service does not require a restart.
devsite daemon status reports whether this config directory has a live daemon. The same
liveness appears in devsite status alongside the config location, pinned signing key, and
locally served ports. The lock is released by the operating system on exit, so a crash does
not leave a stale running state.
A profile is a list of ordinary links and TCP services. Folders are repeated labels on entries and exist only to group the UI; they are not authorization containers. Accepted shares appear as ordinary rows in those folders with their owner noted, not in a separate top-level sharing section. Every site retains its own visibility, invitation state, and revocation; services additionally enforce access for every tunnel connection.
The signed-in homepage is the dashboard. It manages approved and pending shares, revocable
machine credentials, the private-only profile setting, and logout. Profile presentation
remains a bounded list of approved declarations rather than arbitrary CSS: Pico variables
control the theme, while --devsite-folders, --devsite-open-folders, and
--devsite-folder-order control the initial semantic folder layout.
Profile routes load a static shell and one viewer-specific HTML stream. Fixi starts the request. SSEXi reads each event, and Paxi morphs each profile fragment into the page. The control plane sends a new fragment after profile data changes. The browser keeps the reader's folder state during each morph.
Prerequisites: stable Rust and Node.js 24.
The control plane uses the n0 relay preset by default.
Set IROH_SERVICES_API_SECRET to add endpoint-scoped Iroh Services tokens.
Set DEVSITE_RELAY_URLS to a comma-separated list to use custom relays.
The control plane sends the configured URLs and scoped token to each endpoint.
npm ci
npm run build:web
export DEVSITE_PUBLIC_ORIGIN=http://127.0.0.1:4000
cargo run -p devsite-serverShoo is the default OIDC login adapter. A fork can use any public OIDC client without
changing application code by setting DEVSITE_OIDC_ISSUER,
DEVSITE_OIDC_AUTHORIZATION_ENDPOINT, DEVSITE_OIDC_TOKEN_ENDPOINT,
DEVSITE_OIDC_JWKS_URI, and DEVSITE_OIDC_CLIENT_ID. The server owns authorization-code
exchange, PKCE, and token verification; the browser only enters through /auth/start.
See Authentication providers for OIDC replacement, identity
continuity, and implementing a non-OIDC ExternalIdentity adapter.
For operator-only setup, mint a local browser session:
cargo run -p devsite-server -- issue-session aliceBuild and test everything:
npm run check:web
npx playwright install chromium
npm run test:web
cargo test --workspace
cargo build --release -p devsite-cliThe control plane runs as one Fly machine with one attached volume and SQLite behind a
mutex. fly.toml and Dockerfile describe it:
fly deployThe traffic path scales independently because it does not pass through Fly. Iroh carries end-to-end encrypted QUIC directly when possible and through a relay when necessary; the control plane only signs authorization metadata.
The durable boundaries are the OIDC (issuer, subject) identity pairs and
DEVSITE_SIGNING_KEY, whose public half every daemon pins at login. Shoo derives its
default client id and pairwise subjects from DEVSITE_PUBLIC_ORIGIN; changing that origin
is therefore still intentionally disruptive unless the replacement provider preserves
the configured issuer, client, and subjects.
| Path | Responsibility |
|---|---|
crates/devsite-proto |
Opaque ids, signed capabilities, and the TCP stream handshake. |
crates/devsite-iroh |
Shared n0 and custom relay configuration. |
crates/devsite-client |
Native Iroh client endpoint and authorized service streams. |
crates/devsite-daemon |
Capability verification and fixed-target TCP forwarding. |
crates/devsite-cli |
Login, links, service hosting/connect, themes, and daemon lifecycle. |
crates/devsite-server |
Axum/SQLite control plane and capability issuance. |
web/ |
Semantic HTML, strict TypeScript, Fixi, Paxi, SSEXi, Pico CSS, and vendored fonts. |
- A peer supplies only a signed capability. It never supplies the daemon's target address.
devsite service host PORTalways stores127.0.0.1:PORT; port zero is rejected.devsite connectlistens only on a loopback address.- Browser-minted connection tickets are short-lived, stored only as hashes, and consumed once. Redemption creates a client-key-bound session; its raw credential exists only in CLI memory, while the control plane stores its hash until expiry or disconnect.
- Capabilities are bound to the authenticated Iroh client endpoint and cannot be replayed by another endpoint.
- When configured, the control plane keeps the Iroh Services key and gives each endpoint a scoped relay token.
- Each capability opens one stream and its nonce is consumed once.
- Unknown resources, invalid signatures, wrong audiences, wrong clients, expiry, and replay all produce the same denial.
- Service identifiers are locators, not bearer credentials. Visibility and accepted-share state are checked whenever a new capability is requested.
- The control plane rate-limits capability issuance and bounds profiles, names, links, folders, credentials, and share lists.
- Plaintext machine enrollment tickets are consumed once. Enrollment binds the resulting machine credential to the machine's Ed25519 endpoint identity.
- Machine credentials and browser sessions are stored only as SHA-256 hashes and can be revoked from the dashboard. Revocation also closes affected active service streams.
Licensed under the Apache License, Version 2.0.