IndexedDB
This page covers building custom IndexedDB providers. If you only need to configure storage for a deployment, use Providers > IndexedDB.
IndexedDB providers back the persistent state layer. Rather than inventing a custom storage API, Gestalt adopts the W3C IndexedDB specification , the web platform’s native storage primitive. IndexedDB has been implemented across every major browser for over a decade, its semantics are formally specified, and its concepts (object stores, primary keys, named indexes, key ranges) are already documented and widely understood. By building on this foundation, Gestalt inherits a proven API surface without requiring developers to learn a new abstraction.
IndexedDB’s vocabulary maps naturally onto SQL tables, document collections, and key-value stores alike. This means a single provider contract can target Postgres, SQLite, MongoDB, DynamoDB, and others without leaking backend-specific abstractions. The storage backend is fully replaceable: swap from SQLite to Postgres to MongoDB without changing any application code.
| Method | W3C equivalent |
|---|---|
CreateObjectStore | IDBDatabase.createObjectStore |
DeleteObjectStore | IDBDatabase.deleteObjectStore |
Get | IDBObjectStore.get |
GetKey | IDBObjectStore.getKey |
Add | IDBObjectStore.add |
Put | IDBObjectStore.put |
Delete | IDBObjectStore.delete |
GetAll | IDBObjectStore.getAll |
GetAllKeys | IDBObjectStore.getAllKeys |
Count | IDBObjectStore.count |
Clear | IDBObjectStore.clear |
DeleteRange | IDBObjectStore.delete (with key range) |
IndexGet | IDBIndex.get |
IndexGetKey | IDBIndex.getKey |
IndexGetAll | IDBIndex.getAll |
IndexGetAllKeys | IDBIndex.getAllKeys |
IndexCount | IDBIndex.count |
IndexDelete | IDBIndex.delete |
Transaction | IDBDatabase.transaction |
Transaction is the explicit atomic boundary. A transaction declares its object-store scope, mode (readonly or readwrite), and optional durability hint (default, strict, or relaxed) before any operation runs. Operations inside the stream execute in order with read-your-writes semantics; commit applies the readwrite transaction atomically, abort rolls it back, and operation-level errors abort the transaction.
The examples on this page use the first-party RelationalDB provider, which supports PostgreSQL, MySQL, SQLite, and SQL Server through a single dsn config value.
Manifest
An IndexedDB provider manifest declares kind: indexeddb:
kind: indexeddb
source: github.com/valon-technologies/gestalt-providers/indexeddb/relationaldb
version: 0.0.1-alpha.1
displayName: RelationalDB
description: IndexedDB provider supporting PostgreSQL, MySQL, SQLite, and SQL Server.
spec:
configSchemaPath: ./datastore_config.jsonProvider interface
You implement the IndexedDB gRPC service , which covers object store lifecycle, primary key CRUD, bulk operations, and index queries. The examples below show the core CRUD methods. See the W3C IndexedDB specification for the full interface contract.
Go
package relationaldb
import (
"context"
"database/sql"
"fmt"
"strings"
proto "github.com/valon-technologies/gestalt/sdk/go/gen/v1"
"google.golang.org/protobuf/types/known/emptypb"
)
type RelationalDB struct {
db *sql.DB
}
func New() *RelationalDB { return &RelationalDB{} }
func (r *RelationalDB) Configure(_ context.Context, _ string, config map[string]any) error {
dsn, _ := config["dsn"].(string)
if dsn == "" {
return fmt.Errorf("relationaldb: dsn is required")
}
driver := "sqlite"
connStr := dsn
switch {
case strings.HasPrefix(dsn, "postgres://"):
driver, connStr = "pgx", dsn
case strings.HasPrefix(dsn, "mysql://"):
driver, connStr = "mysql", strings.TrimPrefix(dsn, "mysql://")
case strings.HasPrefix(dsn, "sqlserver://"):
driver, connStr = "sqlserver", dsn
case strings.HasPrefix(dsn, "sqlite://"):
driver, connStr = "sqlite", strings.TrimPrefix(dsn, "sqlite://")
}
var err error
r.db, err = sql.Open(driver, connStr)
return err
}
func (r *RelationalDB) HealthCheck(ctx context.Context) error {
return r.db.PingContext(ctx)
}
// Get retrieves a single record by primary key.
func (r *RelationalDB) Get(ctx context.Context, req *proto.ObjectStoreRequest) (*proto.RecordResponse, error) {
return nil, nil
}
// Add inserts a new record. Fails if the key already exists.
func (r *RelationalDB) Add(ctx context.Context, req *proto.RecordRequest) (*emptypb.Empty, error) {
return nil, nil
}
// Put inserts or replaces the record at the given key.
func (r *RelationalDB) Put(ctx context.Context, req *proto.RecordRequest) (*emptypb.Empty, error) {
return nil, nil
}
// Delete removes a record by primary key.
func (r *RelationalDB) Delete(ctx context.Context, req *proto.ObjectStoreRequest) (*emptypb.Empty, error) {
return nil, nil
}
// See the W3C IndexedDB specification for the remaining methods:
// https://www.w3.org/TR/IndexedDB/#object-store-interfacePlugin SDK
Plugins access the IndexedDB provider through the SDK. The host serves the IndexedDB interface over a gRPC socket, and the SDK wraps it into native types for each language.
When a plugin binds a datastore through plugins.<name>.indexeddb, the host
rebuilds that provider with plugin-scoped storage before exposing it over the
SDK socket. Plugins still use plain store names like tasks or snapshots;
the host applies isolation outside the plugin. For schema-capable backends,
that means a plugin-specific schema. For SQLite-backed relationaldb bindings,
the host falls back to <db>_ table prefixes. Other IndexedDB providers fall
back to transport-level <db>_ store prefixes. plugins.<name>.indexeddb.objectStores
can also allowlist logical stores so the host rejects stores outside that set.
Go
import (
"context"
"log"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
)
db, err := gestalt.IndexedDB()
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create an object store with indexes
db.CreateObjectStore(ctx, "tasks", gestalt.ObjectStoreSchema{
Indexes: []gestalt.IndexSchema{
{Name: "by_user", KeyPath: []string{"user_id"}},
{Name: "by_status", KeyPath: []string{"status"}},
},
})
// CRUD by primary key
tasks := db.ObjectStore("tasks")
tasks.Add(ctx, gestalt.Record{"id": "t1", "user_id": "u1", "status": "pending"})
task, err := tasks.Get(ctx, "t1")
tasks.Put(ctx, gestalt.Record{"id": "t1", "user_id": "u1", "status": "done"})
tasks.Delete(ctx, "t1")
// Query by named index
pending, err := tasks.Index("by_status").GetAll(ctx, nil, "pending")
userTasks, err := tasks.Index("by_user").GetAll(ctx, nil, "u1")
// Atomic delete by index
deleted, err := tasks.Index("by_user").Delete(ctx, "u1")
// Key range queries
recent, err := tasks.GetAll(ctx, gestalt.LowerBound("2024-01-01", false))
// Atomic read-modify-write
tx, err := db.Transaction(ctx, []string{"tasks"}, gestalt.TransactionReadwrite, gestalt.TransactionOptions{})
if err != nil {
log.Fatal(err)
}
defer tx.Abort(ctx)
txTasks := tx.ObjectStore("tasks")
task, err = txTasks.Get(ctx, "t1")
task["status"] = "done"
err = txTasks.Put(ctx, task)
if err == nil {
err = tx.Commit(ctx)
}For SQL-backed providers such as indexeddb/relationaldb, declare columns
when creating an object store so the backend can materialize a concrete table
schema. Document-style providers may ignore columns, but keeping the schema in
the SDK call makes the store portable across both styles of backend.
Config reference
Reference an IndexedDB provider in the providers.indexeddb section, and set server.providers.indexeddb to the name of the active provider. For local development, SQLite keeps things simple with a file path:
server:
providers:
indexeddb: main
providers:
indexeddb:
main:
source: ./relationaldb/manifest.yaml
config:
dsn: sqlite://gestalt.dbFor production, use a published provider-release.yaml metadata URL:
providers:
indexeddb:
main:
source: https://artifacts.example.com/indexeddb/relationaldb/v0.0.1-alpha.1/provider-release.yaml
config:
dsn: ${DATABASE_URL}DATABASE_URL is a connection string for your database. The RelationalDB provider accepts postgres://, mysql://, sqlite://, and sqlserver:// prefixes.
Object store names remain logical inside plugins. The host scopes plugin data by rebuilding schema-capable providers with a plugin-specific schema, or by applying a <db>_ prefix on backends without schema support.
What to read next
- Plugins: building plugins that use the datastore SDK
- Releasing: packaging and publishing provider releases
- Configuration: full server config reference