<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Simple IoT - Blog</title>
    <subtitle>Add remote sensor data, telemetry, configuration, and device management to your product. One application runs in the cloud and at the edge, synchronizing data in both directions.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://simpleiot.org/blog/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://simpleiot.org/blog/"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-08-18T00:00:00+00:00</updated>
    <id>https://simpleiot.org/blog/atom.xml</id>
    <entry xml:lang="en">
        <title>Sync is now stream replication</title>
        <published>2026-08-18T00:00:00+00:00</published>
        <updated>2026-08-18T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://simpleiot.org/blog/sync-replication/"/>
        <id>https://simpleiot.org/blog/sync-replication/</id>
        
        <content type="html" xml:base="https://simpleiot.org/blog/sync-replication/">&lt;p&gt;The &lt;a href=&quot;/blog/jetstream-store/&quot;&gt;previous post&lt;/a&gt; covered moving the Simple IoT store
to NATS JetStream. This post covers how we use streams to synchronize
offline/catch-up data. There is also a
&lt;a rel=&quot;noopener noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https://www.youtube.com/watch?v=THmQkxWp9fo&quot;&gt;demo of offline synchronization&lt;/a&gt;
on the Simple IoT YouTube channel.&lt;/p&gt;
&lt;p&gt;Offline synchronization provides several key benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Nothing is lost during network disruptions, which are a fact of life in
distributed systems, especially edge systems.&lt;/li&gt;
&lt;li&gt;We can take down cloud/upstream services (including databases) at any time and
no data is lost. This drastically simplifies operations and maintenance if we
don’t need always-on, highly available infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;the-old-way&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-old-way&quot; aria-label=&quot;Permalink to this section&quot;&gt;The old way&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;In the past, SIOT has synchronized cloud and edge instances using a Merkle-style
hash tree: hash every node, compare hashes up the tree, and push the subtrees
that differ. It worked, but there was a lot of overhead in calculating the
hashes and updating every upstream node every time a point changed.&lt;/p&gt;
&lt;h2 id=&quot;the-new-way&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-new-way&quot; aria-label=&quot;Permalink to this section&quot;&gt;The new way&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Since the SIOT store is now on JetStream, we already have history data
available. The only tricky parts are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How do we handle multiple writers (downstream and upstream)?&lt;/li&gt;
&lt;li&gt;Where do we draw the stream boundaries?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I originally planned one stream per node, but ended up with streams at the
device boundaries, which is simpler and has less overhead.&lt;/p&gt;
&lt;p&gt;Because both the downstream and upstream can write data, there needs to be a
stream for each writer. Sync means each side holds a replica of the other’s
stream, driven by durable NATS consumers: a push pump copies the device’s stream
up, a pull pump copies the hub’s configuration stream down. Current state on
either side is the merge of the two, newest timestamp winning.&lt;/p&gt;
&lt;p&gt;Because no instance ever writes remote data into its own streams, points cannot
echo back and forth between instances. There is no loop to suppress, which
removes an entire class of sync bug by construction.&lt;/p&gt;
&lt;p&gt;A durable consumer remembers its position across disconnects, so a device that
has been offline for two weeks reconnects and receives exactly the backlog, in
order, with original timestamps. No rescan, no comparison, no sync period to
configure. Replication is continuous: a write on one side shows up on the other
as fast as the connection carries it.&lt;/p&gt;
&lt;h2 id=&quot;siot-drastically-simplifies-stream-based-replication&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#siot-drastically-simplifies-stream-based-replication&quot; aria-label=&quot;Permalink to this section&quot;&gt;SIOT drastically simplifies stream-based replication&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;With SIOT, the developer rarely has to be concerned with the low-level stream
details. SIOT handles all of those details under the hood, and all the developer
needs to do is create nodes, points, and the clients that respond to and send
points. The rest happens automatically.&lt;/p&gt;
&lt;p&gt;Distributed systems are hard, especially systems where you can’t assume the
connection is solid and all services will be running all the time. With SIOT, a
lot of this complexity is eliminated. NATS JetStream provides the building
blocks (streams, durable consumers, etc.), and SIOT manages them in a way that
is transparent to the developer.&lt;/p&gt;
&lt;p&gt;The full design is in the
&lt;a rel=&quot;noopener noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https://docs.simpleiot.org/docs/ref/sync.html&quot;&gt;sync reference&lt;/a&gt; and
&lt;a rel=&quot;noopener noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https://docs.simpleiot.org/docs/adr/7-jetstream-store.html&quot;&gt;ADR-7&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>The store is now a stream</title>
        <published>2026-08-13T00:00:00+00:00</published>
        <updated>2026-08-13T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://simpleiot.org/blog/jetstream-store/"/>
        <id>https://simpleiot.org/blog/jetstream-store/</id>
        
        <content type="html" xml:base="https://simpleiot.org/blog/jetstream-store/">&lt;p&gt;Simple IoT 0.23 replaces the SQLite store with
&lt;a rel=&quot;noopener noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https://docs.nats.io/nats-concepts/jetstream&quot;&gt;NATS JetStream&lt;/a&gt;. This is the
biggest architectural change in the project’s history, and it is the first of a
series of posts on what we’ve been building this year. This one covers the store
itself; the next covers what it does for synchronization.&lt;/p&gt;
&lt;h2 id=&quot;two-shapes-of-the-same-data&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#two-shapes-of-the-same-data&quot; aria-label=&quot;Permalink to this section&quot;&gt;Two shapes of the same data&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;SIOT has always been a NATS application. Every component, whether a Modbus
client, a rule, or the UI, communicates by publishing points as messages. The
store’s job was to catch those messages and keep them.&lt;/p&gt;
&lt;p&gt;For years it did that by translating each point into rows in SQLite (and before
that, Bolt and Genji). SQLite served us well, and I would still reach for it in
plenty of applications. But it meant we maintained two shapes of the same data:
the message in flight and the row at rest, with translation code in between, and
a Merkle-style hash tree bolted on top so two instances could figure out how
their rows differed.&lt;/p&gt;
&lt;p&gt;The real problem was never the database. It was that we were translating between
two data models when one of them already did everything we needed.&lt;/p&gt;
&lt;h2 id=&quot;points-are-already-messages&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#points-are-already-messages&quot; aria-label=&quot;Permalink to this section&quot;&gt;Points are already messages&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;A JetStream stream is an append-only log of messages with sequence numbers and
per-subject indexing. That shape matches IoT data unusually well:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Persisting a point means capturing it.&lt;/strong&gt; The same message that moves between
components lands in a stream. No translation layer, no second data model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;History is the natural byproduct.&lt;/strong&gt; A stream retains every point written to
a subject, so time-series history lives in the same place as current state.
The current value of a point is simply the last message on its subject.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sequence numbers replace the hash tree.&lt;/strong&gt; Streams are ordered, so another
instance can replicate one and know exactly what it has and has not seen. More
on this in the next post.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It is already there.&lt;/strong&gt; JetStream runs inside the NATS server every SIOT
instance ships, on cloud machines and small edge devices alike. There is no
separate database process to install or operate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reads never touch JetStream directly. The store keeps the merged current state
in in-memory caches, populated from the subject tips at startup, so queries stay
fast.&lt;/p&gt;
&lt;h2 id=&quot;retention-that-understands-configuration&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#retention-that-understands-configuration&quot; aria-label=&quot;Permalink to this section&quot;&gt;Retention that understands configuration&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Retention is per subject: each point type on each node keeps its own most recent
20,000 messages by default. Per-subject limits have a property that time- or
size-based retention cannot give you: configuration points, written a handful of
times, are effectively kept forever, while a sensor reporting every minute wraps
after about two weeks. Current state is always preserved.&lt;/p&gt;
&lt;p&gt;The default works out to about four months of history for 10-minute data,
bounded on disk even for a device nobody visits. A device with little flash can
lower it with &lt;code&gt;SIOT_STORE_MAX_MSGS_PER_SUBJECT&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;compression-pays-exactly-where-it-matters&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#compression-pays-exactly-where-it-matters&quot; aria-label=&quot;Permalink to this section&quot;&gt;Compression pays exactly where it matters&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Streams are compressed with S2 by default. Point data compresses unusually well,
because the same type repeats in every message, keys come from a small set, and
timestamps march forward. In our testing, a store of 100,000 scraped points went
from 33.4 MB to 11.7 MB. A small store gives up nothing, and compression starts
paying exactly where disk begins to matter.&lt;/p&gt;
&lt;h2 id=&quot;migrating&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#migrating&quot; aria-label=&quot;Permalink to this section&quot;&gt;Migrating&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Existing SQLite databases migrate with &lt;code&gt;siot export&lt;/code&gt; on v0.22.0 followed by
&lt;code&gt;siot import&lt;/code&gt; on 0.23. The full design analysis is recorded in
&lt;a rel=&quot;noopener noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https://docs.simpleiot.org/docs/adr/7-jetstream-store.html&quot;&gt;ADR-7&lt;/a&gt;, and the
&lt;a rel=&quot;noopener noreferrer external&quot; target=&quot;_blank&quot; href=&quot;https://docs.simpleiot.org/docs/ref/store.html&quot;&gt;store reference&lt;/a&gt; covers
retention, compression, and durability tuning in detail.&lt;/p&gt;
&lt;p&gt;This is the kind of simplification that feels right. Stay tuned for how
synchronization works, and how streams simplify so many things.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Hello</title>
        <published>2020-06-23T00:00:00+00:00</published>
        <updated>2020-06-23T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://simpleiot.org/blog/hello/"/>
        <id>https://simpleiot.org/blog/hello/</id>
        
        <content type="html" xml:base="https://simpleiot.org/blog/hello/">&lt;p&gt;The Simple IoT project now has a web site. Stay tuned for more updates!&lt;/p&gt;
</content>
        
    </entry>
</feed>
