Simplix DB
Find a file
Regimantas Baublys d1fb246c60 first commit
2026-06-19 22:06:54 +03:00
data/iot first commit 2026-06-19 22:06:54 +03:00
loadtest first commit 2026-06-19 22:06:54 +03:00
.gitignore first commit 2026-06-19 22:06:54 +03:00
go.mod first commit 2026-06-19 22:06:54 +03:00
go.sum first commit 2026-06-19 22:06:54 +03:00
main.go first commit 2026-06-19 22:06:54 +03:00
README.md first commit 2026-06-19 22:06:54 +03:00

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), or Table(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) or query.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