Welcome to Rift
Everything you need to get started, learn the interface, format messages, and connect your own apps using webhooks.
What is Rift?
Rift is a chat platform by ModemINC built for communities and teams. It has servers, channels, voice chat, direct messages, and a powerful message formatting system - all wrapped in a fast, clean interface.
Creating an Account
Go to modeminc.com/rift/signup and enter a username, email, and password.
Once signed in, you'll land in the main app. You can join a server right away using an invite link, or create your own.
Set up your profile in Settings - Profiles: choose a display name, upload an avatar, and write a bio.
The Desktop App
Rift has a native Windows desktop app that runs in the system tray and supports voice, screen share, and OS notifications.
Download
Download and run ModemInstaller from cdn.modeminc.com/ModemInstaller.exe. It downloads Rift.exe - the whole app in a single file - and sets it up for you. Rift then keeps itself up to date automatically from inside the app.
Startup options
In Settings - Windows you can enable:
- Launch on Windows Startup - Rift starts automatically when you log into Windows.
- Start Minimized - when launching on startup, Rift opens to the system tray instead of showing the window. Toggle this alongside Launch on Startup.
Dashboard Links
The Rift dashboard supports ?page= URL parameters so you can deep-link directly to any section. Share a link or bookmark any of the pages below.
| URL | Opens |
|---|---|
modeminc.com/rift/dashboard | Dashboard (last visited server) |
modeminc.com/rift/dashboard?page=discovery | Server Discovery |
Invite links
Server invite links use the path /rift/invite/{code}. Any member with Create Invite permission can generate one from Server Settings.
https://modeminc.com/rift/invite/{code}
User profiles
Every Rift user has a public profile page at:
https://modeminc.com/rift/users/{username}
Bot invite links
Share this link with server owners to let them install your bot:
https://modeminc.com/rift/bot-invite/{bot_id}
The rift:// protocol
The desktop app registers the rift:// URL scheme with Windows. Any link, script, shortcut or third-party app can use it to open (or focus) Rift at a specific place - if Rift isn't running, it launches first. Nothing is sent over the network; it's a purely local jump.
| Link | Opens |
|---|---|
rift://dm/{user_id} | A direct message with that user (creates it if needed) |
rift://channel/{channel_id} | That channel, switching to its server |
rift://server/{server_id} | That server |
rift://server/{server_id}/{channel_id} | That server, at that channel |
rift://invite/{code} | The invite dialog for a server |
Links are validated before they reach the app: only letters, digits, _, - and / are accepted, so a crafted link can't inject anything. Unknown or unavailable targets show a message rather than navigating. If you aren't signed in yet, the link is remembered and opened once you are.
rift://dm/usr_7Kd92mAqp1 rift://channel/ch_VGBjlnzIWRwz rift://server/srv_A1b2C3/ch_VGBjlnzIWRwz rift://invite/rift-community
Note: rift:// is a Windows desktop-app feature. In a browser these links do nothing unless the desktop app is installed.
Interface Overview
The Rift interface has four main areas:
| Area | What it does |
|---|---|
| Server Rail (far left) | Icons for every server you're in. Click to switch servers. The + button creates or joins a server. |
| Channel Sidebar | Lists all channels in the current server, grouped by category. DMs are shown here too when no server is selected. |
| Messages Area | The main chat area for the selected channel. |
| Members Sidebar | Shows who's in the server and their online status. Click a member to see their profile. |
Settings
Click the gear icon at the bottom of the channel sidebar to open Settings. Tabs cover your profile, account, appearance, notifications, voice & video, and (in the desktop app) Windows-specific options.
Servers
Creating a server
Click the + button in the server rail, then choose Create a Server. Give it a name and an optional icon. Rift automatically creates a General text channel and a General voice channel to get you started.
Joining a server
Click + and choose Join a Server, then paste an invite link. You can also click a direct invite URL (e.g. modeminc.com/rift/invite/abc123).
Inviting people
Right-click any text channel and select Invite People, or go to Server Settings - Invites - Create Invite. Share the link anywhere.
Server Settings
Click the server name at the top of the channel list to open the server menu, then choose Server Settings. From here you can:
- Edit the server name and icon
- Manage roles and permissions
- View and manage members
- Create and manage webhooks
- Set up moderation rules
- Submit the server to the Discovery page
Roles & Permissions
Roles control what members can do. Go to Server Settings - Roles to create roles, set permissions, and assign colors. The @everyone role applies to all members. Roles higher in the list have more authority over lower roles.
Channels
Create a channel by clicking + next to a category name in the channel list. Choose a type:
| Type | Description |
|---|---|
| Text | Standard chat channel. Members send messages, react, reply, and share files. |
| Voice | Join to talk with others. Supports video and screen share. |
| Announcement | Only members with the right permission can post. Good for server-wide news. |
| Forum | Posts with titles and tags. Members reply to individual posts. |
| Rules | A read-only numbered list of server rules. |
| Log | Audit log for bot/webhook messages. Read-only in the UI. Messages auto-delete after 24 hours. |
Channel Permissions
Right-click a channel and select Edit Channel, then go to Permissions. You can allow or deny specific actions for individual roles or members - for example, making a channel read-only for everyone except moderators.
Messaging
Sending a message
Click in the message bar at the bottom of the channel and start typing. Press Enter to send. Use Shift+Enter for a new line without sending.
Replying
Hover over a message and click the reply arrow to reply directly to it. Your message will show a reference to the original above it.
Editing & Deleting
Hover over your own message to reveal the action bar in the top-right corner. Click the pencil to edit or the trash to delete. Server moderators can delete any message.
Reactions
Hover over a message and click the smiley face icon. Pick an emoji from the picker. Click an existing reaction to add yours too. Click your own reaction again to remove it.
Pinning messages
Moderators can pin important messages. Right-click a message (or use the action bar) and select Pin Message. View pinned messages by clicking the pin icon in the channel header.
Formatting toolbar
Click the A icon above the message input to open the format toolbar. It has buttons for Bold, Italic, Underline, Strike, Code, Code Block, Spoiler, Embed, Color, Rainbow, Glow, Bar graph, Circle graph, Gauge, and Stat block.
Attachments
Click the + icon in the message bar to attach a file. Images and videos are embedded inline. Files are stored with their original filename.
Direct Messages
Click a user's avatar or username anywhere in Rift to open their profile, then click Send Message. DMs appear in the left sidebar when you're not in a server.
You can also use the friends panel (the person icon in the server rail) to manage friend requests and open DMs with friends.
Voice & Video
Click a voice channel in the sidebar to join. A control bar appears at the bottom with:
- Mute - toggle your microphone on and off
- Deafen - mute all incoming audio
- Video - share your camera
- Screen Share - share your screen or a specific window (desktop app only)
- Leave - disconnect from the voice channel
Click a user's avatar in the voice channel to adjust their volume for you locally.
Markdown Formatting
Rift supports standard markdown syntax in all messages and webhooks. Formatting is rendered client-side - the server stores raw text.
| Syntax | Result |
|---|---|
**bold** | Bold text |
*italic* | Italic text |
__underline__ | Underlined text |
~~strikethrough~~ | Strikethrough |
||spoiler|| | Hidden until clicked |
`inline code` | Inline monospace code |
```lang + newline + code + ``` | Syntax-highlighted code block |
 | Embedded image |
[label](https://url) | Hyperlink |
| bare URL | Auto-linked |
@username | User mention |
@everyone / @here | Mass mention |
ch_xxxxxx | Channel link (paste a channel id) |
[embed]...[/embed] | Callout / embed block (see Embeds) |
Rift Widgets
Rift has custom formatting tags that go beyond markdown. Use them in messages or webhook content. All widget tags are processed before markdown.
Colored text
Wrap text in a color name or hex code:
[red:This text is red] [#6c63ff:This text is purple] [orange:Warning!]
Rainbow text
[rainbow:Hello Rift!]
Each character cycles through a set of colors.
Glow text
[glow:purple:Online] [glow:#22c55e:Success]
Bar graph
[bargraph:75%:green] [bargraph:40%:#4ecbff]
Inline horizontal progress bar. Value is 0-100%.
Circle graph
[circlegraph:50%:orange]
SVG circular progress ring with percentage in the center.
Gauge
[gauge:80%:red]
Semicircular speedometer-style gauge.
Stat block
[stat:Username:kipi9876] [stat:Account Age:7957 days][stat:Time Spent:6 minutes]
Small card with a label above a value. Stack multiple on one line to make a stat row.
Pill badge
[pill:ONLINE:green] [pill:ERROR:#ef4444]
Rounded colored badge - useful for status labels alongside other content.
Embed / callout block
Wrap content in [embed]...[/embed] to render it as a callout with a colored left bar. Everything inside is parsed normally, so you can nest bold, links, code, colors, graphs and other widgets. Add =color (a name or hex) to set the bar color; the default is your accent color.
[embed] **Server rules** Be kind and read the [guidelines](https://modeminc.com/rift/guidelines). [/embed] [embed=#ef4444] [glow:#ef4444:Warning] Restart is scheduled for [pill:9 PM UTC:orange] [/embed]
Great for rules, announcements and notices. The block spans full width and can hold multiple lines.
Poll
Polls are created from the composer (the + menu β Poll), not typed by hand - they carry live vote data. Once posted they render as an interactive vote card for everyone in the channel.
What are Webhooks?
A Rift webhook is a URL you can POST JSON to and a message appears in your channel instantly. You need a Rift account to create one, but once created any app or script can use the URL - no login required to post.
Each webhook has a name shown as the message author (with an APP badge), an optional avatar, a secret URL, and a channel it posts into.
Creating a Webhook
Click the server name at the top of the channel list and open Server Settings.
Go to the Webhooks tab. Enter a name (e.g. "GitHub Bot"), choose a channel, and click Create Webhook.
Click Copy next to the generated URL. Save it somewhere safe - that URL is your credential.
The webhook URL looks like:
https://rift.modeminc.com/v1/webhooks/{id}/{secret}
You can also manage webhooks here: rename, change avatar, refresh the secret, or delete.
Sending Messages
Send a POST to your webhook URL with a JSON body. Only Content-Type: application/json is required.
Fields
| Field | Required | Description |
|---|---|---|
| content | required | The message text. Max 2000 characters. All Rift formatting works here. |
Quick test
curl -X POST YOUR_WEBHOOK_URL \ -H 'Content-Type: application/json' \ -d '{"content": "Hello from a webhook!"}'
With widgets
Use Rift widgets to build rich messages. Stat blocks are especially useful for join/leave notifications:
{
"content": "Gav2011 has joined the server.\n[stat:Username:Gav2011][stat:Account Age:7957 days]\n[stat:Time Spent:6 minutes]"
}
Code Examples
# Simple message curl -X POST YOUR_WEBHOOK_URL \ -H 'Content-Type: application/json' \ -d '{"content":"Deployment complete"}' # With formatting curl -X POST YOUR_WEBHOOK_URL \ -H 'Content-Type: application/json' \ -d '{"content":"**Build #42 passed** [pill:OK:green]"}' # Stat block notification curl -X POST YOUR_WEBHOOK_URL \ -H 'Content-Type: application/json' \ -d '{"content":"Server alert!\n[stat:CPU:89%][stat:RAM:74%][bargraph:89%:red]"}'
const WEBHOOK = 'YOUR_WEBHOOK_URL'; async function notify(content) { await fetch(WEBHOOK, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) }); } // Simple alert notify('Server restarted'); // Rich stat notification notify(`Gav2011 joined.\n[stat:Username:Gav2011][stat:Level:42][stat:Playtime:166h]`); // CI/CD status notify(`**Deploy complete** on \`main\` [pill:LIVE:green]\nhttps://myapp.com`);
import requests WEBHOOK = "YOUR_WEBHOOK_URL" def notify(content: str): requests.post(WEBHOOK, json={"content": content}).raise_for_status() # Simple notify("Backup complete") # Stat block notify( "Gav2011 left the server.\n" "[stat:Username:Gav2011][stat:Account Age:7957 days][stat:Time Spent:6 minutes]" ) # GitHub Actions import os notify( f"**{os.environ['GITHUB_WORKFLOW']}** finished on `{os.environ['GITHUB_REF_NAME']}` [pill:PASSED:green]" )
package main import ( "bytes"; "encoding/json"; "net/http" ) const webhook = "YOUR_WEBHOOK_URL" func notify(content string) { body, _ := json.Marshal(map[string]string{"content": content}) http.Post(webhook, "application/json", bytes.NewReader(body)) } func main() { notify("Deploy complete [pill:LIVE:green]") notify("Tests failed [pill:FAIL:red]") }
What are Bots?
Rift bots are automated accounts your code controls. They appear in chat with a BOT badge next to their name and can send messages, react to messages, read channel history, and listen to real-time events via WebSocket - all from a server you write yourself.
Unlike webhooks (which only post messages), bots are full participants. They see everything happening in a server in real time and can respond intelligently.
MESSAGE_CREATE, REACTION_ADD, MEMBER_JOIN, VOICE_JOIN and more over a WebSocket gateway.Creating an Application
Go to the Developer Portal and sign in with your Rift account.
Click + New Application. Enter a name (2-32 characters) and an optional description, then click Create.
Your bot token is shown once only. Copy it immediately and store it somewhere safe - you cannot view it again. If you lose it, regenerate it from the portal (which invalidates the old token).
Optionally upload a profile picture for your bot by clicking the avatar in Manage.
Token format
Bot tokens look like:
rbt_bot_XXXXXXXXXXXXXXXXXX.YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
Use it as an Authorization header in every API call:
Authorization: Bot rbt_bot_XXXX.YYYY
Installing a Bot
Each bot has an invite link that server owners use to add it:
https://modeminc.com/rift/bot-invite/{bot_id}
Copy your invite link from the Developer Portal (the Copy Invite Link button on your app card). When a server owner opens it they can choose which of their servers to add the bot to.
A server member with Manage Server permission can also install via the API directly:
# Server owner installs the bot (uses user JWT, not bot token) curl -X POST https://rift.modeminc.com/v1/api/bots/{bot_id}/install \ -H 'Authorization: Bearer {user_jwt}' \ -H 'Content-Type: application/json' \ -d '{"server_id": "srv_abc123"}'
To remove the bot from a server:
curl -X DELETE https://rift.modeminc.com/v1/api/bots/{bot_id}/install/{server_id} \ -H 'Authorization: Bearer {user_jwt}'
Real-time Gateway
Connect your bot to the WebSocket gateway to receive events in real time. Your bot will receive events for every server it is installed in.
Connecting
wss://rift.modeminc.com/v1/bot/gateway?token={bot_token}
Bot prefix). The Authorization header approach also works if your WebSocket client supports it: Authorization: Bot {token}.
READY event
Immediately after connecting you receive a READY event with your bot info and the list of server IDs you are in:
{
"type": "READY",
"payload": {
"bot": {
"id": "bot_XXXX",
"name": "My Bot",
"avatar_hash": "1234567890",
"verified": false
},
"guilds": ["srv_abc", "srv_xyz"]
}
}
Events
| Event type | When it fires |
|---|---|
message_create | A message is sent in any channel in a server the bot is in |
message_update | A message is edited |
message_delete | A message is deleted |
reaction_add | A user adds an emoji reaction |
reaction_remove | A user removes a reaction |
member_join | A user joins a server |
member_leave | A user leaves or is kicked from a server |
voice_state_update | A user joins or leaves a voice channel |
presence_update | A member's online status changes |
Event shape
All events follow the same envelope:
{
"type": "message_create",
"payload": {
"id": "msg_XXXX",
"channel_id": "ch_XXXX",
"content": "Hello!",
"timestamp": 1748800000,
"author": {
"id": "usr_XXXX",
"username": "gavin",
"avatar_hash": "abc",
"bot": false
}
},
"meta": { "server_id": "srv_XXXX", "channel_id": "ch_XXXX", "timestamp": 1748800000 }
}
JavaScript example
const BOT_TOKEN = 'rbt_bot_XXXX.YYYY'; const ws = new WebSocket( `wss://rift.modeminc.com/v1/bot/gateway?token=${BOT_TOKEN}` ); ws.addEventListener('message', (event) => { const { type, payload, meta } = JSON.parse(event.data); if (type === 'READY') { console.log('Connected as', payload.bot.name); } if (type === 'message_create' && !payload.author.bot) { // Reply to "!ping" if (payload.content === '!ping') { sendMessage(payload.channel_id, 'Pong!'); } } }); async function sendMessage(channelId, content) { await fetch( `https://rift.modeminc.com/v1/bot/channels/${channelId}/messages`, { method: 'POST', headers: { 'Authorization': `Bot ${BOT_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ content }) } ); }
REST API
All bot REST endpoints are under https://rift.modeminc.com/v1/bot/ and require the header:
Authorization: Bot {your_bot_token}
Get bot info
GET /v1/bot/me
List guilds (installed servers)
GET /v1/bot/guilds
List channels in a server
GET /v1/bot/guilds/{server_id}/channels
List members in a server
GET /v1/bot/guilds/{server_id}/members
Get voice states in a server
GET /v1/bot/guilds/{server_id}/voice
Read messages
GET /v1/bot/channels/{channel_id}/messages?limit=50
| Param | Description | |
|---|---|---|
| limit | optional | Number of messages to return. Max 100, default 50. |
Send a message
POST /v1/bot/channels/{channel_id}/messages Content-Type: application/json { "content": "Hello from my bot!" }
| Field | Description | |
|---|---|---|
| content | required | Message text. Max 2000 characters. All Rift widget formatting supported. |
Delete a message
Bots can only delete their own messages.
DELETE /v1/bot/channels/{channel_id}/messages/{message_id}
Add a reaction
POST /v1/bot/channels/{channel_id}/messages/{message_id}/reactions/{emoji}
URL-encode the emoji: %F0%9F%91%8D for π, %E2%9D%A4%EF%B8%8F for β€οΈ.
Edit a message
Bots can only edit their own messages.
PATCH /v1/bot/channels/{channel_id}/messages/{message_id} Content-Type: application/json { "content": "Edited text" }
Assign / change a member's roles
Requires the manage_roles permission. Replaces the member's roles with the list you send (send an empty array to clear them).
PUT /v1/bot/guilds/{server_id}/members/{user_id}/roles Content-Type: application/json { "role_ids": ["role_abc", "role_def"] }
Add a single role to a member
Requires the manage_roles permission. Adds just this one role, leaving the member's other roles untouched. Returns the member's full role_ids after the change.
PUT /v1/bot/guilds/{server_id}/members/{user_id}/roles/{role_id}
Remove a single role from a member
Requires the manage_roles permission. Removes just this one role.
DELETE /v1/bot/guilds/{server_id}/members/{user_id}/roles/{role_id}
Add / remove several roles at once
Requires the manage_roles permission. Adds and/or removes multiple roles in a single atomic request, leaving the member's other roles untouched. Prefer this over firing several single-role calls in parallel - those can race and clobber each other. Returns the member's full role_ids after the change.
PATCH /v1/bot/guilds/{server_id}/members/{user_id}/roles Content-Type: application/json { "add": ["role_a", "role_b", "role_c"], "remove": ["role_x"] }
Kick a member
Requires the kick_members permission.
DELETE /v1/bot/guilds/{server_id}/members/{user_id}
Create a channel
Requires the manage_channels permission. type is one of text, voice, stage, announcement, forum, rules, log (defaults to text).
POST /v1/bot/guilds/{server_id}/channels Content-Type: application/json { "name": "general", "type": "text", "topic": "optional", "category_id": "optional" }
Delete a channel
Requires the manage_channels permission.
DELETE /v1/bot/channels/{channel_id}
Move a member to another voice channel
Requires the move_members permission. Works for both mesh and SFU voice channels.
POST /v1/bot/guilds/{server_id}/voice/{user_id}/move Content-Type: application/json { "to_channel_id": "ch_xxxx" }
Disconnect a member from voice
Requires the move_members permission.
POST /v1/bot/guilds/{server_id}/voice/{user_id}/disconnect
Error responses
| Status | Meaning |
|---|---|
401 | Missing or invalid bot token |
403 | Bot is not installed in that server |
404 | Channel or message not found |
400 | Validation error (e.g. content too long) |
Verified Bot Badge
Any user can create a bot and use it right away - no approval required. The β VERIFIED badge is an optional distinction for bots that are widely used, well-maintained, and serve a clear purpose.
Verified bots display a green checkmark next to the BOT badge in every message they send.
Open the Developer Portal and find your application.
Click Apply for Verified Badge. Write a short reason describing your bot's purpose and how many servers it's in.
The Rift team reviews the application. Approved bots get the badge applied immediately and it appears in all past and future messages.
What are Apps?
A Rift App is an interactive mini-program with a native block UI and your own backend. Unlike a webhook or a bot (which post messages), an app draws a live panel - buttons, inputs, cards, progress bars - inside a channel, and reacts when members tap it. Dashboards, game-server managers, tools, status boards.
The model is socket mode: your app connects out to Rift over a single WebSocket. There's no inbound port, no public URL, and nothing to upload - Rift never runs or hosts your code. Your program runs wherever you like (laptop, VPS, Raspberry Pi, a Worker with Node compat) and dials home.
App vs Bot
- Bot - reads and posts messages in real time (see the Bots section). Token auth, message gateway + REST.
- App - renders an interactive block panel in a channel and handles taps. Socket mode, block UI. That's this section.
Get Started
A complete, live app in about ten lines.
1. Create the app & get a token
Open the developer portal β Apps β New App. You get a master token that starts with rapp_ - for your own testing. Put it in an env var; never commit it.
export RIFT_TOKEN="rapp_your_token_here"
2. Install the SDK
npm i @modeminc/rift-sdk
3. Write & run it
import { RiftApp, Blocks } from '@modeminc/rift-sdk';
const app = new RiftApp(process.env.RIFT_TOKEN);
let count = 0;
const view = () => [
Blocks.header('Hello π'),
Blocks.text(`Clicked **${count}** times`),
Blocks.button('Click me', 'click', 'success'),
];
app.onReady(() => app.render(view())); // draw when we connect
app.onAction('click', () => { // a button was tapped
count++;
app.render(view()); // redraw
});
app.connect();
Run it (node app.js, or npx tsx app.ts). It stays connected and keeps your UI live.
4. Add it to a channel
In Rift, open the in-app App Store and add your app to a channel. Members see the panel and tap it; your backend gets the events. Each install gets its own per-server connect token (starts with rappi_) - that's the token that goes in their copy of the app.
WebSocket. See Develop: Server & Networking.The App Home
An app has one home - the block UI it's currently showing. When you call app.render(blocks), Rift stores those blocks as the home and shows them to everyone viewing the app's panel.
How rendering reaches viewers
- render() replaces the stored home (send it as often as you like).
- Viewers' panels refresh the home every ~3 seconds, so a background
render()shows up within a few seconds. - When a member taps something, their client re-fetches the home immediately after the interaction - so responses to a tap feel instant even though passive updates poll.
render() inside its handler (the tapper sees it at once). Use passive polling for slow background changes (a server coming online, a counter ticking).One app, many installs
Your rapp_ master token drives the app's own preview home. Each place someone installs the app gets a separate rappi_ token and its own independent home - so a game-server-manager app can show a different panel per server, each driven by its own connection.
Limits
- A home holds up to 60 blocks and 24 KB of serialized JSON.
- Blocks are declarative and trusted - no scripts, no arbitrary HTML.
Blocks (Design)
Blocks are the whole UI vocabulary. Each is a plain object; the Blocks.* builders just return them, so you can mix builders and literals freely. Rift draws them natively.
Content
Blocks.header('Server Status') // big title
Blocks.text('**Online** - 42 players', { muted: false }) // markdown; muted dims it
Blocks.badge('LIVE', '#22c55e') // small pill (hex or accent name)
Blocks.image('https://β¦/graph.png') // an image
Blocks.divider() // horizontal rule
Blocks.spacer(12) // vertical gap, 2-48px
Blocks.code(['> buildingβ¦', '> done']) // monospace console (string or lines[])
Blocks.keyValue([{ k: 'CPU', v: '38%' }, { k: 'Uptime', v: '3d' }]) // two-column list; v is markdown
Status
Blocks.progress(72) // a bar, 0-100 Blocks.steps(['Connect', 'Configure', 'Run'], 1) // stepper; arg 2 = active index (earlier = done)
Interactive
Blocks.button('Restart', 'restart', 'danger', 'srv-1') // style: success | danger | neutral; last arg = optional value
Blocks.input('name', { label: 'Name', placeholder: 'Ada' })
Blocks.input('pw', { label: 'Password', password: true })
Blocks.textarea('note', { label: 'Note' })
Blocks.select('theme', ['blue', 'green'], { label: 'Theme', value: 'blue' })
Blocks.select('srv', [{ label: 'Main', value: 's1' }], { label: 'Server' })
Layout
Blocks.card([ /* children */ ], 'Optional title') // bordered container
Blocks.section([ /* children */ ], 'Optional title') // like card
Blocks.row([ Blocks.button('Yes','yes','success'), Blocks.button('No','no') ]) // equal columns
text and the v of a keyValue row render Markdown (bold, italic, code, links). Everything else is plain text.Develop: Client
The "client" is the code that draws the UI and handles taps - the block builders plus the event handlers.
The render loop
Keep a view() function that returns the current blocks from your state, and call app.render(view()) whenever the state changes. That's the entire mental model.
Inputs & forms
Give each input, textarea and select an id. The live value of every input in the view is delivered with each interaction, in i.values, keyed by that id - so one Save button reads the whole form:
const view = () => [
Blocks.input('host', { label: 'Host', placeholder: 'play.example.com' }),
Blocks.input('port', { label: 'Port', value: '25565' }),
Blocks.select('mode', ['survival', 'creative'], { label: 'Mode' }),
Blocks.button('Save', 'save', 'success'),
];
app.onAction('save', (i) => {
console.log(i.values.host, i.values.port, i.values.mode); // the whole form
// β¦persist, then redraw a confirmation:
app.render([ Blocks.header('Saved β'), Blocks.text(`Host: ${i.values.host}`) ]);
});
Routing taps
app.onAction('restart', (i) => { /* i.value = the button's value, e.g. 'srv-1' */ });
app.onInteraction((i) => { /* every tap - i.action_id, i.user, i.value, i.values */ });
The i.user object tells you who tapped (id, username, display_name).
Develop: Server & Networking
The "server" is your program - it runs anywhere and owns your state, secrets and logic. Rift only sees the blocks you render and the taps it relays.
Socket mode
Your app opens one outbound WebSocket to Rift and keeps it open. Because the connection is outbound, there are no ports to open and no public URL - it works behind NAT, on a home PC, anywhere with internet.
wss://rift.modeminc.com/v1/apps/gateway?token=<YOUR_TOKEN>
Tokens
rapp_β¦- your app's master token (dev/preview). Drives the app's own home. Keep it private.rappi_β¦- a per-install token, minted when someone adds your app somewhere. Each drives that install's own home. This is the token that ships in their copy.
The raw wire protocol
The SDK is a thin wrapper over three messages. You can speak them directly (Node 22+ has a global WebSocket, zero installs):
const TOKEN = process.env.RIFT_TOKEN;
const ws = new WebSocket("wss://rift.modeminc.com/v1/apps/gateway?token=" + encodeURIComponent(TOKEN));
let n = 0;
const render = () => ws.send(JSON.stringify({ t: "render", blocks: [
{ type: "header", text: "Hello π" },
{ type: "text", text: "Clicked **" + n + "** times" },
{ type: "button", text: "Click me", action_id: "click", style: "success" },
]}));
ws.addEventListener("message", (e) => {
const m = JSON.parse(e.data);
if (m.t === "ready") render(); // connected - draw now
if (m.t === "interaction" && m.action_id === "click") { n++; render(); }
});
| Direction | Message | Meaning |
|---|---|---|
| Rift β app | { t: "ready" } | Connected (also carries the current stored blocks). Render now. |
| Rift β app | { t: "interaction", user, action_id, value?, values? } | Someone tapped. values has every input's live value. |
| app β Rift | { t: "render", blocks: [ β¦ ] } | Store & show this UI (β€ 60 blocks, β€ 24 KB). |
Reconnecting
The SDK auto-reconnects (tunable via reconnect / reconnectDelayMs) and re-fires onReady each time - so always render() in onReady, not just once. On the raw protocol, reconnect yourself and re-render on the next ready.
API Reference: Client (Blocks)
Every builder returns a plain Block. Fields marked * are required.
| Builder | Block |
|---|---|
header(text*) | { type:'header', text } |
text(text*, {muted}) | { type:'text', text, muted? } - markdown |
divider() | { type:'divider' } |
spacer(size=10) | { type:'spacer', size } - 2-48px |
badge(text*, color?) | { type:'badge', text, color? } - hex or accent name |
image(url*) | { type:'image', url } |
button(text*, action_id*, style='neutral', value?) | { type:'button', text, action_id, style, value? } - style: successβdangerβneutral |
input(id*, {label,placeholder,value,password}) | { type:'input', id, β¦ } |
textarea(id*, {label,placeholder,value}) | { type:'textarea', id, β¦ } |
select(id*, options*, {label,value}) | { type:'select', id, options, β¦ } - options: string or {label,value} |
keyValue(rows*) | { type:'keyvalue', rows:[{k,v}] } - v is markdown |
progress(value*) | { type:'progress', value } - 0-100 |
code(text β lines*) | { type:'code', text } or { type:'code', lines } |
steps(steps*, current=0) | { type:'steps', steps, current } |
card(children*, title?) | { type:'card', children, title? } |
section(children*, title?) | { type:'section', children, title? } |
row(children*) | { type:'row', children } - equal columns |
API Reference: Server (RiftApp)
Constructor
new RiftApp('rapp_β¦') // token string
new RiftApp({
token: 'rapp_β¦', // required
url: 'wss://rift.modeminc.com/v1/apps/gateway', // default
reconnect: true, // default
reconnectDelayMs: 2000, // default
})
Handlers
| Method | Fires |
|---|---|
onReady(fn) | every (re)connect - render here |
onAction(actionId, fn) | one button's action_id. fn(i) |
onInteraction(fn) | every tap. fn(i) |
on('connect'β'disconnect'β'error'β'message', fn) | lifecycle + raw frames |
Methods
| Method | Does |
|---|---|
connect() | open the socket (auto-reconnects unless disabled) |
render(blocks) | draw / redraw the home |
close() | disconnect and stop reconnecting |
The interaction object i
{
user: { id, username?, display_name? }, // who tapped
action_id: 'save', // the button's action_id
value: 'srv-1', // the button's optional value
values: { host: 'play.example.com', β¦ }, // live value of EVERY input, by id
}
What is RiftScript?
RiftScript is a tiny automation language built into every server. With it you can react to messages, hand out (or remove) roles, post replies, and move members between voice channels - all without running a bot. The server owner and administrators can edit it under Server Settings β Code. (Because a script can hand out roles, it's intentionally locked to that level - a regular moderator can't use it to elevate themselves.)
It is deliberately simple and safe: there are no loops, variables, or network access, so it can run on every message and reaction without slowing your server down or doing anything you didn't write. One script lives per server and runs automatically once saved and enabled.
New to it? The editor has a row of Templates (Welcome, Reaction role, AFK command, and more) - click one to drop a ready-made rule into your script, then tweak the names to match your server.
on β¦ (a trigger), followed by a { β¦ } block of actions. Comments start with # or // and can be on their own line or at the end of a line.
# When anyone says "ping", reply with pong
on message "ping" {
reply "pong π"
}
Triggers
A trigger decides when a rule runs. Text matching is case-insensitive.
on message "hello" # message is exactly "hello" on message contains "spoiler" # message contains the word on message starts "!" # message starts with "!" on reaction "β " # β added to ANY message on reaction "β " on "msg_id" # β added to ONE specific message on unreaction "β " # β REMOVED from a message (also: on removereaction) on unreaction "β " on "msg_id" # β removed from ONE specific message on join # a member joins the server on leave # a member leaves the server
The member who sent the message, added or removed the reaction, or joined is called the actor - actions like giverole and movevoice apply to them. on unreaction is the mirror of on reaction: pair them to give a role when someone reacts and take it back when they un-react.
on reaction "β
" on "the_id". Without on "id", the rule fires for that emoji on every message.
Conditions
Wrap actions in an if block to run them only when a condition is true. Add not to invert it.
if hasrole "Pong Master" { β¦ } # actor has this role
if hasrole "Mod", "Admin" { β¦ } # actor has ANY of these roles
if not hasrole "Muted" { β¦ } # actor does NOT have the role
if inchannel "general", "chat" { β¦ } # event was in any of these channels
if isowner { β¦ } # actor is the server owner
"Pong Master". List several, comma-separated, and the condition is true if any match - e.g. if hasrole "Mod", "Admin". Names or copied ids both work.
Add an else { β¦ } block to run something when the condition is false:
on message "!verify" {
if hasrole "Verified" {
reply "You're already verified β
"
} else {
giverole "Verified"
reply "You're verified now! π"
}
}
Actions
Actions are what a rule does. Most take one quoted value; a few (delete, removeallroles) take none.
reply "text" # reply to the triggering message
send "text" # post in the same channel
send "mod-log" "text" # post into a named channel instead
react "π" # add a reaction to the triggering message
delete # delete the last message this rule sent
delete "msg_id" # delete one specific message
wait "5" # pause 5 seconds before the next action
giverole "Verified" # give the actor a role
removerole "Muted" # remove a role from the actor
removeallroles # strip every role from the actor
movevoice "AFK" # move the actor into a voice channel
moveallvoice "General" "AFK" # move EVERYONE from one voice channel to another
post "https://your-api.com" "{}" # POST a JSON body to a URL
get "https://your-api.com/ping" # GET a URL
delete with no value removes the last message this rule sent this run; if the rule hasn't sent one yet, it removes the message that triggered it. Give it a message id to delete a specific message (it must be in your server). wait "5" pauses before the next action - handy for a temporary message: reply "Verifyingβ¦", giverole "Verified", wait "3", delete. Waits are capped (up to 10s each, 20s total per run).
Give send two values to target another channel: the first is the channel, the second is the message. The channel must be a text or announcement channel in the same server.
on join / on leave there is no triggering message, so reply and react do nothing - use send "channel" "text" to post a welcome/goodbye. giverole and movevoice work and apply to the member who joined or left. {user} is their name.
Names or IDs
Anywhere you name a role or channel (giverole, removerole, movevoice, and the send target) you can use either its name or its copied ID. Right-click a role or channel and choose Copy ID to paste it in - handy when two roles share a name or a name has tricky characters.
on reaction "β
" {
giverole "role_a1b2c3d4" # by id
giverole "Verified" # or by name - both work
}
Calling an external API (post / get)
post "url" "json" sends a JSON POST; get "url" sends a GET. Use them to call your own webhook or API when something happens. The URL and body can include {user} / {mention}.
on message "!deploy" {
if hasrole "Maintainer" {
post "https://api.yoursite.com/deploy" "{\"by\":\"{user}\"}"
reply "Deploy triggered."
}
}
on message "!status" {
get "https://api.yoursite.com/ping"
}
http/https URLs are allowed, requests time out after 5 seconds, and the response is never read back into Rift - a script can trigger your API but can't pull data back in.
Inside any reply or send text you can use placeholders and references:
{user}- the member's name (plain text){mention}- pings the member (renders as a clickable @mention){args}- everything the member typed after the command word{arg1},{arg2}, β¦ - each individual word after the command- paste a channel id (
ch_β¦) and it becomes a clickable #channel link
Arguments let you build commands that take input. They work in any value - including role/channel names - so a member can type the value:
# A member types: !role Movie Night
on message starts "!role " {
giverole "{args}" # gives the "Movie Night" role
reply "Gave you {args} π¬"
}
# A member types: !say hello everyone
on message starts "!say " {
if hasrole "Mod" { send "{args}" }
}
For on message starts "β¦", {args} is the text after the trigger word. For on message contains it's the whole message, and for an exact on message "β¦" there are no arguments.
Variables
Store a value with set name = "value" and reuse it anywhere as {name}. The value can include arguments and other placeholders, and is worked out at the moment you set it. Variables live for a single run of the script (they are not saved between messages).
on message starts "!hi " {
set who = "{arg1}"
giverole "Member" "{who}"
reply "Welcome {who}! π"
}
Targeting another member
By default giverole, removerole, removeallroles and movevoice act on the member who triggered the rule. Add a target as the last value - a user id, username, or an argument - to act on someone else (they must be a member of your server):
giverole "Muted" "usr_abc123" # give a role to a specific member
removeallroles "{arg1}" # a member types: !wipe usr_abc123
movevoice "AFK" "{arg1}" # move a named member to AFK
Moving a whole voice channel
moveallvoice "fromVC" "toVC" moves everyone currently in one voice channel into another - so you can build your own command:
# a mod types: !moveall General AFK (names or channel ids both work)
on message starts "!moveall " {
if hasrole "Mod" {
moveallvoice "{arg1}" "{arg2}"
}
}
if hasrole "Mod" or if isowner so a regular member can't run !wipe on someone else.
on join {
giverole "Newcomer"
// greet them and point to the rules channel
send "welcome" "Welcome {mention}! Please read the rules in ch_0syK4QTs3kAc π"
}
reply and send are posted as {ServerName}_RiftScript. They're saved like normal messages, so they survive reloads and a moderator can delete them. reply always posts in the channel that triggered it; to post elsewhere use send "channel" "text" with a channel name or id. Each event runs at most 25 actions.
For react, type a : in the Code editor to open an emoji picker, or paste any emoji directly. You can also react with a plain word - it's stored as-is.
Examples
Only let one role get a reply (everyone else is ignored):
on message "ping" {
if hasrole "Pong Master" {
reply "pong π"
react "π"
}
}
Reaction roles - react on one specific message (copy its ID first) to self-assign a role:
on reaction "β
" on "msg_aBc123" {
giverole "Verified"
}
on reaction "π" on "msg_aBc123" {
giverole "Announcements"
}
Owner-only command to wipe all of your own roles:
on message "!resetmyroles" {
if isowner {
removeallroles
reply "All your roles were removed."
}
}
Move yourself to the AFK channel with a command:
on message "!afk" {
movevoice "AFK"
}
Welcome new members and auto-assign a starter role; say goodbye when they leave:
on join {
giverole "Newcomer"
send "welcome" "Welcome to the server, {user}! π"
}
on leave {
send "welcome" "{user} just left. π"
}
Log a keyword to a separate mod channel:
on message contains "report" {
send "mod-log" "{user} mentioned a report."
}
Auto-react to feedback posts, but only in one channel:
on message contains "feedback" {
if inchannel "suggestions" {
react "π"
react "π"
}
}
Limits
RiftScript only does the specific triggers, conditions, and actions listed on this page - it's not a general-purpose programming language.
- Scoped to your server. A script only ever touches its own server's roles, channels, members, and messages - never other servers, DMs, or accounts.
- Outbound requests are write-only.
postandgetcan call externalhttp/httpsAPIs, but the response is never read back into Rift - a script can trigger your API, not pull data back in. - Bounded execution. Each event runs at most 25 actions, there are no loops, and a script that fails to parse never runs.
- Owner/admin only. Because a script can hand out roles, only the server owner and administrators can edit it.
HTML Pages
An HTML channel renders a custom web page (or an embedded external URL) right inside a server. They're created and edited by the Rift owner, and the page runs in a locked-down sandbox: it can use scripts, forms and popups, but it cannot read your token, your messages, or anything else in Rift.
Theme API
So your page can match the user's Rift appearance, Rift sends the current theme to your page with postMessage. Listen for it:
<script>
window.addEventListener('message', (e) => {
if (e.data && e.data.rift === true && e.data.type === 'theme') {
// e.data.theme is "light", "dark", or "custom"
document.documentElement.dataset.theme = e.data.theme;
}
});
// Optional: ask for the theme immediately on load instead of waiting.
parent.postMessage({ rift: true, type: 'getTheme' }, '*');
</script>
- Rift sends the theme once when your page loads, and again every time the user changes their theme - so you can re-style live, no refresh needed.
themeis one of"light","dark", or"custom". "System" resolves to"dark", and all the premium themes (Core, Quantum, Lofi, etc.) report as"custom"- treatcustomas dark unless you have your own palette.- You can request the current theme at any time by posting
{ rift: true, type: 'getTheme' }toparent; Rift replies with the same theme message.
Rich Presence (Rift_Lib.dll)
Show a custom "Playing β¦" activity on the user's Rift profile from your own app or game. Link Rift_Lib.dll, call one method, done.
How it works
Your app connects to the locally running Rift desktop app over a named pipe (rift-rpc). Rift - already signed in - publishes the activity for the user. Your app never receives a token, key, or any account access; it can only set the activity line, which the user can disable entirely with Activity Privacy. Any app running as the same user can set presence.
false - your game keeps working.
Get the library
Rift_Lib.dll targets .NET Standard 2.0, so it works from .NET Framework, .NET 5+/Core, and Unity, with no dependencies. Add it as a reference (or drop it in your Unity Assets/Plugins folder).
Usage (C#)
using Rift;
// Set (or update) the activity - call it whenever your state changes.
RiftPresence.SetActivity(
name: "My Cool Game", // required - the game/app name (max 64 chars)
details: "In the Neon District", // optional - shown instead of name if given
state: "Score 4,200", // optional - appended when it fits
imageUrl: "https://cdn.modeminc.com/β¦", // optional - must be a RIFT-HOSTED image (see below)
start: DateTime.UtcNow); // optional - shows an elapsed "for HH:MM" timer
// When the player leaves the match / returns to a menu:
RiftPresence.Clear();
// On shutdown (Rift also auto-clears when your app exits):
RiftPresence.Disconnect();
API
| Method | What it does |
|---|---|
bool Connect(int timeoutMs = 1500) | Optional - connect to Rift up front. SetActivity/Clear also connect on demand. Returns false if Rift isn't running. |
bool SetActivity(name, details, state, imageUrl, start) | Set or update the activity. Only name is required; the rest are optional named args. |
bool Clear() | Remove the activity. |
void Disconnect() | Close the connection. Rift clears your activity when the pipe drops. |
bool IsConnected { get; } | Whether a connection to Rift is currently open. |
Doing it without the DLL
The DLL is just a thin client for the rift-rpc named pipe. From any language, connect to \\.\pipe\rift-rpc and write one JSON object per line:
{"op":"set","name":"My Cool Game","details":"In the Neon District","state":"Score 4,200","image":"https://cdn.modeminc.com/β¦","since":1737400000000}
{"op":"clear"}
since is a Unix timestamp in milliseconds. Closing the pipe clears the activity.
cdn.modeminc.com URL). An arbitrary URL is rejected - your activity is shown to everyone who can see you, and each of their clients would load the image, leaking their IP to whoever runs that host. Upload your art in the developer portal and use the Rift URL it gives you. (Portal upload for presence art is rolling out; for now the icon is optional.)Core & Quantum
Rift is free. Core and Quantum unlock extra features for power users.
For full pricing details, feature comparisons, and to subscribe, visit the Rift Pricing page.