1# dev.site23[dev.site](https://dev.site) puts links and private TCP services on one developer profile.4Services stay on the machines that own them. An approved user maps a service onto a local5loopback port with the `devsite` CLI.67The control plane handles identity, profiles, sharing, and short-lived capabilities. It8never carries service traffic. Iroh establishes direct or relayed connectivity between9devices, so there is no public application listener or inbound TCP port to configure.10Services are protocol-agnostic TCP byte streams; they are not assumed to be websites.1112## Install1314macOS with [Homebrew](https://brew.sh):1516```bash17brew install FelineStateMachine/tap/devsite18```1920Linux x86-64/ARM64, Windows x86-64, and Apple Silicon archives are attached to the21[latest GitHub release](https://github.com/FelineStateMachine/devsite/releases/latest).22Each release includes SHA-256 checksums and GitHub build-provenance attestations. The CLI23is one binary; `devsite daemon run` is its long-running host mode.2425## Quick start2627Sign in at [dev.site](https://dev.site), create a single-use machine ticket on the dashboard,28and use it to bind this machine's Ed25519 identity:2930```bash31devsite login dmt_...32```3334Host a named local service and keep the daemon running:3536```bash37devsite service host 5432 --name postgres38devsite daemon run39```4041Accept a share on the dashboard, open its profile entry, and choose **Get ticket**. On the42connecting machine:4344```bash45devsite connect dst_...46# connected to postgres (https://dev.site/s/res_...)47# listening on 127.0.0.1:4312748```4950## Shape5152```text53local application54 │ TCP to 127.0.0.1:ephemeral55 ▼56devsite connect57 │ capability-gated Iroh QUIC stream58 ▼59owner's devsite daemon60 │ TCP to 127.0.0.1:PORT61 ▼62private service6364dev.site control plane ── identity, profiles, approved shares, capability signing65```6667The control plane never carries service bytes and never learns the local port. It stores68resource metadata and permissions, mints browser-requested connection tickets, then signs69one short-lived capability per connection.70The daemon verifies the signature, daemon audience, client endpoint key, resource id,71expiry, and one-use nonce before opening the configured loopback target.7273The transport ALPN is `devsite/tcp/1`.7475Concept guides explain the security boundaries and lifecycle in detail:7677- [Tickets](docs/tickets.md)78- [Keys and endpoint identities](docs/keys-and-identities.md)79- [ALPN and the service wire protocol](docs/alpn-and-wire-protocol.md)8081## Services and links8283Services default to private:8485```bash86devsite service host 300087# hosting port 3000 → 127.0.0.1:3000 (private)88# https://dev.site/s/res_...89```9091Names and folders are presentation. Hosted ports enter the `Services` folder unless a92different folder is named:9394```bash95devsite service host 5432 --name postgres --folder databases96```9798Sharing remains per service. A recipient must approve the invitation on their dashboard:99100```bash101devsite service host 6379 --name redis --folder databases --share @bob102```103104An approved user opens the service's profile entry on dev.site, clicks **Get ticket**, and105passes the single-use ticket to the CLI. The connecting machine does not need a saved login:106107```bash108devsite connect dst_...109# connected to postgres (https://dev.site/s/res_...)110# listening on 127.0.0.1:43127111```112113Choose a stable local port when an application expects one:114115```bash116devsite connect dst_... --listen 127.0.0.1:15432117psql -h 127.0.0.1 -p 15432118```119120The connector refuses non-loopback listeners. Stop hosting a service with:121122```bash123devsite service remove postgres124```125126Links default to private, can be shared for recipient approval, or made public:127128```bash129devsite link set --name runbook --url https://example.com/runbook130devsite link set --name staging --url https://staging.example.com --share @bob131devsite link set --name klot.ski --url https://klot.ski --public --folder Games132```133134Setting an existing link or hosting an existing service prints the visibility, recipient,135destination, and folder changes before applying the upsert. Changing a shared link's URL136requires its recipients to approve the new destination again.137138## Daemon139140`devsite daemon run` is the portable foreground service entrypoint. A service manager only141needs to keep that command alive:142143```bash144devsite daemon run145```146147Every command accepts `--json` for automation. Finite commands emit one success or error148object; `connect` and `daemon run` emit newline-delimited lifecycle events. JSON mode never149prompts. Command-specific `--help` is available as structured JSON, and failures include a150recovery suggestion. The complete stdout and exit-code contract is in151[`docs/json-output.md`](docs/json-output.md).152153Inspect remote resources together with this machine's hosting state, validate a mutation154without applying it, or diagnose configuration and control-plane drift:155156```bash157devsite resources list --json158devsite service host 5432 --name postgres --share @bob --plan --json159devsite doctor --json160```161162`--dry-run` is an alias for `--plan`. Plans make no remote or local changes. A portable163agent skill that teaches these intent-based workflows through the CLI's JSON contract lives164at [`skills/devsite-cli/SKILL.md`](skills/devsite-cli/SKILL.md); it has no dependency on a165particular model or agent harness.166167## Delegated agent access168169A sandboxed worker can request temporary service access without receiving a logged-in170machine's credential or endpoint key:171172```bash173# Requester: keep requester.key private; send only request.json to the granting party.174devsite access request postgres --request request.json --key requester.key --json175176# Granting party: requires a machine ticket enrolled with service_grants:issue.177devsite access resolve postgres --json178devsite access grant --request request.json --plan --json179devsite access grant --request request.json --approved-plan dsp_... --json180181# Requester: use the server and dss_ grant returned by the granting party.182devsite --server https://dev.site access connect dss_... --key requester.key --json183```184185The request signs the service keyword, requester endpoint, request id, and expiry. The186granting party resolves that intent only among services its account may access, signs the187chosen resource and expiry, and receives a short-lived session bound to the requester's188endpoint. Requests expire within 10 minutes; sessions within 15 minutes. Replaying an189issued request id is rejected, while the resulting session can authorize multiple TCP190connections during its lifetime. Apply requires the server-signed `dsp_…` ticket returned191by the exact reviewed plan, so changing the request, resource, endpoint, or expiry after192approval is rejected. The dashboard exposes grant authority as an explicit193`service_grants:issue` scope on the single-use machine enrollment ticket, and revoking that194machine credential removes the grant sessions it issued.195196The optional [`plugins/devsite-access`](plugins/devsite-access) bundle packages the same197harness-neutral skill with a local MCP stdio adapter. The adapter implements MCP198`2026-07-28` discovery and request-scoped metadata: there is no `initialize` exchange,199negotiated session, or call-order state. Any compatible MCP client can use the adapter; the200plugin manifest is only a packaging layer. See [delegated access](docs/access-broker.md) for201the trust model and integration contract.202203Homebrew installs can supervise it at login on macOS or Linux:204205```bash206brew services start devsite207```208209Native Linux packages may install `packaging/systemd/devsite.service` as a user unit:210211```bash212systemctl --user enable --now devsite.service213```214215Version 0.6.0 uses `devsite-endpoint.key` and `devsite-endpoint.pub`. It does not read or216move legacy `identity.key` and `identity.pub` files. Start version 0.5.1 once before the217upgrade if these files remain. See [Keys and endpoint identities](docs/keys-and-identities.md)218for the migration rules. The daemon registers that public endpoint id with the control219plane at startup. It publishes its address through Iroh and reloads authorizations every220two seconds.221Adding or removing a service does not require a restart.222223`devsite daemon status` reports whether this config directory has a live daemon. The same224liveness appears in `devsite status` alongside the config location, pinned signing key, and225locally served ports. The lock is released by the operating system on exit, so a crash does226not leave a stale running state.227228## Profiles and folders229230A profile is a list of ordinary links and TCP services. Folders are repeated labels on231entries and exist only to group the UI; they are not authorization containers. Accepted232shares appear as ordinary rows in those folders with their owner noted, not in a separate233top-level sharing section. Every site retains its own visibility, invitation state, and234revocation; services additionally enforce access for every tunnel connection.235236The signed-in homepage is the dashboard. It manages approved and pending shares, revocable237machine credentials, the private-only profile setting, and logout. Profile presentation238remains a bounded list of approved declarations rather than arbitrary CSS: Pico variables239control the theme, while `--devsite-folders`, `--devsite-open-folders`, and240`--devsite-folder-order` control the initial semantic folder layout.241242Profile routes load a static shell and one viewer-specific HTML stream. Fixi starts the243request. SSEXi reads each event, and Paxi morphs each profile fragment into the page. The244control plane sends a new fragment after profile data changes. The browser keeps the245reader's folder state during each morph.246247## Local development248249Prerequisites: stable Rust and Node.js 24.250251The control plane uses the n0 relay preset by default.252Set `IROH_SERVICES_API_SECRET` to add endpoint-scoped Iroh Services tokens.253Set `DEVSITE_RELAY_URLS` to a comma-separated list to use custom relays.254The control plane sends the configured URLs and scoped token to each endpoint.255256```bash257npm ci258npm run build:web259export DEVSITE_PUBLIC_ORIGIN=http://127.0.0.1:4000260cargo run -p devsite-server261```262263Shoo is the default OIDC login adapter. A fork can use any public OIDC client without264changing application code by setting `DEVSITE_OIDC_ISSUER`,265`DEVSITE_OIDC_AUTHORIZATION_ENDPOINT`, `DEVSITE_OIDC_TOKEN_ENDPOINT`,266`DEVSITE_OIDC_JWKS_URI`, and `DEVSITE_OIDC_CLIENT_ID`. The server owns authorization-code267exchange, PKCE, and token verification; the browser only enters through `/auth/start`.268See [Authentication providers](docs/auth-providers.md) for OIDC replacement, identity269continuity, and implementing a non-OIDC `ExternalIdentity` adapter.270271For operator-only setup, mint a local browser session:272273```bash274cargo run -p devsite-server -- issue-session alice275```276277Build and test everything:278279```bash280npm run check:web281npx playwright install chromium282npm run test:web283cargo test --workspace284cargo build --release -p devsite-cli285```286287## Deployment288289The control plane runs as one Fly machine with one attached volume and SQLite behind a290mutex. `fly.toml` and `Dockerfile` describe it:291292```bash293fly deploy294```295296The traffic path scales independently because it does not pass through Fly. Iroh carries297end-to-end encrypted QUIC directly when possible and through a relay when necessary; the298control plane only signs authorization metadata.299300The durable boundaries are the OIDC `(issuer, subject)` identity pairs and301`DEVSITE_SIGNING_KEY`, whose public half every daemon pins at login. Shoo derives its302default client id and pairwise subjects from `DEVSITE_PUBLIC_ORIGIN`; changing that origin303is therefore still intentionally disruptive unless the replacement provider preserves304the configured issuer, client, and subjects.305306## Crates307308| Path | Responsibility |309| --- | --- |310| `crates/devsite-proto` | Opaque ids, signed capabilities, and the TCP stream handshake. |311| `crates/devsite-iroh` | Shared n0 and custom relay configuration. |312| `crates/devsite-client` | Native Iroh client endpoint and authorized service streams. |313| `crates/devsite-daemon` | Capability verification and fixed-target TCP forwarding. |314| `crates/devsite-cli` | Login, links, service hosting/connect, themes, and daemon lifecycle. |315| `crates/devsite-server` | Axum/SQLite control plane and capability issuance. |316| `web/` | Semantic HTML, strict TypeScript, Fixi, Paxi, SSEXi, Pico CSS, and vendored fonts. |317318## Security boundaries319320- A peer supplies only a signed capability. It never supplies the daemon's target address.321- `devsite service host PORT` always stores `127.0.0.1:PORT`; port zero is rejected.322- `devsite connect` listens only on a loopback address.323- Browser-minted connection tickets are short-lived, stored only as hashes, and consumed once.324 Redemption creates a client-key-bound session; its raw credential exists only in CLI325 memory, while the control plane stores its hash until expiry or disconnect.326- Capabilities are bound to the authenticated Iroh client endpoint and cannot be replayed327 by another endpoint.328- When configured, the control plane keeps the Iroh Services key and gives each endpoint a scoped relay token.329- Each capability opens one stream and its nonce is consumed once.330- Unknown resources, invalid signatures, wrong audiences, wrong clients, expiry, and replay331 all produce the same denial.332- Service identifiers are locators, not bearer credentials. Visibility and accepted-share333 state are checked whenever a new capability is requested.334- The control plane rate-limits capability issuance and bounds profiles, names, links,335 folders, credentials, and share lists.336- Plaintext machine enrollment tickets are consumed once. Enrollment binds the resulting337 machine credential to the machine's Ed25519 endpoint identity.338- Machine credentials and browser sessions are stored only as SHA-256 hashes and can be339 revoked from the dashboard. Revocation also closes affected active service streams.340341## License342343Licensed under the [Apache License, Version 2.0](LICENSE).