This roadmap describes what remains between the current release and a complete, rigorously tested, well-documented 1.0 — and what "complete" means for a client of an API that Tailscale itself labels unstable. Shipped work lives in CHANGELOG.md; this document tracks only what is yet to be done.
swift-tailscale-client is an unofficial, MIT-licensed project with no affiliation to Tailscale Inc.
This package connects to an existing tailscaled daemon and speaks its LocalAPI. It is the Swift equivalent of Tailscale's own Go client/local package: control and observe the Tailscale installation the user already has.
That distinguishes it from TailscaleKit and other tsnet-based packages, which embed a second Tailscale node inside your app. Both are valid; they solve different problems. As of mid-2026 this is the only Swift package in the LocalAPI niche.
Primary driver: the Network Weather (NWX) macOS diagnostics app. The roadmap favors read-heavy monitoring and diagnostics first, configuration and management second, and specialized surfaces (Taildrop, Taildrive, Tailnet Lock) after 1.0.
Upstream's own source says LocalAPI paths are namespaced under /localapi/v0/ "to signal to people that they're not necessarily stable APIs." Additionally, since Tailscale 1.80+ the daemon is built from optional feature modules, so endpoint availability depends on how tailscaled was compiled, not just its version. This package answers with a three-tier policy, encoded in the API surface itself:
| Tier | Where it lives | Guarantee |
|---|---|---|
| Stable | Methods on TailscaleClient |
SemVer-protected once 1.0 ships. Covered by unit + integration tests on every supported tailscaled version. |
| Experimental | client.experimental namespace |
Compiles and works, but exempt from SemVer; tracks upstream churn (debug endpoints, log streaming, GUI push contract, self-update). May change or vanish in a minor release. |
| Unsupported | Documented only | Deliberately not wrapped, with the reason recorded in Documentation/LOCALAPI-COVERAGE.md. |
"Complete coverage" means every LocalAPI endpoint has a documented status — implemented, planned, experimental, or unsupported-with-reason — not that every endpoint has a wrapper. Connection-hijacking endpoints (dial), alpha endpoints, and Tailscale-internal plumbing stay unsupported until there is a real use case.
The policy is mechanically enforced today: Documentation/endpoints.json records two independent stability axes per endpoint (Tailscale's own "API maturity" annotation and this package's Swift-support promise) plus the upstream feature gate, all pinned to an immutable tailscale/tailscale commit and re-verified against that commit's source in CI (Scripts/verify-upstream-maturity.py); generated tables and a contradiction check keep the human docs honest.
Standing policy for all code:
- Public memberwise initializers on every model, so consumers can construct fixtures for SwiftUI previews and their own tests.
- Tolerant enums: string/int enums from the wire use an
.unknown(raw)/.othercase rather than failing decodes when upstream adds values. Booleans upstream marksomitemptydecode absent-as-false, never as optional. Sendableeverywhere,Equatableon models;Encodablewhere round-tripping matters.- Typed errors with actionable
recoverySuggestions; every request gets a configurable deadline. Typed status mapping,Tailscale-Versionobservation, and audit-reason injection apply to unary requests (streaming is documented as.transport-only). - Streaming resilience: an undecodable line in a stream is skipped and surfaced through a reporting hook, never fatal to the stream. Reconnection with exponential backoff is an explicit opt-in.
- Concurrency encoded in types:
serve-configreads return a snapshot carrying its ETag; writes require the snapshot, so a stale write surfaces as a typed conflict error rather than silent clobbering. - Naming follows Go
client/localadapted to Swift conventions — including upstream'sNetworkLock→TailnetLockrename andswitchToEmptyProfile()over the legacyaddProfile(). - Secrets never reach diagnostic surfaces — not logs, not
description, not reflection; regression tests assert no substring of an injected secret escapes.
No endpoint is implemented from documentation alone. Every new surface follows the same sequence:
- Spike against a real daemon. Exercise the endpoint with
curl --unix-socket(or a throwaway Swift scratch file) against an actual running tailscaled — locally and/or in the headscale integration environment — and observe real request/response shapes, headers, status codes, and streaming behavior. - Cross-check upstream source at the pinned commit. The authority is
tailscale/tailscale: the handler inipn/localapi/, the Go client method inclient/local/, and the types intailcfg/ipn. Public docs lag the code; the code decides field names, optionality, and edge behavior. Record the symbol, maturity, and gate inendpoints.json— CI verifies all three against the pinned revision. - Capture fixtures from the spike. Real (sanitized) responses become the versioned fixtures the unit tests decode — not hand-typed JSON guessed from docs.
- Then implement, with the fixtures and the spike findings encoding the corner cases (empty bodies, 204s/201s, ETags, chunked framing) into tests before the API is considered done.
The spike workflow and fixture-capture script are documented in Documentation/TESTING.md.
v0.4.0 through v0.12.0 have shipped; their contents are recorded in CHANGELOG.md. What remains:
| Version | Theme | New endpoints | Key non-feature work |
|---|---|---|---|
| v1.0.0 | API freeze | — | Pre-freeze naming audit; drop deprecated addProfile(); 1.0 criteria below; SemVer commitment |
| v1.1 | Taildrop | file-put/, files/ (incl. long-poll), file-targets |
Upload/download progress via IPN bus |
| v1.2 | Taildrive | drive/fileserver-address, drive/shares CRUD |
|
| v1.3 | Tailnet Lock | 13 tka/* endpoints (TailnetLock naming) |
|
| Post-1.0 (additive) | Stable-gap ledger | BugReportWithOpts recording handle; DialTCP/UserDial duplex abstraction |
Both tracked in the coverage ledger; see below |
| Ongoing | Experimental debug surface | debug actions, pprof, `update/install |
progress, appc-route-info, policy/, debug-bus-, prefs/service-clients` |
Criteria (checklist, not a feature list):
- Every always-on LocalAPI handler is wrapped or explicitly tiered Experimental/Unsupported in
Documentation/LOCALAPI-COVERAGE.md— done at v0.12.0:services/shutdownwrapped, and every one of the 62 handlers derivable from the pinned upstream source is either a manifest endpoint or an inventoried unwrapped handler with a reason (CI-enforced) - Test coverage ≥ 85% (floor enforced in CI since v0.12.0; 85.9% measured); streaming path and transport parsers fully unit-tested
- Integration matrix green against at least two tailscaled versions (three hermetic headscale lanes — stable / previous-stable / unstable — plus a live self-hosted macOS lane, on every PR)
- Complete DocC Topics tree (docs CI fails on undocumented public symbols — the abstract-coverage regression floors from v0.12.0 raised to 100%, which means writing the ~200 missing member abstracts in the pre-freeze audit), one tutorial (shipped in v0.12.0), at least two buildable examples in
Examples/(StatusDemo and Recipes exist) - Homebrew formula, Swift Package Index docs, and release automation all live
- Unofficial-status disclaimer and the stability policy present in README, DocC landing page, and error output (README/DocC done; audit error/CLI output)
- Pre-freeze API audit: naming pass against
client/localconventions; remove deprecatedaddProfile(); decide whether the transport-neutral core and safesocket parity work (issue drafts 05/06) changes any public API — if it does, it lands before the freeze or is redesigned to be additive; decide whetherPrefsbecomes lossless (unknown-field preservation) — required beforeStartOptions' internalUpdatePrefscarrier could ever go public, since today re-encoding a fetched snapshot would zero unmodeledipn.Prefsfields - Declare the stable-gap ledger items (
BugReportWithOpts,DialTCP/UserDial) explicitly post-1.0 in the release notes - Governance decisions recorded (issue draft 08): contribution policy/DCO, naming/disclaimer posture for the announcement
- From here: strict SemVer for Stable tier; Experimental tier explicitly exempt
- v1.1 Taildrop —
file-put/<target>/<name>(send),files/(inbox list, incl.?waitfor=long-poll),file-targets; progress observed via theIncomingFiles/OutgoingFilesnotify fields modeled back in v0.4.0 - v1.2 Taildrive —
drive/fileserver-address,drive/shareslist/set/rename/delete - v1.3 Tailnet Lock — the 13
tka/*endpoints underTailnetLocknaming (note:tka/modifyreturns 204) - Stable-gap ledger (additive, tracked in
endpoints.jsonand CI-verified as upstream-stable):BugReportWithOpts— a recording handle that keeps the POST body open until the caller ends the recording (upstream's contract); the experimentalrecord:knob documents today's limitationDialTCP/UserDial— raw duplex streams over HTTP upgrade; needs a Swift connection abstraction design spike first (issue draft 04)
- Transport-neutral core & safesocket parity (issue drafts 05/06) — architecture tracks; timing depends on the pre-freeze audit above
- Ongoing Experimental —
debug(?action=multiplexer),pprof,update/install+update/progress(update/checkshipped supported in v0.11.0),appc-route-info,policy/<scope>(MDM/syspolicy),debug-bus-graph|queues|events,prefs/service-clients, and whatever upstream adds next; wrapped on demand, never SemVer-bound
The operational detail lives in Documentation/TESTING.md and Documentation/RELEASING.md. Most of what these tracks originally listed has shipped; what's left:
- Coverage gate: 85 since v0.12.0; the pre-1.0 audit may ratchet further as gaps close
- Mutation/property tests: randomized truncation and field-deletion of fixtures must throw typed errors, never crash (nice-to-have before 1.0)
- Scripted headscale login lifecycle (v0.12.0)
- Upstream drift automation: scheduled re-pin job for
endpoints.jsonprovenance (decide cadence in v0.12.0) - CodeQL analysis; Dependabot for github-actions and swift ecosystems (not yet enabled)
- The menu-bar tutorial (v0.12.0); everything else in the article set has shipped
- Keep the AI-agent adapters (
.claude/skills/…,AGENTS.md,llms.txt, copilot instructions) in sync with each release — INTEGRATING.md is the single source
- GitHub topics (maintainer-side):
tailscale,swift,swift-6,localapi,wireguard,vpn,macos,async-await - Tailscale Community Projects submission — maintainer-approval gated; unblocked now that SPI shows current releases
- Announcement wave: awesome-tailscale PR, r/Tailscale, Swift Forums; Show HN + Tailscale forum at 1.0
- homebrew-core as a post-1.0 aspiration once the notability bar is met
- v0.11.0 release mechanics: maintainer tag push after the release PR merges (single tag — >3 tags in one push suppresses GitHub push events), then the Homebrew tap bump
- Homebrew formula smoke test as part of release verification:
brew install(orbrew audit+ install from the tap) against the freshly tagged release assets before announcing
- Embedded Tailscale — creating new tailnet nodes belongs in TailscaleKit
- CLI replacement —
tailscale-swiftdemonstrates the library; it does not compete with the official CLI - Shelling out — everything in pure Swift
- Wrapping everything —
dial(connection hijack with no clean Swift mapping — until the post-1.0 duplex abstraction exists),conn25/state,alpha-set-device-attrs,check-so-mark-in-use,upload-client-metrics, anddebug-capturestay unsupported until a real use case appears; each has its reason recorded in the coverage matrix - Pre-1.0 API stability — APIs may change before 1.0; from 1.0 the Stable tier follows SemVer strictly
Need an endpoint sooner, or one that's tiered Unsupported? Open a GitHub issue with your use case, the endpoint, and the expected request/response shapes. Community input reorders this list.