Integration Guide
Decide which transport, storage, and auth strategy fits your app
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:
mkdir my-teleportal-app && cd my-teleportal-appbun init -ybun add teleportal yjs crossws srvxmkdir my-teleportal-app && cd my-teleportal-appnpm init -ynpm install teleportal yjs crossws srvxCreate 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 storageconst server = new Server({ storage: new MemoryDocumentStorage(),});
// Set up WebSocket handlersserve({ 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 syncawait provider.synced;
// Create a Y.js text type and insert contentconst ytext = provider.doc.getText("content");ytext.insert(0, "Hello, world!");
console.log("Document updated:", ytext.toString());
// Listen for updates from other clientsprovider.doc.on("update", () => { console.log("Document updated:", ytext.toString());});
// Flush pending messages before cleanupawait 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.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:
bun run server.tsnode --experimental-strip-types server.tsThe server starts listening on ws://localhost:3000. Then in a separate terminal, run the client:
bun run client.tsnode --experimental-strip-types client.tsYou 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.
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