Skip to content

Repository files navigation

dev.site

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.

Install

macOS with Homebrew:

brew install FelineStateMachine/tap/devsite

Linux 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.

Quick start

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 run

Accept 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:43127

Shape

local 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 and links

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 databases

Sharing remains per service. A recipient must approve the invitation on their dashboard:

devsite service host 6379 --name redis --folder databases --share @bob

An 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:43127

Choose 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 15432

The connector refuses non-loopback listeners. Stop hosting a service with:

devsite service remove postgres

Links 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 Games

Setting 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.

Daemon

devsite daemon run is the portable foreground service entrypoint. A service manager only needs to keep that command alive:

devsite daemon run

Every 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.

Delegated agent access

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 --json

The 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 devsite

Native Linux packages may install packaging/systemd/devsite.service as a user unit:

systemctl --user enable --now devsite.service

Version 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.

Profiles and folders

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.

Local development

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-server

Shoo 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 alice

Build and test everything:

npm run check:web
npx playwright install chromium
npm run test:web
cargo test --workspace
cargo build --release -p devsite-cli

Deployment

The control plane runs as one Fly machine with one attached volume and SQLite behind a mutex. fly.toml and Dockerfile describe it:

fly deploy

The 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.

Crates

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.

Security boundaries

  • A peer supplies only a signed capability. It never supplies the daemon's target address.
  • devsite service host PORT always stores 127.0.0.1:PORT; port zero is rejected.
  • devsite connect listens 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.

License

Licensed under the Apache License, Version 2.0.

About

Links and private TCP services on one developer profile, connected with capability-gated Iroh QUIC.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages