npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@discordkit/tauri

v0.1.0

Published

Tauri adapter for @discordkit/native — run the Discord Social SDK in a Node sidecar and reach it from the webview over a typed kkrpc bridge

Downloads

261

Readme

@discordkit/tauri

Run the Discord Social SDK (via @discordkit/native) in a Node sidecar and reach it from a Tauri webview over a typed, bidirectional kkrpc bridge.

[!WARNING]

🚧 Pre-1.0 and under active development. The API may change between minor versions. 🚧

The SDK's FFI is a Node-runtime binding, so it can't live in Tauri's Rust core — it runs in a sidecar process that Tauri's shell plugin spawns. This package is the glue: a sidecar host that runs the SDK and speaks kkrpc over stdio, and a webview client that drives it as a typed object. The bridge is composed per-domain (mirroring @discordkit/native's subpaths), so an app bundles only the native code for the features it actually wires — importing presence never pulls in voice.

There is no Rust crate to install: kkrpc rides the standard tauri-plugin-shell. You add one plugin line and merge a permissions snippet — see Getting started.

📦 Installation

npm install @discordkit/tauri @tauri-apps/api @tauri-apps/plugin-shell

@tauri-apps/api and @tauri-apps/plugin-shell are peer dependencies. You also supply the Discord Social SDK shared library yourself (it can't be redistributed) — see @discordkit/native for how the sidecar resolves it.

🧠 The two contexts

A Tauri app splits into the Rust-spawned sidecar (where the SDK runs) and the webview (your UI). This package has a subpath for each, plus a per-domain module under each:

| Context | Subpath | What you call | | ----------- | ------------------------------------- | -------------------------------------------------- | | Sidecar | @discordkit/tauri/sidecar | createSidecar([registrars], opts) | | | @discordkit/tauri/sidecar/<domain> | registerUsers, registerLobbies, … (registrars) | | Webview | @discordkit/tauri/client | createClient([slices]) → the typed bridge | | | @discordkit/tauri/client/<domain> | usersSlice, lobbiesSlice, … (slice factories) | | | @discordkit/tauri/renderer | CoreBridge / FullBridge typing | | | @discordkit/tauri/renderer/<domain> | per-domain bridge + value types | | | @discordkit/tauri/signals | framework-agnostic reactive helpers (optional) |

The core (createSidecar / createClient) covers presence, auth, status, and log — the baseline every integration uses. Feature domains (users, …) are opt-in: you only bundle a domain's native code if you compose its registrar (sidecar) and slice (client).

🔧 Getting started

1. The sidecar entry (your app owns + builds it)

Create discord.sidecar.ts. Composing registrars here is what keeps the built binary tree-shaken — only the domains you list end up in it:

import { createSidecar } from "@discordkit/tauri/sidecar";
import { registerUsers } from "@discordkit/tauri/sidecar/users";

createSidecar([registerUsers], { applicationId: "1234567890" });
// presence/auth/status/log are core (always present); add feature domains above

Build it to a binary and register it as a Tauri sidecar in tauri.conf.json:

// tauri.conf.json
{ "bundle": { "externalBin": ["binaries/discord-sidecar"] } }

stdout carries the RPC protocol — your sidecar code must not console.log. Diagnostics go to stderr (the default), or route them via createSidecar's onError.

2. Permissions (merge the scaffolded snippet)

kkrpc talks to the sidecar through tauri-plugin-shell, which needs permission to spawn it. Register the plugin in your src-tauri:

// src-tauri/src/lib.rs
tauri::Builder::default()
    .plugin(tauri_plugin_shell::init())
    // …

…and merge the shell permissions scoped to the sidecar into your capability. This package ships them at @discordkit/tauri/tauri/capabilities.json — copy them into src-tauri/capabilities/ (or merge into your existing capability):

// the scoped permissions (see the shipped capabilities.json)
{
  "identifier": "shell:allow-execute",
  "allow": [{ "name": "binaries/discord-sidecar", "sidecar": true }]
}
// + shell:allow-spawn (same scope), shell:allow-kill, shell:allow-stdin-write

3. The webview

Compose the matching client slices; createClient spawns the sidecar and returns the typed bridge:

import { createClient } from "@discordkit/tauri/client";
import { usersSlice } from "@discordkit/tauri/client/users";

const discord = await createClient([usersSlice]);

await discord.connect(); // OAuth2 + connect
await discord.setActivity({ type: "playing", state: "In Match" });
const me = await discord.users.getCurrent();

// on teardown:
await discord.close(); // closes the channel + stops the sidecar

The bridge type is inferred from the slices you compose. To name it explicitly, combine the pieces from @discordkit/tauri/renderer:

import type { CoreBridge } from "@discordkit/tauri/renderer";
import type { UsersBridge } from "@discordkit/tauri/renderer/users";

type Discord = CoreBridge & { users: UsersBridge };

🔐 Session persistence (@discordkit/tauri/keyring)

Session persistence is native-owned (@discordkit/native): connect resolves to a silent reconnect when a tokenStore is configured, logout ends the session, and createSidecar auto-resumes on boot — the integrator just supplies a TokenStore. For production, this package ships an OS-vault backend (Windows Credential Manager / macOS Keychain / Linux Secret Service) via tauri-plugin-keyring:

// sidecar entry — store tokens in the OS vault
import { tauriKeyringStore } from "@discordkit/tauri/keyring";
createSidecar([registerUsers], {
  applicationId: "1234567890",
  tokenStore: tauriKeyringStore()
});

// webview — expose the vault to the sidecar (only the shell can reach it)
import { keyringRelay } from "@discordkit/tauri/keyring";
const discord = await createClient([usersSlice], {
  expose: keyringRelay("my-app")
});

The sidecar can't touch the OS vault directly, so tauriKeyringStore's reads/writes relay through the webview's keyringRelay. Setup adds the tauri-plugin-keyring crate to src-tauri, the tauri-plugin-keyring-api peer dep, and the keyring:allow-*-password capabilities. (Don't want a vault? Use native's addon-free fileStore — no extra setup.)

🪞 What crosses the bridge (snapshots + id-keyed RPC)

@discordkit/native returns live Lobby / Call objects (native handles + methods) — those can't cross the process boundary. Over the bridge the webview instead gets serializable snapshots (LobbySnapshot, CallSnapshot) from reads, and drives those entities with id-keyed RPC — identical to the @discordkit/electron model.

Ids are branded snowflakes (UserId, LobbyId, ChannelId, …) inherited from @discordkit/native — an id returned by one call type-checks straight into another that wants the same kind, and the compiler rejects mixing them up. They're plain strings at runtime (Discord's wire convention, matching the REST client), so they serialize over kkrpc's JSON transport unchanged — this adapter does no id conversion. The snapshots come from native's Lobby.toJSON() / Call.toJSON(), so the adapter never hand-writes serialization.

🔔 Events

Every on* method returns an Unsubscribe. Events ride the same bidirectional kkrpc channel as calls (no separate Tauri emit/listen wiring needed). Client-wide streams carry ids; re-fetch for the full object:

const off = discord.messages.onCreated((messageId) => {
  void discord.messages.get(messageId).then(render);
});
// later: off();

⚡ Renderer conveniences (@discordkit/tauri/signals)

The webview↔sidecar split makes two things awkward that a plain bridge call doesn't solve: turning the bridge's push events into state you can read synchronously, and managing the async request lifecycle of a pull read. This subpath ships both, framework-agnostic, on TC39 Signals — read a current value with .get() and adapt to any framework. It's a separate subpath, so signal-polyfill is only bundled if you use it. (Same surface as the @discordkit/electron signals — only the transport beneath differs.)

Event-backed signals (auto-updating)

For bridge state that pushes events, these seed from the getter and stay live:

import {
  statusSignal, // Signal.State<Status>        — getStatus + onStatus
  devicesSignal, // input + output device lists — get*Devices + onDeviceChange
  logSignal, // rolling buffer of log lines     — onLog (capped, default 100)
  lobbyIdsSignal, // the lobbies you're in      — getIds + onCreated/onDeleted
  lobbySignal, // ONE lobby's live snapshot     — get(id) + member events for id
  isConnectedSignal, // Signal.Computed<boolean> — derived: status === "ready"
  subscribe
} from "@discordkit/tauri/signals";

const status = statusSignal(discord);
const ready = isConnectedSignal(status);
const lobby = lobbySignal(discord.lobbies, lobbyId);

using off = subscribe(lobby, (snap) => renderMembers(snap?.members ?? []));

// React:  useSyncExternalStore((cb) => subscribe(status, cb), () => status.get())
// Vue/Solid: read status.get() inside a reactive scope
// Svelte: readable(status.get(), (set) => subscribe(status, set))

Async resources (for pull-only reads)

Some bridge reads have no event stream to drive a reactive signal — relationships.list, voice.getCalls, messages.getUserMessages, isSelfMuted, … For those, asyncSignal models the actual challenge: the request lifecycle. It runs the read, exposes { loading, data, error } reactively, guards against out-of-order replies, and offers reload():

import { asyncSignal, subscribe } from "@discordkit/tauri/signals";

const friends = asyncSignal(() => discord.relationships.list());
using off = subscribe(friends, ({ loading, data, error }) =>
  render(loading, data, error)
);

// refresh after a mutation:
await discord.relationships.block(userId);
await friends.reload();

Why not a "refreshable signal" for these too? That would dress a static read up as auto-reactive when nothing actually pushes. asyncSignal is honest about it being request state — use the event-backed signals where the bridge genuinely pushes, and asyncSignal where it doesn't.

⚠️ Consent

Per Discord's SDK guidance, action APIs (send a message, send/accept an invite, friend requests, …) must only be called in response to an explicit user action — never automatically.

🪪 License

MIT © Drake Costa