External Client

Hey team, I'm trying to migrate my app from partykit to Rivet on Vercel functions. I need to accept a media stream from Twilio. I can't seem to figure out how this is best done in Rivet. I have a Convex backend and NextJS api routes as possible backend solutions - neither one is capable of upgrading a WebSocket request. In Partykit this is handled by the CF Worker and the fetch is forwarded to the Durable Object. I don't want to have to run a Cloudflare Worker just to upgrade the websocket but it seems like that may be required for this use case? I thought about providing the Rivet endpoint directly to Twilio, but there's no way to tell Twilio to use the protocols required by Rivet.
13 Replies
gizmo_jake_04110
gizmo_jake_04110OP2mo ago
thinking this might be something I need to wait until Bun support on Vercel to run?
Nathan
Nathan2mo ago
hey! i missed this msg, apologies for the delay. can you send me docs on how twilio works for the websockets? are they trying to open a websocket -> your server?
Nathan
Nathan2mo ago
rivet can accept raw websockets, but they currently require specifying a custom protocol for the token (https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#protocols) we're going to add support for adding the token in the query instead. let me know if that's what you need and we'll bump that up on the roadmap
MDN Web Docs
WebSocket: WebSocket() constructor - Web APIs | MDN
The WebSocket() constructor returns a new WebSocket object and immediately attempts to establish a connection to the specified WebSocket URL.
gizmo_jake_04110
gizmo_jake_04110OP2mo ago
Twilio Media Streams don't support query params or protocol. They just send a standard websocket request to whichever URL and expect that you upgrade the Request and establish the connection from there. At first glance this sounds like a good use-case for the forward section of your docs:
import { Hono } from "hono";
import { upgradeWebSocket } from "hono/ws";
import { serve } from "@hono/node-server";
import { registry } from "./registry";

const { client } = registry.start();

const app = new Hono();

// Forward WebSocket connections to actor's WebSocket handler
app.get("/ws/:name", upgradeWebSocket(async (c) => {
const name = c.req.param("name");

// Connect to WebSocket
const actor = client.counter.getOrCreate(name);
let actorWs = await actor.websocket("/");

return {
onOpen: async (evt, ws) => {
// Bridge actor WebSocket to client WebSocket
actorWs.addEventListener("message", (event) => {
ws.send(event.data);
});

actorWs.addEventListener("close", () => {
ws.close();
});
},
onMessage: (evt, ws) => {
// Forward message to actor WebSocket
actorWs.send(evt.data);
},
onClose: (evt, ws) => {
// Forward close to actor WebSocket
actorWs.close();
},
};
}));

serve(app);
import { Hono } from "hono";
import { upgradeWebSocket } from "hono/ws";
import { serve } from "@hono/node-server";
import { registry } from "./registry";

const { client } = registry.start();

const app = new Hono();

// Forward WebSocket connections to actor's WebSocket handler
app.get("/ws/:name", upgradeWebSocket(async (c) => {
const name = c.req.param("name");

// Connect to WebSocket
const actor = client.counter.getOrCreate(name);
let actorWs = await actor.websocket("/");

return {
onOpen: async (evt, ws) => {
// Bridge actor WebSocket to client WebSocket
actorWs.addEventListener("message", (event) => {
ws.send(event.data);
});

actorWs.addEventListener("close", () => {
ws.close();
});
},
onMessage: (evt, ws) => {
// Forward message to actor WebSocket
actorWs.send(evt.data);
},
onClose: (evt, ws) => {
// Forward close to actor WebSocket
actorWs.close();
},
};
}));

serve(app);
But that's only if you have a long-running server available. Part of the value of using Rivet is, of course, to not have to maintain a long-running server. I'm not sure how you'd go about implementing this. It works fine with Partykit, but only because they can pair a CF Worker with a CF Durable Object. From what I can see, Rivet is missing the "Worker" piece. unless you can make it possible to upgrade any standard websocket request and use handlers like onBeforeConnect to handle auth?
Nathan
Nathan2mo ago
@gizmo_jake_04110 i just implemented this last night. going to prod in a few. will send docs later today. the websocket url will look like:
https://api.rivet.dev/raw/connect?x_rivet_target=actor&x_rivet_actor=<actor_id>&x_rivet_namepsace=<ns>&x_rivet_token=<token>
https://api.rivet.dev/raw/connect?x_rivet_target=actor&x_rivet_actor=<actor_id>&x_rivet_namepsace=<ns>&x_rivet_token=<token>
Part of the value of using Rivet is, of course, to not have to maintain a long-running server.
yep – the goal here is to make sure this is the case
I'm not sure how you'd go about implementing this.
the key here is that we need to make sure you can address each actor with a unique url, as i sent above
Twilio Media Streams don't support query params
i can't find this in the docs. do they just strip query params altogether?
The url does not support query string parameters. To pass custom key value pairs to the WebSocket, make use of Custom Parameters instead.
nvm found it. ok, let me see if i can update the schema to use a path instead of qp
gizmo_jake_04110
gizmo_jake_04110OP2mo ago
Awesome, thank you! That should work perfectly if its possible. Then onAuth can be used to verify the Twilio signature and I can handle the messages from there
Nathan
Nathan2mo ago
@gizmo_jake_04110 this is (re)implemented, going live tomorrow. url format will look like:
https://api.rivet.dev/gateway/actors/{actor id}/route/raw/websocket
https://api.rivet.dev/gateway/actors/{actor id}/route/raw/websocket
i'll ping you when it's up
gizmo_jake_04110
gizmo_jake_04110OP2mo ago
Is this live?
Nathan
Nathan2mo ago
not yet – it will today. decided to include a handful of bug fixes first.
gizmo_jake_04110
gizmo_jake_04110OP2mo ago
cool! if you can remember to tag me when its live that'd be great, otherwise I'll check back. Excited to start testing it out @Nathan this end up going live?
Nathan
Nathan2mo ago
nope, ended up with a bit of feature creep around this i promise it's going up today haha wanted to make it a bit cleaner for people to use, since we keep getting a lot of requests for raw ws
Nathan
Nathan2mo ago
just released 2.0.22 the websocket path is /gateway/{actor_id}/raw/websocket. in prod, you'll need a token so it'll look like /gateway/{actor_id}@{token}/raw/websocket here's an example: https://github.com/rivet-dev/rivet/blob/d6f2ea0727971c91dafe3f26b871b39f0138ad74/examples/cursors-raw-websocket/src/frontend/App.tsx#L93 this is going live on Rivet Cloud tomorrow, but you can test locally by updating RivetKit
GitHub
rivet/examples/cursors-raw-websocket/src/frontend/App.tsx at d6f2ea...
An open-source library for long-lived processes with realtime, persistence, and hibernation - rivet-dev/rivet
Nathan
Nathan3w ago
this shipped to rivet cloud a while ago, forgot to update you

Did you find this page helpful?