Skip to Content

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.

MethodW3C equivalent
CreateObjectStoreIDBDatabase.createObjectStore
DeleteObjectStoreIDBDatabase.deleteObjectStore
GetIDBObjectStore.get
GetKeyIDBObjectStore.getKey
AddIDBObjectStore.add
PutIDBObjectStore.put
DeleteIDBObjectStore.delete
GetAllIDBObjectStore.getAll
GetAllKeysIDBObjectStore.getAllKeys
CountIDBObjectStore.count
ClearIDBObjectStore.clear
DeleteRangeIDBObjectStore.delete (with key range)
IndexGetIDBIndex.get
IndexGetKeyIDBIndex.getKey
IndexGetAllIDBIndex.getAll
IndexGetAllKeysIDBIndex.getAllKeys
IndexCountIDBIndex.count
IndexDeleteIDBIndex.delete
TransactionIDBDatabase.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.json

Provider 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.

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-interface

Plugin 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.

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.db

For 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.

  • Plugins: building plugins that use the datastore SDK
  • Releasing: packaging and publishing provider releases
  • Configuration: full server config reference