Awesome
<img src='https://raw.githubusercontent.com/local-first-web/branding/main/svg/relay-h.svg' width='600' alt="@localfirst/relay logo"/>
@localfirst/relay
is a tiny service that helps local-first applications connect with peers on
other devices. It can run in the cloud or on any device with a known address.
Deploy to: Glitch | Heroku | AWS | Google | Azure | local server
Why
<img src='./images/relay-1.png' width='500' align='center' ></img>
Getting two end-user devices to communicate with each other over the internet is hard. Most devices don't have stable public IP addresses, and they're often behind firewalls that turn away attempts to connect from the outside. This is a connection problem.
Even within a local network, or in other situations where devices can be reached directly, devices that want to communicate need a way to find each other. This is a problem of discovery.
What
This little server offers a solution to each of these two problems.
1. Discovery
Alice can provide a documentId
(or several) that she's interested in. (A documentId
is a unique ID for a topic or channel ā it could be a GUID, or just a unique string like
ambitious-mongoose
.)
If Bob is interested in the same documentId
, each will receive an Introduction
message with the other's peerId. They can then use that information to connect.
2. Connection
Alice can request to connect with Bob on a given documentId. If we get matching connection requests from Alice and Bob, we pipe their sockets together.
How
Server
From this repo, you can run this server as follows:
pnpm
pnpm start
You should see something like thsi:
> @localfirst/relay@4.0.0 start local-first-web/relay
> node dist/start.js
š Listening at http://localhost:8080
You can visit that URL with a web browser to confirm that it's working; you should see something like this:
<img src='./images/screenshot.png' width='300' align='center' />Running server from another package
From another codebase, you can import the server and run it as follows:
import { Server } from "@localfirst/relay/Server.js"
const DEFAULT_PORT = 8080
const port = Number(process.env.PORT) || DEFAULT_PORT
const server = new Server({ port })
server.listen()
Client
This library includes a lightweight client designed to be used with this server.
The client keeps track of all peers that the server connects you to, and for each peer it keeps track of each documentId (aka discoveryKey, aka channel) that you're working with that peer on.
import { Client } from "@localfirst/relay/Client.js"
client = new Client({ peerId: "alice", url: "myrelay.somedomain.com" })
.join("ambitious-mongoose")
.on("peer-connect", ({ documentId, peerId, socket }) => {
// `socket` is a WebSocket
// send a message
socket.write("Hello! š")
// listen for messages
socket.addEventListener("data", event => {
const message = event.data
console.log(`message from ${peerId} about ${documentId}`, message)
})
})
ā Security
This server makes no security guarantees. Alice and Bob should probably:
- Authenticate each other, to ensure that "Alice" is actually Alice and "Bob" is actually Bob.
- Encrypt all communications with each other.
The @localfirst/auth library can be used with this relay service. It provides peer-to-peer authentication and end-to-end encryption, and allows you to treat this relay (and the rest of the network) as untrusted.
Server API
The following documentation might be of interest to anyone working on the @localfirst/relay
Client
, or replacing it with a new client. You don't need to know any of this to interact with this server if you're using the included client.
This server has two WebSocket endpoints: /introduction
and /connection
.
In the following examples, Alice is the local peer and Bob is a remote peer. We're using alice
and bob
as their peerId
s; in practice, typically these would be GUIDs that uniquely identify their devices.
/introduction/:localPeerId
:localPeerId
is the local peer's uniquepeerId
.
Alice connects to this endpoint, e.g. wss://myrelay.somedomain.com/introduction/alice
.
Once a WebSocket connection has been made, Alice sends an introduction request containing one or more documentId
s that she has or is interested in:
{
type: 'Join',
documentIds: ['ambitious-mongoose', 'frivolous-platypus'], // documents Alice has or is interested in
}
If Bob is connected to the same server and interested in one or more of the same documents IDs, the server sends Alice an introduction message:
{
type: 'Introduction',
peerId: 'bob', // Bob's peerId
documentIds: ['ambitious-mongoose'] // documents we're both interested in
}
Alice can now use this information to request a connection to this peer via the connection
endpoint:
/connection/:localPeerId/:remotePeerId/:documentId
Once Alice has Bob's peerId
, she makes a new connection to this endpoint, e.g.
wss://myrelay.somedomain.com/connection/alice/bob/ambitious-mongoose
.
:localPeerId
is the local peer's uniquepeerId
.:remotePeerId
is the remote peer's uniquepeerId
.:documentId
is the document ID.
If and when Bob makes a reciprocal connection by connecting to
wss://myrelay.somedomain.com/connection/bob/alice/ambitious-mongoose
, the server pipes their
sockets together and leaves them to talk.
The client and server don't communicate with each other via the connection
endpoint; it's purely a
relay between two peers.
Deployment
Deploying to Glitch
You can deploy this relay to Glitch by clicking this button:
Alternatively, you can remix the local-first-relay project.
Deploying to Heroku
This server can be deployed to Heroku. By design, it should only ever run with a single dyno. You can deploy it by clicking on this button:
Or, you can install using the Heroku CLI as follows:
heroku create
git push heroku main
heroku open
Deploying to AWS Elastic Beanstalk
Install using the AWS CLI:
eb init
eb create
eb open
Deploying to Google Cloud
Install using the Google Cloud SDK:
gcloud projects create my-local-first-relay --set-as-default
gcloud app create
gcloud app deploy
gcloud app browse
Deploying to Azure
Install using the Azure CLI:
az group create --name my-local-first-relay --location eastus
az configure --defaults group=my-local-first-relay location=eastus
az appservice plan create --name my-local-first-relay --sku F1
az webapp create --name my-local-first-relay --plan my-local-first-relay
az webapp deployment user set --user-name PEERID --password PASSWORD
az webapp deployment source config-local-git --name my-local-first-relay
git remote add azure https://PEERID@my-local-first-relay.scm.azurewebsites.net/my-local-first-relay.git
git push azure main
az webapp browse --name my-local-first-relay
AWS Lambda, Azure Functions, Vercel, Serverless, Cloudwatch Workers, etc.
Since true serverless functions are stateless and only spun up on demand, they're not a good fit for this server, which needs to remember information about connected peers and maintain a stable websocket connection with each one.
License
MIT
Prior art
Inspired by https://github.com/orionz/discovery-cloud-server
Formerly known as š Cevitxe Signal Server. (Cevitxe is now @localfirst/state)