Skip to main content

What is the native bridge?

Some capabilities don’t live in JavaScript at all: reading files, fetching device and build info, reloading the bundle. They live in native modules, compiled into the app and exposed to JS through React Native’s bridge. native is how you reach them. It resolves native modules by name and ships a handful of pre-resolved handles for the ones Unbound needs most. Separately, the Unbound loader installs its own JSI object on the global as UnboundNative. That one is not a Discord module. It’s Unbound’s own native surface, and it carries a versioned feature API you should query before you call into it.

Resolving native modules

A native module’s name varies across platforms and Discord versions. The iOS file manager might be DCDFileManager on one build and RTNFileManager on another. So getNativeModule takes a list of candidate names and returns the first one that resolves, checking both NativeModules and the TurboModule registry.
Always pass every name you know a module by. A single name that’s right on your device may be wrong on someone else’s. The candidate list is what makes a module resolve everywhere.

Built-in handles

The native modules Unbound depends on are already resolved for you:
Build and release metadata: version, release channel, build number, identifier.
The bundle updater. Drives OTA update checks and reloads (see below).
Hardware and OS details: device model, manufacturer, system version, RAM.
The native file module the fs module is built on. Prefer fs for file work, since it resolves paths and defaults encodings for you.

Reloading

reload() is the safe way to restart the bundle: it persists pending settings first, then reloads, so nothing in flight is lost.

UnboundNative

UnboundNative is the JSI object the iOS loader installs directly on the JS global. It is re-exported from the native module so you can reach it through the API:
It is organized into a top-level introspection surface plus six namespaces.
Eight feature-lifecycle functions, described below, let you ask what this build actually supports: getNativeModuleVersion, supportsFeature, getFeatureInfo, isFeatureDeprecated, isFeatureRemoved, getSupportedFeatures, getDeprecatedFeatures, and getRemovedFeatures. evaluateBytecode also lives here.
Hardware, OS and install-integrity details about the device Unbound is running on.
string
The device model identifier.
string
The iOS version as a string.
boolean
Whether the device is jailbroken.
boolean
Whether the app is installed as a system app.
boolean
Whether the running build passes the loader’s signature check.
object
The application’s entitlements as an object. Returns an empty object when they can’t be read.
string
The same entitlements formatted as a plist string, for display or export.
string
How this copy of the app was installed. Useful when a feature depends on the install method rather than on the device.
string
Schedules a local notification and returns its identifier. Every argument is optional: title falls back to "Notification", body to an empty string, timeDelay to 1, soundEnabled to true, and identifier to a freshly generated UUID. Since the generated identifier is returned, capture it if you need to reference the notification later.
string | null
Starts picture-in-picture playback for a video URL and returns a playback identifier. Returns null when the URL is missing or empty, so check the result rather than assuming playback started.
Native chat appearance controls. The getters read current state synchronously; the setters are fire-and-forget (see the warning below).
number
The current avatar corner radius, or -1 when none is set.
undefined
Sets the avatar corner radius. Defaults to 0 when the argument is missing.
undefined
Clears the avatar corner radius override.
boolean
Whether message bubbles are on. Defaults to false when unset.
string
The light-theme bubble color.
string
The dark-theme bubble color.
number
The bubble corner radius, defaulting to 10 when unset.
undefined
Toggles message bubbles. The two color arguments are optional; pass neither and only the enabled flag is applied.
undefined
Sets both bubble colors. Missing arguments are sent as empty strings.
undefined
Sets the bubble corner radius. Defaults to 10 when the argument is missing.
undefined
Clears every message bubble override.
undefined
Opens the native toolbox menu. Fire-and-forget (see the warning below).
The setters are fire-and-forget. chat.setAvatarCornerRadius, chat.resetAvatarCornerRadius, chat.setMessageBubblesEnabled, chat.setMessageBubbleColors, chat.setMessageBubbleCornerRadius, chat.resetMessageBubbles, and toolbox.showMenu all hop to the main queue and return undefined immediately. There is no promise to await and no error to catch. Reading a value back on the next line can still return the old one, because the write hasn’t landed yet. Treat your own state as the source of truth rather than reading it back from native.
If UnboundNative is missing, the environment isn’t a real Unbound install and most native features won’t work. That’s a bug worth reporting, not a state to handle in your addon. The feature API below is for handling older Unbound installs, which is a real and expected case.

Feature lifecycle

The native surface changes between loader releases. Rather than making you sniff for the presence of a function, UnboundNative publishes a version and a table of named features, each tagged with the versions it was introduced, deprecated, and removed in. Ask before you call, and your addon keeps working on installs both older and newer than the one you developed against. The features named in the current table are device.info, device.entitlements, app.source, notifications, pip.video, chat.avatar, chat.messageBubbles, toolbox.menu, and native.evaluateBytecode. All nine were introduced in 1.0.0, and none are currently deprecated or removed.
boolean
true when the feature is known, has an introduced version this build is at or past, and has not been removed. This is the check to gate a call on.
boolean
true when the build is at or past the feature’s deprecated version and the feature has not yet been removed. A deprecated feature still works, so this is a signal to migrate, not to stop calling.
boolean
true when the build is at or past the feature’s removed version. A removed feature never reports as supported.
object
The full metadata record for a feature.
string
The native module’s own semver version. This is the version every feature check is evaluated against.
string[]
Every feature currently supported, sorted alphabetically.
string[]
Every feature currently deprecated, sorted alphabetically.
string[]
Every feature already removed, sorted alphabetically.

The five statuses

getFeatureInfo(...).status resolves to exactly one of these, checked in this order:
unknown and unavailable mean different things but call for the same handling: don’t call the feature. Only supported and deprecated are safe to call. Branch on supportsFeature for the decision, and read status when you want to tell the user why something is off.

How versions are compared

The comparison is strict, and knowing the rule saves you from a surprising result:
  • A version must be exactly three dot-separated components. 1.0 and 1.0.0.1 are both invalid.
  • Every component must be digits only and non-empty. 1.0.0-beta, v1.0.0, and 1..0 are all invalid.
  • Valid versions compare numerically, component by component, left to right. So 1.10.0 is newer than 1.9.0, not older.
  • A malformed version sorts below a valid one. If one side is malformed and the other isn’t, the malformed side is treated as older. Two malformed versions compare equal.
The practical consequence: a feature whose introduced value is malformed can never report as supported, because supportsFeature requires a parseable introduced string before it will compare anything. Prerelease tags aren’t a thing here.

Running precompiled bytecode

evaluateBytecode runs Hermes bytecode inside the app’s JS runtime and returns whatever the bytecode evaluates to. It’s how you ship an addon as precompiled bytecode instead of source, skipping parse and compile at load time.
ArrayBuffer
required
The Hermes bytecode to run. Must be an ArrayBuffer.
string
A label for the evaluation, used in stack traces and loader logs. Defaults to UnboundNative.evaluateBytecode. Pass your addon’s id so failures are traceable to you.
evaluateBytecode throws rather than returning an error value. It throws when the first argument isn’t an ArrayBuffer, when the buffer is empty, and when the bytes aren’t recognized as Hermes bytecode. Anything the bytecode itself throws propagates to your call site too. Wrap the call and fall back to shipping source if you can’t guarantee the target runtime.
Gate this one behind supportsFeature('native.evaluateBytecode') like any other feature. Note that fs reads return strings, not ArrayBuffers, so you’ll need to produce the buffer yourself. See managers for how addon bundles are loaded in the first place.