# Getting started with the URnetwork SDK

This guide takes a developer from an empty project to a first connection:
get an account JWT, install the binding for your platform, bring up a
device, and confirm traffic flows. The SDK is one Go module,
`github.com/urnetwork/sdk`, exposed through four bindings:

- **Android** — the core compiled to native code and bound for Kotlin/Java,
  shipped as an AAR.
- **iOS / macOS** — the same gomobile path, bound for Swift, shipped as an
  xcframework.
- **cgo (Windows, Linux)** — the core as a c-shared library behind a C ABI,
  with a C++17 header over it. Any language with an FFI reaches it here.
- **JavaScript (wasm/web)** — the core compiled to WebAssembly and run in the
  page, driven by your browser framework.

They front one core, so behavior matches across platforms — but they are not
interchangeable, and the differences decide your architecture rather than just
your syntax. Read [Which binding](#which-binding) before you pick one, and the
[SDK tour](/docs/tour-sdk) for how each is built.

URnetwork uses member-run exit devices. Providers do not receive your
users' source IPs on relayed paths. Devices built on the SDK also encrypt
traffic to the provider by default. Read
[How URnetwork works](/docs/overview) for the full model.

## What you need

All four bindings front the same core the official URnetwork apps are built
on: the device (the connection engine), the API client, the network-space
configuration model (which platform deployment you talk to, with what
endpoints and flags), and the view controllers, headless state-plus-events
objects for the common screens that you can bind your UI to or ignore. The
operator API behind it is documented at [/docs/api](/docs/api). You are
building your own app on URnetwork's hosted platform: the API, relays, and
providers are the live service, and your users bring or create accounts.

Platform floors, per binding:

- **Android** — API level 24 or newer. The Java package is
  `com.bringyour.sdk`.
- **iOS / macOS** — iOS 16.0 and macOS 13.5.
- **cgo** — Ubuntu 22.04+ (glibc 2.35+) or Windows 10+, amd64 and arm64.
- **JavaScript** — browser-only.
- **Building from source** — Go 1.26+.

The SDK is MPL-2.0 (Mozilla Public License): link it freely into
closed-source apps. The file-level copyleft only obliges sharing changes
to the SDK's own files.

## Which binding

The one difference that is not a matter of taste: **only three of the four can
run a device.** A browser page cannot own a tun interface, so the JavaScript
binding ships the control plane and no packet path. If your product needs to
move a user's traffic itself, wasm is not a smaller version of the others — it
is a different job.

| | Android | iOS / macOS | cgo | JavaScript |
| --- | --- | --- | --- | --- |
| You write | Kotlin / Java | Swift | C++ (or any FFI) | JS / TypeScript |
| The core arrives as | native code, gomobile-bound | native code, gomobile-bound | c-shared library, C ABI | wasm in the page |
| Runs the packet path | yes, in your app process | yes, in the NetworkExtension | yes, in a daemon you write | **no** |
| Process model | single process | app + extension, loopback mTLS | UI + privileged service, loopback mTLS | page only |
| Artifact | ~37 MB AAR | ~116 MB xcframework | ~21–25 MB library | ~43.5 MB wasm |

What that costs you, in the order it will bite:

- **Android** is the simplest: one process holds the UI and the `VpnService`,
  so your app owns the `DeviceLocal` directly and `IoLoop` pumps the tun fd
  inside Go with no per-packet JNI crossing.
- **iOS / macOS** is the same binding with a harder shape. Every Apple VPN runs
  packet handling in a separate sandboxed process, so the device lives in the
  extension and your app drives it remotely. Budget for the re-attach lifecycle
  and the extension memory limit from day one — it is not an edge case, the OS
  restarts that process routinely.
- **cgo** is the most portable and the least ergonomic. The ABI is handles and
  JSON rather than typed objects, which is what lets Rust, Python and C# reach
  it; the C++17 header is the one place that ergonomics were spent. You also
  write the privileged daemon yourself.
- **JavaScript** trades the packet path for reach. You get the API surface, the
  view controllers, and `DeviceRemote` against a device hosted elsewhere — and
  a 43 MB payload to lazy-load.

Threading is where all four converge on one rule: non-view objects are
concurrent-safe, view controllers are not. Browser JS gets that for free; the
other three do not. See the [tour](/docs/tour-sdk) for the details.

## Sign in

Every snippet below takes a `byJwt`: the account JWT, a signed login token
the URnetwork API issues when an account authenticates. There is no
separate API-key system.

Your app runs a login flow once through the SDK's API client. `authLogin`
reports an identifier's auth methods; `authLoginWithPassword` completes it
(`authVerify` finishes an emailed or SMS'd code), and wallet auth and
`networkCreate` are parallel entry points. Wallet auth is signature-only:
you send `wallet_address`, `wallet_message`, and `wallet_signature`, never
a key, and no URnetwork surface asks for a wallet's private key or
mnemonic. `networkCreate` with no auth method at all is the instant-account
path: it mints a permanent account and returns a `seedphrase`, URnetwork's
own recovery phrase for that account, generated server-side and handed back
exactly once. Surface it to the user then, or the account has no recovery
path.

Every path returns `by_jwt`. Persist it, call `setByJwt`, and pass it to
the device constructor. You write no refresh code; the `Api`'s token
manager rotates the JWT. Treat a hard auth failure as "run login again."

The account is also the billing unit: the account whose JWT the device
holds is the one whose plan is metered. Free is a daily data allowance,
Pro a large monthly one; current numbers at
[ur.io/products](https://ur.io/products). Intend one account per end user,
which the instant-account path makes frictionless, rather than one embedded
account pooling every user's usage and conduct into a single actor. When an
account runs out of data, transfer stalls until the allowance refreshes, so
say "out of data" in your UX, not a generic network error.

The SDK also owns the paid-plan flow, because the client must never name
its own price: register an intent with `createSolanaPaymentIntent`
(`reference` from `createPaymentReference`, `plan` `"monthly"` or
`"yearly"`), take `amountUsd` from the result, and pass it to
`buildSolanaPaymentUrl`. The C ABI has the intent call, not yet the URL
builder.

## Install

Prebuilt artifacts are release assets on
[github.com/urnetwork/build](https://github.com/urnetwork/build/releases).
Versions are date-based, `vYYYY.M.D-<code>` (e.g. `v2026.7.22-999364023`),
not SemVer: pin a release and upgrade deliberately, since the version says
when it was cut, not whether the API moved.

**Android.** Download `URnetworkSdk-<version>.aar` (plus `-sources.jar` for
IDE navigation) and drop them into a directory your Gradle module already
scans, e.g.:

```groovy
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.aar'])
}
```

**iOS / macOS.** Download `URnetworkSdk.xcframework.zip`, unzip it, and
reference it from a local Swift package as a `binaryTarget`:

```swift
// Package.swift
targets: [ .binaryTarget(name: "URnetworkSdk", path: "URnetworkSdk.xcframework")
]
```

All types are `Sdk`-prefixed (`SdkDeviceLocal`, `SdkNetworkSpace`, ...).
The xcframework carries `ios/arm64`, `iossimulator/arm64`, `macos/arm64`,
and `macos/amd64`; there is no Intel iOS-simulator slice.

**cgo (Windows, Linux).** C-shared libraries, the same layer the shipping
URnetwork Linux and Windows apps are built on, with two curated headers
alongside them:

- `libURnetworkSdk.so` — Linux.
- `URnetworkSdk.dll` (+ `urnetwork_sdk.def`) — Windows.
- `urnetwork_sdk.h` — the plain C ABI.
- `urnetwork_sdk.hpp` — a header-only C++17 RAII wrapper over it,
  requiring [nlohmann/json](https://github.com/nlohmann/json) on your
  include path.

Anything with a C foreign-function interface (Rust bindgen, Python ctypes,
C# P/Invoke) maps onto this handles-and-JSON ABI; the C++ header is the one
language convenience. With MSVC, generate the import library from the
`.def` file first:

```bat
lib /def:urnetwork_sdk.def /machine:x64 /out:URnetworkSdk.lib
```

**JavaScript (wasm/web).** The npm package is
[`@urnetwork/sdk-js`](https://www.npmjs.com/package/@urnetwork/sdk-js), a
browser-only loader that fetches Go's `wasm_exec.js` plus the SDK wasm and
instantiates them in the page. Install the `nightly` tag; it is cut from
the current SDK on the same dated version scheme and ships the wasm.
`latest` is months behind and carries no wasm at all, so a plain
`npm install` leaves you a loader with nothing to load.

```sh
npm install @urnetwork/sdk-js@nightly   # then pin the version it resolved to
```

The wasm is roughly 43 MB, the entire Go core, so it will not shrink, but
it compresses well: serve it as a static asset (gzip or brotli, cached
hard) and load it lazily, only when the user reaches a connect surface.
Never let a bundler inline or transform it, and always ship `sdk.wasm` and
`wasm_exec.js` from the same build; the glue is ABI-paired with the Go
toolchain that compiled the wasm, and the SDK's build gates on the two
matching byte for byte.

**From source.** You need Go 1.26+, and for mobile `make init` first: it
pins the exact gomobile the bindings are cut with and installs the checksec
the Android target runs. `build_android` also needs `ANDROID_NDK_HOME` set,
since it strips `.comment` with the NDK's `llvm-objcopy`.
`sdk/build-android.sh` and `sdk/build-ios.sh` wrap both. From `sdk/build`:

```sh
make init             # pinned gomobile + checksec; run before the mobile targets
make build_android    # AAR
make build_apple      # xcframework (build_ios is an alias)
make build_js         # wasm + loader
make build_linux      # c-shared .so, cross-compiled with zig
make build_windows    # c-shared .dll; not in `all` — the shipped one builds in a VM
```

`build_android` gates on gomobile's skip list, so a missing symbol is a
version mismatch, not a silent binding drop; gomobile binds no slices of
structs (lists cross as `SdkStringList`, `SdkIdList`, ...) and no
`context.Context`.

Third-party security assessments, and their limits, are covered in the
[threat model](/docs/threat-model).

## The tunnel permission

The operating-system consent, and the tunnel itself, belong to your app.
The SDK begins at the packet layer:

- On **Android**, the `VpnService` is yours: your app declares it, obtains
  VPN consent with `VpnService.prepare()`, and calls `establish()`. The
  SDK takes over at that file descriptor.
- On **Apple platforms**, the split is Apple's requirement, not the SDK's:
  packet tunnels run in a separate NetworkExtension with tight memory
  limits, so the tunnel provider owns the device while your app process
  attaches to it remotely.
- In a **browser**, a page cannot own a network interface, so there is no
  permission to ask, and nothing in the JavaScript layer tunnels the
  page's own traffic. Both JavaScript device models drive a device on the
  platform instead. The URnetwork extension covers browser tabs; a native
  app covers the whole machine.

For what URnetwork records about connections, read the
[threat model](/docs/threat-model).

## Connect

A new device starts on the network defaults. The client-provider session
is sealed out of the box: `setPerformanceProfile` ships with
`PostQuantumEncryption` on, the flag behind the apps' "Post Quantum
Encryption" control, and every current provider build enables the
responder side. With the flag on the client is fail-closed: it will not
carry application data in the clear, and it skips a provider it cannot
seal to rather than using it unsealed. Turn the flag off and traffic can
take the standard path again. `AllowDirect` is off by default; it is the
opt-in speed setting that removes the anonymizing hop and hands that
provider your user's real IP, the apps surface it inverted as "Strong
Anonymization", and hosted device profiles force it off. Leak protection
is yours to wire: the kill switch is the primitive `setRouteLocal`, a
device starts with local routing allowed, and `setRouteLocal(false)`
makes traffic stop rather than fall back when the tunnel is down. The SDK
embeds no analytics or crash reporting; its only connections are the
platform endpoints and the relays and providers the device uses.

### Android

Bootstrap order: create a `NetworkSpaceManager` pointed at app-private
storage (the directory holds local state, including credentials), create
the network space, then set the account JWT on its API client. The key is
a host name plus an environment name; production is
`("ur.network", "main")`, and `updateNetworkSpace` is what creates one,
while `getNetworkSpace` only reads back a space that exists, so it returns
null on a fresh install. URLs derive from the key (`https://api.<host>`,
`wss://connect.<host>`; a non-`main` env prefixes the service), but the
derivation prefers `migrationHostName`, which production sets to
`bringyour.com`: the shipping apps reach `api.bringyour.com`, and
`api.ur.network` does not resolve. Then create the device and hand it the
tunnel's file descriptor:

```kotlin
import com.bringyour.sdk.Sdk

val manager = Sdk.newNetworkSpaceManager(context.filesDir.absolutePath)
val key = Sdk.newNetworkSpaceKey("ur.network", "main")
val networkSpace = manager.updateNetworkSpace(key) { it.migrationHostName = "bringyour.com" }
networkSpace.api.setByJwt(byJwt)

val device = Sdk.newDeviceLocalWithMemoryTarget(
networkSpace, byJwt,
    deviceDescription,   // free-form, shown in the account's device list
    deviceSpec,          // e.g. Build.MODEL
appVersion, Sdk.newId(),         // instanceId; persist and reuse per install
    /* enableRpc */ false,
    keyMaterial,         // persisted DeviceLocalKeyMaterial, or null for ephemeral
memoryTargetByteCount, )

// inside your VpnService, after establish():
val detachedFd = pfd.detachFd()          // ParcelFileDescriptor -> raw fd
val ioLoop = Sdk.newIoLoop(device, detachedFd) { /* done callback */ }
```

`newIoLoop` takes ownership of the detached fd and pumps packets in both
directions until you close it. Do not touch the fd again from Java after
detaching.

Three constructor arguments deserve care. `instanceId`: generate one
`Sdk.newId()` on first run, persist it, and reuse it for the life of the
install (one live device per process); a fresh id every launch adds a
phantom entry to the account's device list. `keyMaterial`: null is fine
for a pure client, but if the device will ever provide capacity, persist
`getKeyMaterial()` in platform secure storage. It is the device's provider
identity, and losing it resets the provider's reliability history.
`memoryTargetByteCount`: a byte budget the device sizes its buffers and GC
pacing against; pick one that fits your process's real ceiling (see the
[tour](/docs/tour-sdk)). `DeviceLocal` is in-process: if your process dies
the tunnel dies with it, so run the `VpnService` as a foreground service
and recreate the device on restart with the same `instanceId` and key
material.

### iOS / macOS

The `NEPacketTunnelProvider` owns the `SdkDeviceLocal` and packet flow,
while the app process attaches to it as a remote device over loopback:

```swift
// in the packet tunnel provider (owns the device):
var err: NSError? let device = SdkNewDeviceLocalWithMemoryTarget(
    networkSpace, byJwt, deviceDescription, deviceSpec, appVersion,
    instanceId, /* enableRpc */ true, keyMaterial, memoryTarget, &err)

// in the app process (attaches to it):
let remote = SdkNewDeviceRemoteWithDefaults(networkSpace, byJwt, instanceId, &err)
try remote?.setRpcServer(clientPem, serverCertPem: serverCertPem, hostPort: hostPort)
```

The app mints the key material and RPC PEMs
(`SdkGenerateDeviceRpcKeyMaterial`) and hands them to the extension in the
`NETunnelProviderProtocol` provider configuration; the extension reads
`rpc_server_pem`, `rpc_client_pem`, and `rpc_listen_hostport` from there.
There is no app group on this path. See the [tour](/docs/tour-sdk) for why
the split exists and how reconnection works.

### cgo (Windows, Linux)

The ABI contract, in short:

- Objects are **opaque `uint64_t` handles**. `urnet_release(h)` frees the
  handle without stopping the object, so call its `*_close`/`*_stop`
  first where one exists.
- Returned `char*` strings are caller-owned; free them with
  `urnet_free_string`.
- Structured data crosses the boundary as **UTF-8 JSON** strings; ids are
  UUID strings, times are Unix epoch milliseconds (`0` = none).
- Callbacks fire on **arbitrary Go-managed threads**; marshal to your own
  thread. Their strings and buffers live only for the call, and handles
  they hand you are yours to release.
- Fallible calls take a `char** out_error`; on failure it is set to a
  message you free with `urnet_free_string`. Pass `NULL` to ignore the
  text.

The header carries one platform split: `urnet_new_io_loop`, the fd pump
the Linux app uses, sits inside `#if !defined(_WIN32)` and is absent from
the Windows `.def`. On Windows you move packets with
`urnet_device_local_send_packet` and
`urnet_device_local_add_receive_packet`.

A working end-to-end example lives at `cgo/smoke`; `make smoke_hpp` in
`sdk/cgo` builds the C++ wrapper smoke test against a host build and runs
it.

### JavaScript (wasm/web)

```js
import { URNetwork } from "@urnetwork/sdk-js";

const sdk = await URNetwork.init({
  wasmUrl: "/wasm/sdk.wasm",
  wasmExecUrl: "/wasm/wasm_exec.js",
});
```

The wasm registers its exports on `window`, so run one module instance per
page. `init` is idempotent within a module (a second call returns the same
instance), but two copies of the loader, say two bundles or two frames
sharing a realm, race over the same globals. Initialize once in a
module-level singleton, never inside a component lifecycle.

Two device models:

- `sdk.createProxyDevice(...)` — a lightweight client of hosted proxy
  URLs, for when all you need is "a proxy URL that exits through
  URnetwork".
- `sdk.createPlatformDeviceRemote({...})` — a full `DeviceRemote` speaking
  device-RPC to a hosted device over a websocket, for a real connect UI
  (locations, listeners, stats):

```js
const device = sdk.createPlatformDeviceRemote({
  apiUrl: "api.bringyour.com",
  platformUrl: "connect.bringyour.com",
byJwt, proxyUrl,        // from the platform's proxy config endpoint
  signedProxyId,   // HMAC auth token, not the JWT — see the tour
});

const unsub = device.addConnectLocationChangeListener((loc) => {
  console.log("location:", loc?.name);
}); device.setConnectLocation({ bestAvailable: true });
```

`createPlatformDeviceRemote` throws if the loaded wasm predates the
`DeviceRemote` binding; that is the symptom of an old npm tag.

## Confirm it works

Bring the device up and check the exit: any "what is my IP" check through
the tunnel should now report a provider's address, not the machine's. The
same state is visible in code and in the account: listeners fire as the
connection changes (the JavaScript snippet above logs each location change
as it lands), and the device appears in the account's device list under
the `deviceDescription` you passed. On the C ABI,
`urnet_live_handle_count()` reports live handles; assert it returns to
baseline in leak tests.

## If something fails

Support is the open channels: issues on the repos
([github.com/urnetwork](https://github.com/urnetwork)), product feedback
at [feedback.ur.io](https://feedback.ur.io), and security reports to
security@ur.io (disclosure policy at [ur.io/vdp](https://ur.io/vdp)).
There is no paid SDK support tier today. Two stalls have easy
explanations: a transfer stall on a working tunnel is usually an account
out of data, and a JavaScript loader with nothing to load is the `latest`
npm tag. The [tour](/docs/tour-sdk) covers the failure modes to know
before you ship.

## What next

- **Take the [SDK tour](/docs/tour-sdk).** The architecture behind each
  binding: process splits, threading, auth, reconnection, and sizing the
  memory target.
- **Tell your users what they are joining.** If your app routes traffic
  through URnetwork, the [overview](/docs/overview) is the canonical
  account of the path and what each party can see, and the
  [threat model](/docs/threat-model) is the full record behind it.
- **See the finished apps** for the experience your users will get:
  [Android](/docs/getting-started-android),
  [iOS](/docs/getting-started-ios), [macOS](/docs/getting-started-macos),
  [Windows](/docs/getting-started-windows),
  [Linux](/docs/getting-started-linux), and the
  [browser](/docs/getting-started-browser).
