From 469268b4493695494aefc4711e9cb1e1038eb649 Mon Sep 17 00:00:00 2001 From: nenharper Date: Fri, 3 Oct 2025 22:46:37 -0500 Subject: [PATCH 1/2] Add docs for realtime --- docs/reference/components/applications.md | 1 - .../developers/components/managing.md | 1 - .../developers/components/managing.md | 1 - .../version-4.6/developers/real-time/index.md | 27 ++++ .../version-4.6/developers/real-time/mqtt.md | 79 ++++++++++ .../version-4.6/developers/real-time/sse.md | 98 +++++++++++++ .../developers/real-time/websockets.md | 136 ++++++++++++++++++ .../reference/components/applications.md | 1 - .../{developers => reference}/real-time.md | 0 9 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 versioned_docs/version-4.6/developers/real-time/index.md create mode 100644 versioned_docs/version-4.6/developers/real-time/mqtt.md create mode 100644 versioned_docs/version-4.6/developers/real-time/sse.md create mode 100644 versioned_docs/version-4.6/developers/real-time/websockets.md rename versioned_docs/version-4.6/{developers => reference}/real-time.md (100%) diff --git a/docs/reference/components/applications.md b/docs/reference/components/applications.md index 2af170bf..0bde610c 100644 --- a/docs/reference/components/applications.md +++ b/docs/reference/components/applications.md @@ -39,7 +39,6 @@ Alternatively, to mimic interfacing with a hosted Harper instance, use operation package= \ restart=true ``` - - Make sure to omit the `target` option so that it _deploys_ to the Harper instance running locally - The `package=` option creates a symlink to the application simplifying restarts - By default, the `deploy` operation command will _deploy_ the current directory by packaging it up and streaming the bytes. By specifying `package`, it skips this and references the file path directly diff --git a/versioned_docs/version-4.4/developers/components/managing.md b/versioned_docs/version-4.4/developers/components/managing.md index 97402e39..8b8d03cc 100644 --- a/versioned_docs/version-4.4/developers/components/managing.md +++ b/versioned_docs/version-4.4/developers/components/managing.md @@ -37,7 +37,6 @@ Alternatively, to mimic interfacing with a hosted Harper instance, use operation package= \ restart=true ``` - - Make sure to omit the `target` option so that it _deploys_ to the Harper instance running locally - The `package=` option creates a symlink to the component simplifying restarts - By default, the `deploy_component` operation command will _deploy_ the current directory by packaging it up and streaming the bytes. By specifying `package`, it skips this and references the file path directly diff --git a/versioned_docs/version-4.5/developers/components/managing.md b/versioned_docs/version-4.5/developers/components/managing.md index 97402e39..8b8d03cc 100644 --- a/versioned_docs/version-4.5/developers/components/managing.md +++ b/versioned_docs/version-4.5/developers/components/managing.md @@ -37,7 +37,6 @@ Alternatively, to mimic interfacing with a hosted Harper instance, use operation package= \ restart=true ``` - - Make sure to omit the `target` option so that it _deploys_ to the Harper instance running locally - The `package=` option creates a symlink to the component simplifying restarts - By default, the `deploy_component` operation command will _deploy_ the current directory by packaging it up and streaming the bytes. By specifying `package`, it skips this and references the file path directly diff --git a/versioned_docs/version-4.6/developers/real-time/index.md b/versioned_docs/version-4.6/developers/real-time/index.md new file mode 100644 index 00000000..04bdee15 --- /dev/null +++ b/versioned_docs/version-4.6/developers/real-time/index.md @@ -0,0 +1,27 @@ +--- +title: Real-Time +--- + +# Real-Time + +Modern applications demand experiences that update instantly; collaboration apps that feel alive, IoT dashboards that react the moment sensors change, and user interfaces that always show the freshest data without reloads. Harper makes this possible by embedding real-time communication directly into the database layer. Instead of bolting on a separate broker or managing multiple systems, you get structured real-time data and messaging as a first-class capability. + +Harper real-time is built around database tables. Any declared table can double as a messaging topic, which means you don’t just publish and subscribe, you also persist, query, and synchronize that data across a distributed cluster. This is where Harper is different from a generic pub/sub hub: it treats data as structured records, not raw messages, and it speaks standard protocols so you can connect from any environment. + +You can get started with real-time in Harper in a single step by adding a table to your schema: + +```graphql +type Dog @table @export +``` + +From here, clients can subscribe to this topic, publish structured messages, and receive updates in real-time. Content negotiation is built in: one client can publish JSON, while another consumes CBOR or MessagePack. The database handles translation seamlessly. + +Harper supports several real-time protocols so you can pick the one that best fits your application architecture: + +- [MQTT](./real-time/mqtt) for IoT and event-driven systems, with tight integration to database records. +- [WebSockets](./real-time/websockets) for full-duplex connections and interactive applications. +- [Server Sent Events (SSE)](./real-time/sse) for lightweight one-way streaming to the browser. + +Each protocol page in this section gives you background on the protocol itself, shows how Harper implements it, and walks you through how to use it in your apps. + +👉 Whether you’re building dashboards, chat apps, or IoT systems, Harper gives you real-time capabilities without extra infrastructure; just connect and start streaming. diff --git a/versioned_docs/version-4.6/developers/real-time/mqtt.md b/versioned_docs/version-4.6/developers/real-time/mqtt.md new file mode 100644 index 00000000..f0bf8f2a --- /dev/null +++ b/versioned_docs/version-4.6/developers/real-time/mqtt.md @@ -0,0 +1,79 @@ +--- +title: MQTT +--- + +# MQTT + +MQTT is widely used for lightweight, event-driven communication, especially when you need devices and apps to stay updated in real time. In Harper, MQTT is integrated directly into the database, so your topics map to real records instead of being just abstract channels. This makes it possible to persist state, stream updates, and control how data flows across distributed servers, all while using a standard protocol. + +To make this concrete, let’s use a `Dog` table for a pet adoption app. Every record in this table automatically becomes an MQTT topic. That means if you store a dog with the ID `123`, you can subscribe to `dog/123` and immediately receive updates whenever that record changes. + +```graphql +type Dog @table @export { + id: ID @id + name: String + breed: String +} +``` + +## Subscribe to a record + +When a client subscribes to `dog/123`, Harper first delivers the current record value as a retained message, then streams every update or deletion that follows. This ensures your app always has the latest state without needing a separate fetch. + +## Publish updates + +Publishing to the same topic updates or notifies subscribers depending on whether the retain flag is used. + +- Retained messages update the record, replacing its state. +- Non-retained messages leave the record unchanged but notify subscribers. + +This gives you control over whether messages represent state or events. + +## Wildcards for multiple records + +To follow more than one dog at once, you can subscribe with a trailing multi-level wildcard: + +```bash +dog/# +``` + +This streams notifications for every record in the `Dog` table. + +## Configuration + +MQTT is enabled by default, but you can adjust ports, TLS, and authentication in your `harperdb-config.yaml`: + +```yaml +mqtt: + network: + port: 1883 + securePort: 8883 + webSocket: true + mTLS: false + requireAuthentication: true +``` + +For more advanced options (like enabling mTLS or customizing ports), see the [Configuration page](../../deployments/configuration). + +👉 If you connect over WebSockets, remember to include the sub-protocol: `Sec-WebSocket-Protocol: mqtt`. + +## Event hooks + +On the server side, you can hook into MQTT lifecycle events to log or react when clients connect, fail authentication, or disconnect: + +```javascript +server.mqtt.events.on('connected', (session, socket) => { + console.log('client connected with id', session.clientId); +}); +``` + +## Ordering and delivery + +Because Harper is distributed, messages can arrive out of order depending on network paths. Delivery rules are: + +- **Non-retained messages** - always delivered, even if delayed or out of order. +- **Retained messages** - only the latest state is kept across all nodes. + +This lets you handle use cases like chat (where every message matters) and IoT telemetry (where the latest reading matters most). + +👉 Next: explore [WebSockets with Harper](./websockets) for full-duplex connections directly in the browser. diff --git a/versioned_docs/version-4.6/developers/real-time/sse.md b/versioned_docs/version-4.6/developers/real-time/sse.md new file mode 100644 index 00000000..1399d649 --- /dev/null +++ b/versioned_docs/version-4.6/developers/real-time/sse.md @@ -0,0 +1,98 @@ +--- +title: Server-Sent Events (SSE) +--- + +# Server-Sent Events (SSE) + +There are times when you don’t need a two-way connection. The client never needs to send data back, it only needs to stay in sync with what the server knows. For example, a dog adoption dashboard might show the availability of each dog, and all it has to do is keep that list updated in real time. + +SSE (Server-Sent Events) is perfect for this. It gives you a persistent one-way stream from the server to the client, using simple HTTP. Harper makes this stream available on any resource path. + +## Opening a connection to a record + +On the client, you create an `EventSource` that points to the dog you want to follow. + +```javascript +const es = new EventSource('https://server/dog/341', { withCredentials: true }); +``` + +When the connection opens, it stays alive until you close it. Messages arrive as events you can listen for. + +```javascript +es.onmessage = (event) => { + const data = JSON.parse(event.data); + renderDogProfile(data); // update the profile in your UI +}; +``` + +## Handling errors and reconnects + +SSE connections will automatically try to reconnect if they drop, but you should still handle errors gracefully. + +```javascript +es.onerror = (err) => { + console.error('sse error', err); + showConnectionWarning(); +}; +``` + +When the user leaves the profile page, close the connection cleanly. + +```javascript +es.close(); +``` + +## Adding custom events from the server + +By default, connecting to `/dog/341` streams record updates. But Harper also lets you enrich that stream by defining a `connect()` method on the resource. This lets you send your own events alongside updates. + +```javascript +export class DogStream extends Resource { + connect() { + const outgoing = super.connect(); + + // send a friendly reminder every 30 seconds + const reminder = setInterval(() => { + outgoing.send({ dogId: '341', notice: 'still looking for a home' }); + }, 30000); + + outgoing.on('close', () => { + clearInterval(reminder); + }); + + return outgoing; + } +} +``` + +Now the client receives both the automatic record updates and the server’s notices. + +## Reading server messages on the client + +On the client, branch on fields in the incoming event and update different parts of the UI. + +```javascript +es.onmessage = (event) => { + const msg = JSON.parse(event.data); + + if (msg.notice) { + showNotice(msg.notice); + return; + } + + renderDogProfile(msg); // treat as a record update +}; +``` + +## Why choose SSE for your app + +- Lightweight and browser-native: just new EventSource(). +- Automatic reconnects built into the browser. +- Ideal for dashboards, feeds, and one-way updates. +- Less complex than WebSockets when clients don’t need to send data. + +At this point, your adoption dashboard can show dog profiles that stay fresh automatically, display occasional server notices, and handle connection lifecycle gracefully, all without writing a line of custom client transport code. + +When you want to adjust TLS, authentication, or ports, do that in your Harper configuration file. See the [Configuration page](../../deployments/configuration) for details. + +👉 If you later decide the client should also send messages back (like user comments or notes), [WebSockets](./websockets) give you that two-way channel. diff --git a/versioned_docs/version-4.6/developers/real-time/websockets.md b/versioned_docs/version-4.6/developers/real-time/websockets.md new file mode 100644 index 00000000..e13202d5 --- /dev/null +++ b/versioned_docs/version-4.6/developers/real-time/websockets.md @@ -0,0 +1,136 @@ +--- +title: WebSockets +--- + +# WebSockets + +There are times when you want more than just notifications about record changes. You need a live connection that stays open, one that lets updates stream from the server and messages flow back from the client. That’s exactly what WebSockets provide, and Harper makes them even more powerful by tying them directly to resources. + +Let's imagine you have a dog profile screen open and you want it to stay fresh while the user looks at it. If the record for `dog/341` changes, the page should reflect it right away. A WebSocket gives you a live path from server to client and back again, so the page can breathe in real time. + +You begin on the client by opening a connection to the resource path for that record. + +```javascript +const ws = new WebSocket('wss://server/dog/341'); +``` + +As soon as it connects, confirm it is alive so you can update any loading indicator. + +```javascript +ws.onopen = () => { + console.log('connected to dog/341'); +}; +``` + +Now listen for messages. Each message is JSON that represents either a record update or an event you choose to send from the server. + +```javascript +ws.onmessage = (event) => { + const data = JSON.parse(event.data); + renderDogProfile(data); +}; +``` + +If the connection has trouble, surface it. Errors are rare but silence is painful during development. + +```javascript +ws.onerror = (err) => { + console.error('websocket error', err); +}; +``` + +When the user navigates away, close the socket so the server can clean up. + +```javascript +ws.close(1000, 'leaving dog profile'); // normal closure +``` + +That already gives you a live profile. Any change to `dog/341` flows to the page without reloads or polling. Sometimes, though, you want more than passive updates. You want the page to send signals to the server, and you want the server to push its own messages alongside record changes. + +Send a small message from the client. Keep it simple and structured. + +```javascript +ws.send(JSON.stringify({ dogId: '341', note: 'good with kids' })); +``` + +On the server, define how the resource streams data back to connected clients. You can take full control by implementing `connect(incomingMessages)` on a resource. The method returns a stream. Yield values to push them to the client. + +```javascript +export class DogNotes extends Resource { + async *connect(incomingMessages) { + for await (const message of incomingMessages) { + // relay user notes for this dog + yield { dogId: message.dogId, note: message.note }; + } + } +} +``` + +This turns the profile into a small collaborative space. Anyone connected to `dog/341` can post a note and everyone sees it arrive in real time. + +Sometimes you want to keep the default behavior for record updates and also add your own messages. Call `super.connect()` to get a convenient stream object that you can send on, and that fires a close event when the client leaves. + +```javascript +export class DogStatus extends Resource { + connect(incomingMessages) { + const outgoing = super.connect(); + return outgoing; + } +} +``` + +Add a very small server message so the client can show connection health. A steady pulse makes debugging simple and gives product a tiny hook for a status badge. + +```javascript +const timer = setInterval(() => { + outgoing.send({ dogId: '341', status: 'connection active' }); +}, 1000); +``` + +Let the server react to what the client sends. Here we echo notes back through the stream so everyone connected stays in sync. + +```javascript +incomingMessages.on('data', (message) => { + outgoing.send({ dogId: message.dogId, note: message.note }); +}); +``` + +Clean up properly when the socket closes. No timers left behind. + +```javascript +outgoing.on('close', () => { + clearInterval(timer); +}); +``` + +Return the stream and you are done. + +```javascript +return outgoing; +``` + +Back on the client, keep the message handler tidy. Branch on simple fields and update the page. + +```javascript +ws.onmessage = (event) => { + const msg = JSON.parse(event.data); + + if (msg.note) { + appendNote(msg.dogId, msg.note); + return; + } + + if (msg.status) { + setConnectionBadge(msg.status); + return; + } + + renderDogProfile(msg); // treat as a record update +}; +``` + +At this point the dog profile page is alive. It receives record updates for `dog/341`, it lets users post live notes, and it shows a gentle status pulse so the UI can reflect connection health. All of this runs over one WebSocket to one resource path. + +If you need to tune transport details like TLS, ports, or authentication, keep the page focused and link out rather than dumping settings here. See the [Configuration page](../../deployments/configuration) when you are ready to adjust instance settings. + +👉 Next up, if you prefer a one way stream where the server talks and the client just listens, move to [Server Sent Events (SSE)](./sse). diff --git a/versioned_docs/version-4.6/reference/components/applications.md b/versioned_docs/version-4.6/reference/components/applications.md index 2af170bf..0bde610c 100644 --- a/versioned_docs/version-4.6/reference/components/applications.md +++ b/versioned_docs/version-4.6/reference/components/applications.md @@ -39,7 +39,6 @@ Alternatively, to mimic interfacing with a hosted Harper instance, use operation package= \ restart=true ``` - - Make sure to omit the `target` option so that it _deploys_ to the Harper instance running locally - The `package=` option creates a symlink to the application simplifying restarts - By default, the `deploy` operation command will _deploy_ the current directory by packaging it up and streaming the bytes. By specifying `package`, it skips this and references the file path directly diff --git a/versioned_docs/version-4.6/developers/real-time.md b/versioned_docs/version-4.6/reference/real-time.md similarity index 100% rename from versioned_docs/version-4.6/developers/real-time.md rename to versioned_docs/version-4.6/reference/real-time.md From 99929689ad7640de073d824f62e6dc2faf0d55eb Mon Sep 17 00:00:00 2001 From: Nathan Heskew Date: Tue, 7 Oct 2025 13:57:39 -0700 Subject: [PATCH 2/2] format fix --- docs/reference/components/applications.md | 1 + versioned_docs/version-4.4/developers/components/managing.md | 1 + versioned_docs/version-4.5/developers/components/managing.md | 1 + versioned_docs/version-4.6/reference/components/applications.md | 1 + 4 files changed, 4 insertions(+) diff --git a/docs/reference/components/applications.md b/docs/reference/components/applications.md index 0bde610c..2af170bf 100644 --- a/docs/reference/components/applications.md +++ b/docs/reference/components/applications.md @@ -39,6 +39,7 @@ Alternatively, to mimic interfacing with a hosted Harper instance, use operation package= \ restart=true ``` + - Make sure to omit the `target` option so that it _deploys_ to the Harper instance running locally - The `package=` option creates a symlink to the application simplifying restarts - By default, the `deploy` operation command will _deploy_ the current directory by packaging it up and streaming the bytes. By specifying `package`, it skips this and references the file path directly diff --git a/versioned_docs/version-4.4/developers/components/managing.md b/versioned_docs/version-4.4/developers/components/managing.md index 8b8d03cc..97402e39 100644 --- a/versioned_docs/version-4.4/developers/components/managing.md +++ b/versioned_docs/version-4.4/developers/components/managing.md @@ -37,6 +37,7 @@ Alternatively, to mimic interfacing with a hosted Harper instance, use operation package= \ restart=true ``` + - Make sure to omit the `target` option so that it _deploys_ to the Harper instance running locally - The `package=` option creates a symlink to the component simplifying restarts - By default, the `deploy_component` operation command will _deploy_ the current directory by packaging it up and streaming the bytes. By specifying `package`, it skips this and references the file path directly diff --git a/versioned_docs/version-4.5/developers/components/managing.md b/versioned_docs/version-4.5/developers/components/managing.md index 8b8d03cc..97402e39 100644 --- a/versioned_docs/version-4.5/developers/components/managing.md +++ b/versioned_docs/version-4.5/developers/components/managing.md @@ -37,6 +37,7 @@ Alternatively, to mimic interfacing with a hosted Harper instance, use operation package= \ restart=true ``` + - Make sure to omit the `target` option so that it _deploys_ to the Harper instance running locally - The `package=` option creates a symlink to the component simplifying restarts - By default, the `deploy_component` operation command will _deploy_ the current directory by packaging it up and streaming the bytes. By specifying `package`, it skips this and references the file path directly diff --git a/versioned_docs/version-4.6/reference/components/applications.md b/versioned_docs/version-4.6/reference/components/applications.md index 0bde610c..2af170bf 100644 --- a/versioned_docs/version-4.6/reference/components/applications.md +++ b/versioned_docs/version-4.6/reference/components/applications.md @@ -39,6 +39,7 @@ Alternatively, to mimic interfacing with a hosted Harper instance, use operation package= \ restart=true ``` + - Make sure to omit the `target` option so that it _deploys_ to the Harper instance running locally - The `package=` option creates a symlink to the application simplifying restarts - By default, the `deploy` operation command will _deploy_ the current directory by packaging it up and streaming the bytes. By specifying `package`, it skips this and references the file path directly