- Go 100%
| data/iot | ||
| loadtest | ||
| .gitignore | ||
| go.mod | ||
| go.sum | ||
| main.go | ||
| README.md | ||
Simplix
Simplix is an embedded Go JSON document database built on BadgerHold and Badger.
It is designed for small apps, IoT devices, and local-first tools where the data shape changes often and the API should stay easy to read.
Import Into Your Project
Copy the simplix folder into your Go project.
If your go.mod file says:
module myapp
then import the library like this:
import "myapp/simplix"
Example:
package main
import (
"fmt"
"myapp/simplix"
)
func main() {
app := simplix.OpenApp("data")
defer app.Close()
doc, _ := app.DB("iot").C("readings").Put("temp", 23.23)
fmt.Println(doc.ID)
}
For this repository, the module name is simplix, so the example app imports the library with:
import "simplix/simplix"
Portable Folder Use
Simplix stores data inside the folder you pass to OpenApp or Open.
Use a relative path when you want the app folder to be portable:
app := simplix.OpenApp("data")
defer app.Close()
With this setup, keep your program and the data folder together. You can copy the whole project or compiled app folder to a USB flash drive, move it to another computer, and keep using the same local database from that folder.
Avoid hard-coded absolute paths such as C:\Users\name\... if the folder needs to move between computers.
app := simplix.OpenApp("data")
defer app.Close()
readings := app.DB("iot").C("readings")
doc, _ := readings.Put(map[string]any{
"temp": 23.23,
"device": "esp32-kitchen",
"at": time.Now(),
})
latest, _ := readings.
Match(`device == "esp32-kitchen" && temp > 20`).
Desc("at").
Limit(10).
All()
fmt.Println(doc.ID, latest[0].Float("temp"))
Databases And Tables
Open databases by name:
app := simplix.OpenApp("data")
defer app.Close()
iot := app.DB("iot")
logs := app.DB("logs")
devices := iot.C("devices")
events := logs.C("events")
C("devices"), Collection("devices"), and Table("devices") do the same thing.
Writing Data
devices := app.DB("iot").C("devices")
devices.Put(map[string]any{
"id": "esp32",
"online": true,
})
readings := app.DB("iot").C("readings")
readings.Put(map[string]any{
"temp": 23.23,
"device": "esp32",
"at": time.Now(),
})
readings.PutMany(
map[string]any{"device": "esp32", "temp": 21.1},
map[string]any{"device": "esp32", "temp": 22.4},
)
You can also use key/value pairs, or pass a JSON string/raw bytes when your data already comes from an API, MQTT payload, or file.
devices.Put("id", "dev-1", "room", "garage")
devices.Put(`{"id":"dev-2","room":"lab","online":true}`)
devices.Update("dev-1", "online", true, "metrics.rssi", -61)
Nested fields use dot notation:
devices.Where("metrics.rssi").Lt(-50).All()
Finding Data
hot, _ := readings.
Where("temp").Gt(25).
Where("device").Eq("esp32").
Latest("at").
Limit(20).
All()
offline, _ := devices.
Where("online").Eq(false).
OrderBy("room", "-battery").
All()
For flexible queries, use Match. Simplix uses expr, so filters can read like small Go-like expressions:
hot, _ := readings.
Match(`temp > 25 && device == "esp32"`).
Desc("temp").
All()
weak, _ := devices.
Match(`battery < 20 || online == false`).
All()
Useful field filters:
devices.Where("online").Is(true).All()
devices.Where("device").Not("esp32").All()
devices.Where("room").In("lab", "garage").All()
devices.Where("room").Out("office").All()
devices.Where("metrics.rssi").Lt(-50).All()
devices.Where("name").Contains("kitchen").All()
devices.Where("firmware").Exists().All()
devices.Where("deleted_at").Missing().All()
Useful sorting:
readings.OrderBy("device", "-at").All()
readings.Asc("device").Desc("at").All()
readings.Latest("created_at").Limit(50).All()
Auto Indexes
Simplix automatically indexes scalar document fields and hidden time fields as documents are written.
Indexed fields work with:
readings.Where("device").Eq("esp32").All()
readings.Where("created_at").Gt(time.Now().Add(-10 * time.Minute)).Count()
readings.Where("at").TimeBetween(from, to).All()
readings.Where("battery").Between(20, 80).All()
readings.Where("room").In("lab", "garage").All()
Result Control
first, _ := readings.
Where("device").Eq("esp32").
Latest("at").
First()
latest10, _ := readings.
Latest("at").
Take(10).
All()
page2, _ := readings.
OrderBy("-created_at").
Page(2, 25).
All()
total, _ := readings.
Where("device").Eq("esp32").
Count()
Distinct documents and values:
types, _ := readings.
Where("room").Eq("lab").
OrderBy("typ").
DistinctValues("typ")
latestByType, _ := readings.
Where("room").Eq("lab").
Latest("created_at").
Distinct("typ")
fmt.Println("types:", len(latestByType))
latestByDeviceAndType, _ := readings.
Where("room").Eq("lab").
Latest("created_at").
Limit(8).
Distinct("id", "typ")
for _, doc := range latestByDeviceAndType {
fmt.Println(doc.ID, doc.Text("id"), doc.Text("typ"))
}
Distinct fields are free-form. Use any document fields you need, for example Distinct("typ"), Distinct("room", "typ"), or Distinct("id", "typ").
Distinct(fields...) returns full documents as []simplix.Doc. It keeps the first document for each unique field combination after sorting, so use Latest("created_at") when you want the newest document per group.
DistinctValues(field) is a shortcut when you only need one field as []any.
Use "_id" or "$id" only if you need Simplix's internal document id instead of a JSON field named id.
Page(2, 25) means page 2 with 25 documents per page. It is the same as Skip(25).Limit(25).
Example list page:
page := 1
size := 20
items, _ := readings.
Where("device").Eq("esp32").
OrderBy("-created_at").
Page(page, size).
All()
total, _ := readings.
Where("device").Eq("esp32").
Count()
fmt.Println("items:", len(items), "total:", total)
Updating Data
Update one document by id:
devices := app.DB("iot").C("devices")
doc, _ := devices.Save("esp32-kitchen", map[string]any{
"room": "kitchen",
"online": false,
"battery": 44,
})
devices.Update(doc.ID,
"online", true,
"metrics.rssi", -61,
)
Update many documents with Where:
updated, _ := devices.
Where("room").Eq("kitchen").
Update(
"online", true,
"checked_at", time.Now(),
)
Deleting Data
Delete one document by id:
devices.Delete("esp32-kitchen")
Delete many documents with Where:
deleted, _ := readings.
Where("created_at").Lt(time.Now().Add(-24*time.Hour)).
Delete()
Time And IoT Queries
Time values are stored as RFC3339Nano strings, so they stay JSON-friendly and sortable.
Every record also gets hidden system timestamps: created_at and updated_at. They are not included in doc.Data, doc.JSON(), or doc.Pretty(), but you can filter and sort with them.
from := time.Now().Add(-time.Hour)
to := time.Now()
docs, _ := readings.
Where("at").TimeBetween(from, to).
Sort("device", "-at").
All()
recent, _ := readings.
Where("created_at").TimeBetween(from, to).
Latest("created_at").
All()
Check if a device stopped sending:
last, err := readings.
Where("device").Eq("esp32-kitchen").
Latest("created_at").
First()
if err == simplix.ErrNotFound {
fmt.Println("device never sent data")
} else if last.CreatedAt().Before(time.Now().Add(-5 * time.Minute)) {
fmt.Println("device stopped sending")
}
Changes
Changes are in-process and near real time. They fire after Put, Save, Update, and Delete.
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
events := readings.
Where("device").Eq("esp32-kitchen").
Changes(ctx)
for event := range events {
fmt.Println(event.Type, event.Doc.JSON())
}
Flexible change streams:
events := readings.
Match(`device == "esp32-kitchen" && temp > 25`).
Changes(ctx)
for event := range events {
fmt.Println(event.Type, event.Doc.JSON())
}
MQTT
Simplix can store MQTT JSON payloads directly. A good Go MQTT client is github.com/eclipse/paho.mqtt.golang.
app := simplix.OpenApp("./data")
defer app.Close()
readings := app.DB("iot").C("readings")
opts := mqtt.NewClientOptions().
AddBroker("tcp://localhost:1883").
SetClientID("simplix")
client := mqtt.NewClient(opts)
client.Connect().Wait()
client.Subscribe("devices/+/telemetry", 0, func(c mqtt.Client, msg mqtt.Message) {
doc, err := readings.Put(msg.Payload()) // payload: {"temp":23.23,"device":"esp32"}
if err != nil {
fmt.Println("mqtt payload ignored:", err)
return
}
fmt.Println("stored", doc.ID)
})
Publish database changes back to MQTT:
events := readings.Changes(ctx)
for event := range events {
client.Publish("simplix/readings/changes", 0, false, event.Doc.JSON())
}
Backup And Export
Use Badger backup files for fast server moves:
app.DB("iot").Backup("./backup/iot.badger")
app.DB("logs").Backup("./backup/logs.badger")
Use JSONL when you want readable exports or migration files:
app.DB("iot").ExportJSONL("iot.jsonl")
app.DB("iot").ImportJSONL("iot.jsonl")
One JSONL line looks like this:
{"id":"r1","collection":"readings","created_at":"2026-06-04T12:00:00Z","updated_at":"2026-06-04T12:00:00Z","data":{"temp":23.23}}
API Shape
OpenApp(root)opens multiple named embedded databases from one root folder.DB(name)opens a named database inside an app.C(name),Collection(name), orTable(name)selects a collection.db.Backup(path)writes a Badger backup file.db.ExportJSONL(path)exports readable JSONL.db.ImportJSONL(path)imports readable JSONL.collection.Put(data)creates a document with an automatic id.collection.PutMany(data...)inserts many documents faster in batches.collection.PutID(id, data)creates a document with your id.collection.Save(id, data)creates or replaces one document.collection.Get(id)reads one document.collection.Update(id, pairs...)patches one document.collection.Delete(id)deletes one document.collection.Where(field)starts a query.doc.CreatedAt()returns the hidden insert time.doc.UpdatedAt()returns the hidden update time.collection.Match(expr)starts a flexible expression query.collection.Changes(ctx)watches every change in a collection.query.Changes(ctx)watches matching changes.query.Take(n)orquery.Limit(n)controls how many documents are returned.query.Skip(n)skips documents.query.Page(page, size)returns one page of documents.query.First()returns the first matching document.query.Count()counts matching documents.query.Distinct(fields...)returns full documents, one per unique field combination.query.DistinctValues(field)returns unique values for one field.query.Update(pairs...)patches all matching documents.query.Delete()deletes all matching documents.
Author
Regimantas baublys