A secure local credential vault and API proxy for VRChat accounts.
VRCSecureLogin (VRCSL) eliminates the need to hand over your VRChat username and password to every third-party application or website. It stores your credentials securely on your machine, keeps your sessions alive, and lets third-party tools access VRChat on your behalf through a scoped, consent-driven local API.
- The Problem
- How It Works
- Features
- Supported Platforms
- Installation
- Development
- API Overview
- Pipeline and Event System
- Scope System
- Security
- For Developers (Third-Party Integration)
- Technology Stack
- Project Structure
- License
VRChat does not offer a standard OAuth 2.0 system. Every third-party application or website that requires VRChat authentication must emulate VRChat's proprietary login flow, which means users are forced to enter their raw credentials (username, password, and 2FA codes) directly into each service. This creates several issues:
- Plaintext credential exposure to every application that requests login.
- No way to limit what an application can do with your account.
- No way to revoke a single application's access without changing your password.
- Repeated login prompts because each application manages its own session.
- You add your VRChat accounts to VRCSL. Credentials are stored in your operating system's secure keychain (Windows Credential Manager or Linux libsecret) and never written to disk in plaintext.
- VRCSL logs into VRChat and keeps your sessions alive automatically.
- When a third-party application or website wants to interact with VRChat, it connects to VRCSL's local API instead of asking for your password.
- VRCSL shows you a consent dialog detailing exactly what the application is requesting and which accounts it wants access to. You choose what to allow.
- The application receives a scoped, time-limited token. It can only perform the actions you approved, on the accounts you selected.
- You can review, modify, or revoke any application's access at any time from the VRCSL dashboard.
- Secure credential storage using OS-level keychain (DPAPI on Windows, libsecret on Linux).
- Multi-account support with simultaneous session management.
- Automatic session keep-alive with silent cookie/token refresh.
- Scoped API proxy that maps VRChat API endpoints to granular permission scopes.
- User consent dialogs that show the requesting application's identity, icon, process path, code signature, and requested permissions before granting access.
- Account selection limits allowing apps to specify a maximum number of accounts they need (e.g., single-account tools).
- Process verification that binds tokens to specific executables by inspecting PID, file path, and code signature.
- Short-lived tokens (1-hour access, 30-day refresh) with automatic rotation.
- Per-token rate limiting to prevent abuse from rogue applications.
- Audit logging of all API requests and security events.
- Real-time event pipeline combining VRChat pipeline events and VRCSL internal events, delivered via WebSocket subscription or HTTP Server-Sent Events (SSE).
- DeepLink support (
vrcsl://) for user-facing actions like one-click avatar switching. - System tray integration to keep running in the background.
- Auto-update from GitHub Releases with SHA-256 integrity verification.
| Platform | Status |
|---|---|
| Windows | Supported |
| Linux | Supported |
Download the latest release from the Releases page. Choose the appropriate installer for your platform:
- Windows --
.exeinstaller - Linux --
.AppImageor.debpackage
- Node.js (v18 or later)
- npm (included with Node.js)
cd electron-app
npm installnpm run dev# Windows
npm run build:win
# Linux
npm run build:linux| Command | Description |
|---|---|
npm run lint |
Run ESLint across the project. |
npm run typecheck |
Type-check all TypeScript and Svelte files. |
npm run format |
Format all files with Prettier. |
VRCSL exposes three APIs, all bound exclusively to 127.0.0.1:7642. No external network access is possible.
Standard REST API for native applications.
| Endpoint | Method | Description |
|---|---|---|
/register |
POST |
Register a new third-party application. Triggers a user consent dialog. |
/refresh |
POST |
Refresh an expired access token using a refresh token. |
/accounts |
GET |
List VRChat accounts the token has access to. |
/api |
POST |
Proxy a single VRChat API request. |
/api/batch |
POST |
Proxy multiple VRChat API requests in one call. |
/events |
GET |
Server-Sent Events stream for real-time pipeline events. |
All authenticated endpoints require the Authorization: Bearer vrcsl_at_... header.
Same functionality as the HTTP API, designed for web-based clients to avoid CORS restrictions. Connect to ws://127.0.0.1:7642/ws and communicate using JSON messages with a requestId-based request/response protocol.
{
"requestId": "unique-id",
"type": "api_request",
"userId": "usr_xxx",
"body": {
"method": "GET",
"path": "/avatars/{avatarId}"
}
}WebSocket clients can also subscribe to real-time pipeline events by sending a subscribe message after authentication, specifying which accounts and event types to receive.
User-facing actions triggered via the vrcsl:// protocol. These do not require tokens and always prompt the user for confirmation.
| DeepLink | Description |
|---|---|
vrcsl://switchavatar?avatarId=avtr_xxx |
Switch avatar on an account. |
vrcsl://joinworld?worldId=wrld_xxx |
Join a world instance. |
vrcsl://addfriend?userId=usr_xxx |
Send a friend request. |
vrcsl://open |
Open and focus the VRCSL window. |
All DeepLinks accept an optional accountIdx parameter. If omitted and the user has multiple accounts, an account picker dialog is shown.
VRCSL provides a real-time event pipeline that combines two sources:
- VRChat pipeline events -- Forwarded from VRChat's own WebSocket pipeline (
friend-online,friend-offline,friend-location,user-update,notification, etc.). - VRCSL internal events -- Session and account lifecycle events (
session-refreshed,session-expired,account-online,account-offline,token-revoked).
WebSocket -- Send a subscribe message after authenticating on ws://127.0.0.1:7642/ws:
{
"requestId": "sub-1",
"type": "subscribe",
"body": {
"accountIds": ["usr_xxx"],
"events": ["friend-online", "friend-offline"]
}
}HTTP SSE -- Connect to the /events endpoint with query parameters:
GET /events?accountIds=usr_xxx&events=friend-online,friend-offline
Authorization: Bearer vrcsl_at_...
Accept: text/event-stream
Both methods deliver events in the same unified format:
{
"userId": "usr_xxx",
"eventType": "friend-online",
"source": "vrchat",
"timestamp": "2026-04-18T12:00:00.000Z",
"data": { "userId": "usr_yyy", "user": { "displayName": "FriendName" } }
}Clients specify which accounts to subscribe to and can optionally filter event types. Events are scope-filtered: only events matching the token's granted vrchat.pipeline.* or vrcsl.events.* scopes are delivered.
Permissions follow a hierarchical dot notation: vrchat.<category>.<action>. Wildcard scopes grant access to all actions within a category.
| Scope | Description |
|---|---|
vrchat.users.get |
Read user profiles. |
vrchat.users.search |
Search users. |
vrchat.friends.list |
List friends. |
vrchat.friends.status |
Read friend online status. |
vrchat.avatars.get |
Read avatar details. |
vrchat.avatars.select |
Switch avatar. |
vrchat.avatars.list |
List owned avatars. |
vrchat.avatars.* |
All avatar operations. |
vrchat.worlds.get |
Read world info. |
vrchat.worlds.list |
List worlds. |
vrchat.instances.get |
Read instance info. |
vrchat.instances.create |
Create instances. |
vrchat.invites.send |
Send invites. |
vrchat.invites.list |
List invites. |
vrchat.favorites.* |
All favorite operations. |
vrchat.groups.* |
All group operations. |
vrchat.notifications.* |
All notification operations. |
vrchat.playermod.* |
All player moderation operations. |
vrchat.files.* |
All file operations. |
vrchat.pipeline.* |
All real-time pipeline events (VRChat). |
vrchat.pipeline.friend-online |
Friend online events only. |
vrchat.pipeline.friend-offline |
Friend offline events only. |
vrchat.pipeline.friend-location |
Friend location change events. |
vrchat.pipeline.user-update |
User profile update events. |
vrchat.pipeline.notification |
Notification events. |
vrcsl.events.* |
All VRCSL internal events (session, account, token). |
vrcsl.events.session |
Session lifecycle events only. |
vrcsl.events.account |
Account lifecycle events only. |
vrcsl.events.token |
Token lifecycle events only. |
vrchat.* |
Full unrestricted access (triggers a warning in the consent dialog). |
For the complete scope-to-endpoint mapping, see the Project Design Record.
VRCSL is designed with the assumption that other processes on the local machine may be hostile. The key security measures are:
| Measure | Detail |
|---|---|
| Credential storage | OS keychain only (DPAPI / libsecret). Never written to disk in plaintext. |
| Data encryption | All persistent data files use AES-256-GCM with a key stored in the OS keychain. |
| API binding | HTTP and WebSocket servers bind exclusively to 127.0.0.1. |
| Process verification | Tokens are bound to the requesting application's executable path and code signature. |
| Token lifecycle | Access tokens expire after 1 hour. Refresh tokens expire after 30 days and rotate on every use. |
| Rate limiting | 60 requests per minute, 10 burst per second, enforced per token. |
| Consent | Every new application must be explicitly approved by the user through a topmost dialog. |
| Audit trail | All API requests and security events are logged with rotation. |
VRCSL does not protect against a fully compromised operating system (kernel-level rootkits, memory dumpers running as administrator).
There are two ways to integrate with VRCSL: using the official client SDK or communicating directly with the raw API.
The recommended integration method. vrcsl.js is the official JavaScript/TypeScript client SDK that handles transport selection, token lifecycle, event streaming, and error recovery automatically. It works in browsers, Node.js 18+, and Bun 1.0+.
Install:
npm install vrcsl.jsRegister and make API calls:
import { VRCSLClient, Scopes } from "vrcsl.js";
const client = new VRCSLClient({
appName: "My VRChat Tool",
appDescription: "Manages avatars across accounts",
appImage: "https://example.com/my-app-icon.png",
scopes: [Scopes.AVATARS_ALL, Scopes.USERS_GET],
maxAccounts: 1,
});
// Connect (attempts WebSocket first, falls back to HTTP).
await client.connect();
// Register triggers the consent dialog on the user's machine.
const result = await client.register();
console.log("Granted accounts:", result.grantedAccounts);
// Proxy VRChat API requests through VRCSL.
const avatar = await client.api("usr_xxx", "GET", "/avatars/avtr_yyy");
console.log(avatar.data);Subscribe to real-time events:
await client.subscribe(["usr_xxx"], ["friend-online", "friend-offline"]);
client.on("friend-online", (event) => {
console.log(event.data.user.displayName, "came online");
});Browser (script tag):
<script src="https://unpkg.com/vrcsl.js/dist/index.global.js"></script>
<script>
const client = new VRCSL.Client({
appName: "My Web Tool",
scopes: [VRCSL.Scopes.AVATARS_ALL],
});
async function init() {
await client.connect();
await client.register();
const accounts = await client.getAccounts();
console.log(accounts);
}
init();
</script>For the full SDK documentation, see the vrcsl.js README.
For languages and environments without SDK support, you can communicate directly with the VRCSL HTTP API.
Register:
curl -X POST http://127.0.0.1:7642/register \
-H "Content-Type: application/json" \
-d '{
"appName": "My VRChat Tool",
"appDescription": "Manages avatars across accounts",
"appImage": "https://example.com/my-app-icon.png",
"scopes": ["vrchat.avatars.*", "vrchat.users.get"],
"maxAccounts": 1
}'Proxy a VRChat API call:
curl -X POST http://127.0.0.1:7642/api \
-H "Authorization: Bearer vrcsl_at_..." \
-H "Content-Type: application/json" \
-d '{
"userId": "usr_xxx",
"method": "GET",
"path": "/avatars/avtr_xxx"
}'Refresh an expired token:
curl -X POST http://127.0.0.1:7642/refresh \
-H "Content-Type: application/json" \
-d '{
"refreshToken": "vrcsl_rt_..."
}'Subscribe to events (SSE):
GET /events?accountIds=usr_xxx&events=friend-online,friend-offline
Authorization: Bearer vrcsl_at_...
Accept: text/event-stream
Connect to ws://127.0.0.1:7642/ws and communicate using JSON messages.
Authenticate:
{
"requestId": "auth-1",
"type": "auth",
"body": { "token": "vrcsl_at_..." }
}Make an API request:
{
"requestId": "req-1",
"type": "api_request",
"userId": "usr_xxx",
"body": {
"method": "GET",
"path": "/avatars/avtr_xxx"
}
}Subscribe to events:
{
"requestId": "sub-1",
"type": "subscribe",
"body": {
"accountIds": ["usr_xxx"],
"events": ["friend-online", "friend-offline"]
}
}For detailed message formats, error codes, and protocol specification, see the Project Design Record.
| Layer | Technology |
|---|---|
| Framework | Electron |
| Build tool | electron-vite |
| Frontend | Svelte 5, TailwindCSS 4, shadcn-svelte |
| Language | TypeScript |
| VRChat SDK | vrchat npm package |
| Credential storage | OS keychain via keytar |
| Data storage | AES-256-GCM encrypted JSON |
| Local server | Node.js http + ws |
| Client SDK | vrcsl.js (TypeScript, ESM/CJS/UMD) |
| Packaging | electron-builder |
VRCSecureLogin/
├── electron-app/ # Main Electron application
│ ├── src/
│ │ ├── main/ # Main process (API server, session management, IPC)
│ │ ├── preload/ # Preload scripts for renderer isolation
│ │ └── renderer/ # Svelte frontend (account management, consent dialogs)
│ ├── build/ # Build resources (entitlements, icons)
│ └── resources/ # Static application resources
├── vrcsl.js/ # Official client SDK for third-party integration
│ ├── src/ # SDK source (client, transports, token store, deeplinks)
│ └── tests/ # SDK test suite
├── pdr/ # Project Design Records
│ ├── VRCSECURELOGIN_V1_PDR.md
│ └── VRCSLJS_PDR.md
├── LICENSE
└── README.md
This project is licensed under the terms specified in the LICENSE file.




