Skip to content

Getting Started

This tutorial walks you through creating a working real-time collaborative app with Teleportal. By the end, you’ll have a server syncing Y.js documents and a client that connects, edits, and receives live updates.

You need a JavaScript runtime installed. Teleportal works with:

You should also have basic familiarity with Y.js. If you haven’t used Y.js before, What is Teleportal? covers the foundations.

Create a new project and install Teleportal:

Terminal window
mkdir my-teleportal-app && cd my-teleportal-app
bun init -y
bun add teleportal yjs crossws srvx

Create a file called server.ts with the following:

import { serve } from "crossws/server";
import { Server } from "teleportal/server";
import { MemoryDocumentStorage } from "teleportal/storage";
import { getWebsocketHandlers } from "teleportal/websocket-server";
// Create a Teleportal server with in-memory storage
const server = new Server({
storage: new MemoryDocumentStorage(),
});
// Set up WebSocket handlers
serve({
websocket: getWebsocketHandlers({
server,
onUpgrade: async () => {
// Extract user context from the request
// In production, you'd verify authentication here
return {
context: { userId: "user-123", room: "workspace-1" },
};
},
}),
fetch: () => new Response("Not found", { status: 404 }),
});

Here is what each piece does:

  • Server is the core of Teleportal. It manages document sessions, coordinates sync between clients, and handles the Y.js protocol.
  • MemoryDocumentStorage stores documents in memory. This is fine for development – for production, swap in a persistent storage backend like SQLite or Postgres.
  • getWebsocketHandlers bridges Teleportal to a WebSocket server. It handles connection upgrades, message routing, and disconnection cleanup.
  • onUpgrade runs when a client connects. The returned context object is attached to the connection and can be used for access control. In production, you would verify a JWT or session token here.

Create a file called client.ts:

import { Provider, websocketTransport } from "teleportal/providers";
import { createEncryptionKey } from "teleportal/encryption-key";
// Create a provider that connects to the server.
// End-to-end content encryption is the default, so an `encryptionKey` is
// required. Share this key with collaborators out-of-band (e.g. in the URL
// fragment) — the server never sees it.
const provider = await Provider.create({
url: "ws://localhost:3000",
document: "my-document",
encryptionKey: createEncryptionKey(),
transports: [websocketTransport()],
});
// Wait for the document to sync
await provider.synced;
// Create a Y.js text type and insert content
const ytext = provider.doc.getText("content");
ytext.insert(0, "Hello, world!");
console.log("Document updated:", ytext.toString());
// Listen for updates from other clients
provider.doc.on("update", () => {
console.log("Document updated:", ytext.toString());
});
// Flush pending messages before cleanup
await provider.flush();
await provider.destroy();

Here is what each piece does:

  • Provider.create() establishes a WebSocket connection to the server, performs the sync handshake, and returns a ready-to-use provider.
  • websocketTransport() configures the WebSocket transport layer for real-time communication.
  • encryptionKey enables end-to-end content encryption (E2EE), which is on by default. The server never sees your plaintext content. To share a document between clients, they need the same key – distribute it out-of-band (e.g. in the URL fragment). To run without encryption, pass encryptionKey: false.
  • provider.synced resolves once the initial document state has been downloaded from the server.
  • provider.doc is a standard Y.js Y.Doc – use it exactly as you would in any Y.js application.
  • Updates are automatically synced to all other clients connected to the same document.
  • provider.destroy() cleans up the connection when you’re done.

Note: With content-level E2EE the server only ever sees the plaintext CRDT structure and encrypted content sidecars – never your text or keys. Offline persistence stores the encrypted wire representation to IndexedDB, so data at rest is encrypted too. One caveat: awareness/presence routing IDs (clientID/userId) travel in cleartext even though the awareness payload itself is encrypted.

Start the server:

Terminal window
bun run server.ts

The server starts listening on ws://localhost:3000. Then in a separate terminal, run the client:

Terminal window
bun run client.ts

You should see Document updated: Hello, world! printed as the client inserts text and the change round-trips through the server.

To see real-time sync in action, open a second terminal and run the client again. Both clients connect to the same document, so edits from one appear in the other. Since both use createEncryptionKey() without a password, they derive the same encryption key from the document ID ("my-document"), allowing them to decrypt each other’s content. If you modify the client script to insert different text, you’ll see both clients converge to the same state – that’s the CRDT at work.

Note: The document ID-based key derivation is suitable for getting started, but provides minimal security – anyone who knows the document ID can derive the key. For production, consider using createEncryptionKey("password") for password-based encryption, sharing keys via URL fragment (e.g., #token=...), or using the key registry for server-managed key distribution.

In a real application, the client code runs in a browser and connects to a Y.js editor binding (like y-prosemirror, y-monaco, or y-codemirror.next). The Provider works the same way regardless of the environment.

  1. Client connects: The provider establishes a WebSocket connection to the server
  2. Sync protocol: The client and server exchange state vectors to determine what updates are needed
  3. Document sync: Missing updates are sent from server to client, and the document is synchronized
  4. Real-time updates: When you make changes, they’re sent to the server and broadcast to all other clients
  5. Awareness: Cursor positions and user presence are automatically synchronized

Integration Guide

Decide which transport, storage, and auth strategy fits your app

Core Concepts

Understand the protocol, server architecture, and provider lifecycle

Guides

Step-by-step recipes for authentication, persistent storage, scaling, and more