Разработчикам

Embed

20 мин чтенияОткрыть как Markdown ↗

Whitelabel URnetwork into your own apps: your backend provisions a client for each installation, your apps embed the SDK, and no root credential ships in an app.

URnetwork runs on every major platform, including Android, iOS, macOS, Windows and Linux, and integrates as a native library inside your app with minimal permissions. With tunneled sockets it needs only network access: no VPN permission, no device-wide tunnel and no administrator rights.

Most apps replace their own sockets with tunneled sockets from the Device, as Socket shows. You can also tunnel the whole system, which takes the platform's tunnel permission, or pass custom messages between your installations with Messages.

An Embed plan is required. Embedding URnetwork in your own product needs an Embed plan for your network. Each plan starts with a review of your project and a contract, and then our team enables Embed for your network. Request one on the Services page, with your monthly active users and monthly data budget.

Embed — your backend signs in your user, provisions one URnetwork client per installation with the root JWT (POST /network/auth-client answers with a client_id and a scoped client JWT), keeps each installation's client_id and may set per-installation data caps, and delivers only the client JWT to your app on the user's device; the app's embedded SDK connects to URnetwork with it and carries the app's own traffic, in-app only rather than a device VPN; past 100 active installations an Embed plan raises your network's client limit, and no root JWT ships in the app

An embed integration puts URnetwork inside your own product, under your own brand, and it has two halves of equal weight. Your backend provisions a client for each installation of your app and sets its data caps. Your app packages the SDK and starts a Device with that installation's client JWT. The Device carries only your app's own traffic; what your app sends through it continues in Socket. Runnable examples cover twelve languages and three platforms of their own: Electron, Tauri and Android. They all implement the embed contract, which holds the exact rules; this page explains how the parts fit.

Who holds what

An embed integration splits the work three ways, and no key ships in an app.

  • Your backend signs in your users and holds your network's root credential. It provisions one client per installation, sets each client's data caps, delivers each installation's scoped client JWT and removes the clients you no longer want.
  • Your app embeds the SDK, runs a local Device with its installation's client JWT, sends its own traffic through it and shows the status. It never holds the root credential, never sets caps and never provisions or removes clients.
  • URnetwork enforces each client's data caps and your network's client limit.
ValueKept byPurpose
Root credential: an API key or URNETWORK_ROOT_JWTYour backend's secret storeProvisions clients, sets data caps and removes clients for the whole network
Client map, user:<service-user-id>:<installation-id> to client_idYour backend's private stateDecides which installation may obtain or renew which client; it stores client IDs, never tokens
client.jwtThe installation's private stateThe installation's scoped credential, which the SDK refreshes
instance-idThe installation's private stateOne UUID for the life of the installation, and the installation ID your app sends your backend
Data capsURnetwork, set by your backendOptional monthly and running-total byte caps for each client; the app reads its own
ACL groupURnetwork, set by your backenddefault or isolated: whether the client is in your network's peer list

Provision a client for each installation

Your backend provisions with POST /network/auth-client and the root credential: the same request the integration contract uses for a top-level client.

One client per running installation

The platform keeps one live connection per client_id. When a second installation connects with the same client, it displaces the first, and the two keep knocking each other off. So every installation that can run at the same time as another needs its own client.

Your backend's map keeps each installation's client_id under one composite key: user:<service-user-id>:<installation-id>. The installation ID is the installation's instance-id, which the app sends. The service user ID comes from your own sign-in, never from the request. That user part is what lets your backend refuse another user's installation: whatever installation ID a caller presents, it only reaches a key under the caller's own user, so your backend never reissues another user's client. A service whose users only ever run one installation may key by user:<service-user-id> alone.

An app picks its own installation ID, so your backend bounds how many installations one user may create. The token server allows 5 by default.

The root credential

Use an API key as your backend's credential in production. Create one with POST /account/api-key, called with a network JWT from sign-in. The key begins urn_ and is shown only in that answer. GET /account/api-keys lists your keys without their secrets, and POST /account/api-key/remove revokes one.

An API key authenticates as your network exactly like a network JWT and carries no client_id, so it is the bearer credential for every request on this page. Its scope is the same: the whole network. It is not narrower, and anyone who holds it can provision clients, change caps and remove clients. Treat it like a production database password: keep it in your backend's secret store, give it only to the service that provisions, and keep it out of apps, logs, error messages, URLs and repositories.

To rotate a key, create a new one, deploy it to your backend, then remove the old one. Remove a leaked key at once. Removing a credential stops it from provisioning; it does not cut off a user. To do that, pause or remove their client.

Provision and reissue

An installation's first sign-in provisions a new client. Send description and device_spec, and neither client_id nor source_client_id:

JSON
{
  "description": "embed client",
  "device_spec": "urnetwork-examples/curl"
}

The answer carries the new client_id and its scoped by_client_jwt. Map the client_id to the installation's key and deliver the JWT, which the installation saves as client.jwt. Later sign-ins reissue: send the stored client_id with the same description and device spec, and the answer carries a fresh JWT for the same client.

A client that has not connected for 30 days is deactivated, and its reissue answers Client does not exist. Drop that mapping, provision a new client and apply the installation's caps to it again.

Check every answer before you store or deliver anything: the HTTP result, error, both success fields, that the JWT's client_id claim equals the answer's client_id, and that your map does not already give that client_id to another key. A refusal answers 200 with error.message. URnetwork can see the description, so keep your users' identifiers out of it; your map already links each client to its user. The contract shows the requests with curl.

The client limit

A network can have 100 active top-level clients by default. Exactly, the limit counts the network's clients that are active, have no source_client_id and are not provider installs. With one client per running installation, that is your active installations. A client stops counting when you remove it, or when it is deactivated after 30 days without connecting. Child clients and provider installs never count.

Past the limit, provisioning a new client is refused with error.client_limit_exceeded and the message Client limit exceeded. An Embed plan raises your network's limit: request one on the Services page, with your monthly active users and monthly data budget. A refusal with error.upgrade_required comes from a second limit, your plan's cap on concurrently connected top-level clients; where the platform can quote an upgrade, the call answers HTTP 402 with its terms instead. An Embed plan sets your network's allowance for both limits.

Check your network with GET /network/embed and the root credential. It answers enabled, whether the team has enabled Embed for your network; client_limit, your network's limit; and active_client_count, the exact number that limit counts.

Data caps for each installation

Each client has two optional caps. Set either, both or neither:

  • The monthly cap limits the bytes the client uses in each UTC calendar month. It resets at 00:00 UTC on the first of the month, not at midnight in your user's time zone.
  • The running-total cap limits the bytes used since the total started: at your first cap request for the client, or at its last reset_total. It never resets on its own: raise or clear it, or start a new total with reset_total.

At a cap the client gets no new transfer contracts until the month rolls over, the cap is raised or cleared, or the total is reset. That is a hard stop, not a throttle, and its new contracts are refused the way an empty balance is, so read the caps to tell the two apart. Usage is counted when transfer contracts settle and rolled up within about two minutes, so the used counts lag live traffic, a cap takes effect within about two minutes of being reached, and a client can go past a cap by up to the size of its contracts that are still open: leave headroom. Metering never over-counts; each contract is counted at most once. A cap of 0 pauses the client at once, and the pause lasts until that cap changes. A new month does not lift a monthly cap of 0.

Every byte the client pays for counts toward its caps; no traffic is exempt. Lowering a cap below what the client has already used stops it within the same two minutes, and raising or clearing a cap lets it resume. At a cap your app's status reads data cap reached, or paused for a cap of 0.

Set caps with POST /network/client-data-cap and the root credential. A client JWT is refused, and the client must belong to your network. Each limit field merges, so one call changes one cap without touching the other. Omit a field to keep its value, send null to clear that cap, or send a whole number of bytes, 0 or more, to set it. In the table, C is the client's client_id:

BodyEffect
{"client_id": C, "monthly_byte_limit": 10000000000}10 GB a month; the running-total cap is unchanged
{"client_id": C, "total_byte_limit": null}Clears the running-total cap; the monthly cap is unchanged
{"client_id": C, "monthly_byte_limit": 0}Pauses the client until the monthly cap changes
{"client_id": C, "reset_total": true}Starts a new running total; both caps are unchanged

The API counts whole bytes; show GB or TB only in your interface. Read caps and usage three ways:

  • GET /network/client-data-cap?client_id=C with the root credential reads any client of your network.
  • The same request with a client JWT reads that client. This is how your app shows its own data.
  • GET /network/client-data-caps with the root credential pages through every client that has a cap set. limit takes 1 to 1000, and you pass each answer's next_cursor back as cursor until it is null.

Each answer carries both caps, the bytes used this month and in the running total, the period dates, and capped with its reason. Monthly usage is metered for every client, capped or not, so the single read also serves your billing. There are no threshold notifications: your backend and your app poll these reads.

Caps belong to a client, so with one client per installation they apply per installation. To give one user one budget across several installations, set each installation's caps from that user's budget, read usage across the user's clients with the bulk read, and adjust.

The data-cap routes work once the team has enabled Embed for your network. Until then they answer with the error.message Embed isn't enabled for this network. If Embed is later disabled, caps you already set keep applying.

Keep installations out of each other's peer list

Every top-level client is a peer in your network: it can appear in the network's peer list and receive that list. An embed app usually wants the opposite, so each client has an ACL group:

  • default: the client is a normal peer. New clients start here.
  • isolated: the client never appears in your network's peer list, receives no peer list itself, and does not count toward the 100 recently active clients that keep the peer list on. It still counts toward your client limit and your concurrent connection limit.

Set the group with POST /network/client-acl-group and the root credential, for example {"client_id": C, "acl_group": "isolated"}, and read it with GET /network/client-acl-group, using the root credential or the client's own JWT. Set it when you provision, before you deliver the client JWT: a client isolated while it is connected stops receiving peer updates, but keeps the list it already had. The token server and backend tools isolate new clients by default (URNETWORK_DEFAULT_ACL_GROUP). Messages finds other clients through the peer list, so an installation that uses Messages stays in default.

Like the data-cap routes, the ACL-group routes answer Embed isn't enabled for this network. until the team enables Embed for your network, and groups you already set keep applying if Embed is later disabled.

Pause, remove or delete a user

  • Pause an installation with a cap of 0. It gets no new transfer contracts, and its data stops within the life of the contracts already open. Set the cap back to resume.
  • Remove a client with POST /network/remove-client and {"client_id": C}. The platform refuses its client JWT at the next connection, so the installation loses access when it reconnects. Drop the mapping as well. If that installation signs in again, your backend would provision a new client, so to keep a user out, refuse their sign-in in your own service.
  • Delete a user by removing the client of each of their installations and dropping those mappings.

Coming from another whitelabel SDK, the usual controls map like this:

ElsewhereHere
Disable a userA cap of 0 on each of their installations
Delete a userRemove each installation's client and drop its mapping
Account expiryNo expiry; a client idle for 30 days is deactivated
Device limitYour backend's bound on installations per user; the token server allows 5
Per-user data limitCaps on each installation, budgeted from the user's limit
Hide users from each otherThe isolated ACL group

The token server and backend tools

The examples include a minimal backend in two forms:

  • The Go token server, in go/embed/server, has the shape of your service's sign-in endpoint. Your app posts its instance-id to POST /urnetwork/client-token, with a demo session as the bearer token. The server provisions or reissues that installation's client, applies optional default caps to a new client, and answers with the client_id, the by_client_jwt and the current caps. It uses only the Go standard library, allows 5 installations per user by default and listens on 127.0.0.1:8790: serve it to the internet only through your own HTTPS front end, and replace its demo sessions with your real sign-in. The Electron, Tauri and Android examples use it.
  • Each console language has a backend tool in <language>/embed/server/. It extends that language's integration allocator with provision, cap, usage, usage-all and remove.

Both keep the private client map, hold a lock while they provision so that one key never gets two clients, check every answer, and never print the root credential or a client JWT. Each has a credential-free self-test. The contract defines the token server's HTTP contract and the backend tools' commands.

Package and embed the SDK

Permissions

With tunneled sockets, the embedded Device routes only the traffic your app sends through it: a socket opened on the Device, or an HTTP client pointed at its loopback proxy. It does not use the operating system's VPN interfaces, so your app needs no Android VpnService and no Apple Network Extension VPN entitlement, and Google Play's VpnService declaration does not apply. Whole-system tunneling is different: your app holds the platform's tunnel permission, and the store rules for VPN apps then apply. The embed examples use tunneled sockets.

Your privacy disclosures still apply. Your app routes some of its users' traffic through URnetwork, and its privacy policy, App Store privacy details and Play data safety form must say so. The SDK ships no Apple privacy manifest (PrivacyInfo.xcprivacy), so your app's own disclosures cover it. Under the terms you must not permit unlawful use of the network, which makes your users' use your responsibility, and you support your own users.

Add the SDK

Pin an exact release, never a branch:

PlatformAdd and pin the SDKBinding
Gogo get github.com/urnetwork/sdk/v2026@<version>The Go SDK, in process
Cvcpkg or Conan urnetwork-sdk, pinned by port or recipe versionThe C ABI, urnetwork_sdk.h
C++The same package, with urnetwork_sdk.hppThe C ABI
C#NuGet URnetwork.SDKP/Invoke over the C ABI
JavaMaven io.ur:urnetwork-sdk:<version>JNA over the C ABI
KotlinThe same Maven artifactJNA over the C ABI
Pythonpip install urnetwork-sdk==<version>ctypes over the C ABI
Rubygem install urnetwork-sdk -v <version>FFI over the C ABI
Rustcargo add urnetwork-sdk@<version>The C ABI, through the crate
SwiftThe C ABI through a SwiftPM C module; Apple app targets may use the gomobile XCFramework insteadThe C ABI, or gomobile on Apple
JavaScriptThe native companion in embed mode, started as a child processA side-service process
TypeScriptThe same companionA side-service process
ElectronThe same companion, started by the main processA side-service process
TauriThe urnetwork-sdk crate in the Rust coreThe C ABI, through the crate, in process
Androidimplementation("io.ur:urnetwork-sdk-android:<version>")The gomobile AAR

The C ABI bindings load the URnetwork SDK shared library, which ships with your app; the NuGet, Maven, Python and Ruby packages and the Rust crate carry it for you. JavaScript, TypeScript and Electron ship one companion binary for each OS and architecture. Some package coordinates are awaiting their first publication, and each example's README gives the local build path. Install the SDK covers the packages.

Keep the installation's state

Each installation keeps its state in one private directory: client.jwt, instance-id and the SDK's bounded logs/, plus token-server.json in the GUI and Android apps. On POSIX the directory is 0700 and its files 0600, a file that group or others can read, or a symlinked file, is refused, and every file is replaced atomically. On Windows keep it under %LOCALAPPDATA%. Console examples read its absolute path from URNETWORK_EMBED_STATE_DIR; GUI and Android apps use a private embed directory in their application data, on Android in the no-backup files directory.

The client JWT is a bearer secret: never print, log or back it up. A production app may also encrypt it with the platform's secret store:

PlatformSecret store
Apple platformsThe Keychain
AndroidThe Android Keystore
WindowsDPAPI
LinuxThe Secret Service (libsecret)
ElectronsafeStorage

Start and stop the Device

The app's lifecycle is the same on every platform:

  1. Load the installation state, and create instance-id on the first run.
  2. Obtain the client JWT: from your backend, or from a client.jwt that a backend tool wrote. The examples call the token server on every start, which reissues the client. A network JWT is refused, because it has no client_id.
  3. Point the SDK's logs at logs/, create a network space manager, and give the space's API the client JWT.
  4. Create the local Device with NewDeviceLocalWithDefaults and the installation's instance-id. Leave the provide mode at its default: an embed app does not provide.
  5. Listen for token refreshes and save each one as client.jwt, for a logout and stop, and for contract status changes and read the caps again.
  6. Set the connect location to the best available location.
  7. Read the caps with the client JWT at start and every 5 minutes, and the status every second.
  8. To stop, close the subscriptions, then the Device, then the manager.

The contract's app lifecycle gives the exact calls for the Go SDK, gomobile and the C ABI.

What the app shows

Every example shows three fields, with the same rules:

FieldShows
Statussigned out, stopped, client limit, paused, data cap reached, connected or connecting, checked in that order
Data this monthchecking, unavailable, no cap, or the bytes used of the monthly cap
Data totalThe same, for the running-total cap

signed out and stopped appear only in the GUI and Android apps. client limit adds its retry time, as client limit, retry at HH:MM UTC. paused means the cap that stopped the client is 0. A monthly cap adds when it resets, as data cap reached, resets YYYY-MM-DD HH:MM UTC. connected means the Device has at least one provider. Amounts use decimal units, as data plans do. Console examples print a status line whenever a field changes, and otherwise once a minute:

Text
status: connected | data this month: 1.2 GB of 5.0 GB | data total: no cap

The contract's golden vectors pin every text exactly. Console examples exit 0 after a requested stop, 78 for a configuration or credential problem that a restart does not fix, and 1 for any other failure.

License

The SDK and connect are licensed under the Mozilla Public License 2.0, a file-level copyleft. Your app's own code can stay proprietary; changes you make to the SDK's own source files must be shared under the MPL. Publish the SDK's licenses and data attributions with your app: GetLicenses(app), or urnet_get_licenses in the C ABI, returns them for your app kind without a network call.

Next: Sockets and Messages

The embed integration ends where your app's own traffic begins.

  • Sockets route your app's TCP, UDP and HTTP clients through the Device. Libraries that need an operating system socket use its loopback proxy. Start with Socket and each language's socket example.
  • Messages exchange text with other clients of your network over the URMS protocol. They do not run on the embed Device: messages attach to a provider-capable Device, and the embed Device does not provide. A Messages program starts its own provider-capable Device for the installation, which provides to your network, so your network's other clients can route traffic through that installation. Before you turn that on, show a consent screen that says so, let the user turn it off at any time, and disclose it in your terms. A Messages installation also stays in the default ACL group.

The installation's state works with the socket and messages programs: pass the content of client.jwt as URNETWORK_CLIENT_JWT and of instance-id as URNETWORK_INSTANCE_ID, and run one program at a time with one identity. An embed client in the default ACL group appears in your network's peer list while the network has 100 or fewer recently active top-level clients; past that, the peer list is off, and the Messages examples depend on it. An isolated client never appears in it.

Examples

Each example's README has build and run steps for every OS it supports, the backend steps for its language, and a credential-free self-test. The console examples run on Windows, macOS and Linux. The Go embed app and its token server are the reference implementations.

PlatformExampleBuilt on
CC embedThe C ABI
C++C++ embedThe C ABI
C#C# embedThe C ABI, through .NET
GoGo embedThe Go SDK, with the token server
JavaJava embedThe C ABI, through JNA
JavaScriptJavaScript embedNode and the native companion
KotlinKotlin embedThe C ABI, through JNA
PythonPython embedThe C ABI, through ctypes
RubyRuby embedThe C ABI, through FFI
RustRust embedThe C ABI, through the Rust crate
SwiftSwift embedThe C ABI, through a SwiftPM C module
TypeScriptTypeScript embedNode and the native companion
ElectronElectron embedA desktop app on the native companion
TauriTauri embedA desktop app with the Rust crate in process
AndroidAndroid embedThe gomobile AAR

Release availability: the embed examples need no SDK API beyond what ships today: they read the caps over HTTP. They need an SDK release from the first release after sdk c638dfa8, which adds the client limit status. The data-cap routes and the per-network client limit that an Embed plan raises ship with the server release that adds them. On an older server the cap routes answer 404: the apps show unavailable for the data fields, and the backend tools report the failure. Install the SDK covers the packages, and the examples browser has each language's other examples.