IndexedDB
IndexedDB providers back Gestalt’s persistent state layer, modeled on the W3C Indexed Database API . This contract allows Gestalt to be agnostic of relational, document, or key-value backends.
Using IndexedDB from a plugin
Apps use the IndexedDB client from the language SDK. By default, Gestalt
uses the host IndexedDB provider and the app name as the database scope.
Configure apps.<name>.indexeddb only when an app needs a different
provider or database scope.
For example:
server:
providers:
indexeddb: main
providers:
indexeddb:
main:
source:
package: github.com/valon-technologies/gestalt-providers/indexeddb/relationaldb
version: 0.0.1-alpha.2
config:
dsn: ${SYSTEM_DATABASE_URL}
workplaceHub:
source:
package: github.com/valon-technologies/gestalt-providers/indexeddb/relationaldb
version: 0.0.1-alpha.2
config:
dsn: ${APP_DATABASE_URL}
apps:
workplace_hub:
source: ./plugin/manifest.yaml
indexeddb:
provider: workplaceHub
db: workplace_hubThen inside the plugin:
Go
import (
"context"
"log"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
)
ctx := context.Background()
db, err := gestalt.IndexedDB()
if err != nil {
log.Fatal(err)
}
defer db.Close()
tasks := db.ObjectStore("tasks")
if err := tasks.Put(ctx, gestalt.Record{
"id": "task-1",
"title": "Ship docs",
}); err != nil {
log.Fatal(err)
}
record, err := tasks.Get(ctx, "task-1")
if err != nil {
log.Fatal(err)
}
_ = recordNormal app config gives the app one effective IndexedDB binding. If you need a different backing store or a different scoped database name, choose that in YAML instead of encoding transport details in app code.
Advanced reads and writes
The snippets below continue from the basic setup above: db is a connected
IndexedDB client, tasks is the tasks object-store handle, and Go examples
also use ctx. The examples assume primary keys such as task-100 and a
secondary index named by_status.
Range reads
Use a key range when you need a bounded primary-key scan instead of one exact lookup.
Go
recent, err := tasks.GetAll(ctx, &gestalt.KeyRange{
Lower: "task-100",
Upper: "task-199",
})
if err != nil {
log.Fatal(err)
}
_ = recentCursor pagination
IndexedDB pagination is cursor-based. The SDKs do not expose a page-token list API for object stores; keep the last primary key from one cursor page and use an exclusive lower bound for the next page.
Go
func readTaskPage(
ctx context.Context,
tasks *gestalt.ObjectStoreClient,
after string,
limit int,
) ([]gestalt.Record, string, error) {
var keyRange *gestalt.KeyRange
if after != "" {
keyRange = &gestalt.KeyRange{
Lower: after,
LowerOpen: true,
}
}
cursor, err := tasks.OpenCursor(ctx, keyRange, gestalt.CursorNext)
if err != nil {
return nil, "", err
}
defer cursor.Close()
page := make([]gestalt.Record, 0, limit)
var nextAfter string
for len(page) < limit && cursor.Continue() {
record, err := cursor.Value()
if err != nil {
return nil, "", err
}
page = append(page, record)
nextAfter = cursor.PrimaryKey()
}
if err := cursor.Err(); err != nil {
return nil, "", err
}
return page, nextAfter, nil
}
page, nextAfter, err := readTaskPage(ctx, tasks, "", 25)
if err != nil {
log.Fatal(err)
}
_ = page
_ = nextAfterSecondary index queries
Use an index when you need to find or count rows by a field other than the
primary key. In TypeScript, the first index-query argument is the optional key
range; pass undefined before index values when no range is needed.
Go
byStatus := tasks.Index("by_status")
activeCount, err := byStatus.Count(ctx, nil, "active")
if err != nil {
log.Fatal(err)
}
activeKeys, err := byStatus.GetAllKeys(ctx, nil, "active")
if err != nil {
log.Fatal(err)
}
_, _ = activeCount, activeKeysTransactions
Use an explicit transaction when a group of reads and writes should commit or roll back together.
Go
import "time"
tx, err := db.Transaction(
ctx,
[]string{"tasks"},
gestalt.TransactionReadwrite,
gestalt.TransactionOptions{
DurabilityHint: gestalt.TransactionDurabilityStrict,
},
)
if err != nil {
log.Fatal(err)
}
txTasks := tx.ObjectStore("tasks")
now := time.Now().UTC().Format(time.RFC3339)
for _, record := range []gestalt.Record{
{"id": "task-123", "status": "active", "priority": 2, "updatedAt": now},
{"id": "task-124", "status": "queued", "priority": 1, "updatedAt": now},
} {
if err := txTasks.Put(ctx, record); err != nil {
_ = tx.Abort(ctx)
log.Fatal(err)
}
}
if err := tx.Commit(ctx); err != nil {
log.Fatal(err)
}Configuring storage
For local development, SQLite keeps setup simple:
server:
providers:
indexeddb: main
providers:
indexeddb:
main:
source: ./relationaldb/manifest.yaml
config:
dsn: sqlite://gestalt.dbFor production, use the first-party package source:
server:
providers:
indexeddb: main
providers:
indexeddb:
main:
source:
package: github.com/valon-technologies/gestalt-providers/indexeddb/relationaldb
version: 0.0.1-alpha.2
config:
dsn: ${DATABASE_URL}The first-party RelationalDB provider accepts postgres://, mysql://,
sqlite://, and sqlserver:// DSN prefixes.
Other first-party IndexedDB providers use their own config blocks:
providers:
indexeddb:
main:
source:
package: github.com/valon-technologies/gestalt-providers/indexeddb/dynamodb
version: 0.0.1-alpha.1
config:
table: gestalt
region: us-east-1
# endpoint: http://localhost:8000providers:
indexeddb:
main:
source:
package: github.com/valon-technologies/gestalt-providers/indexeddb/mongodb
version: 0.0.1-alpha.1
config:
uri: ${MONGODB_URI}
database: gestaltOperational notes
Gestalt needs exactly one active datastore selected by
server.providers.indexeddb. Published datastore packages can be locked and
prepared with gestaltd lock and gestaltd sync --locked just like other
provider types. Apps still go through the
host’s request and auth model, so they do not talk to the datastore provider
directly.
Building your own IndexedDB provider
Manifest
An IndexedDB provider manifest declares kind: indexeddb:
kind: indexeddb
source: github.com/valon-technologies/gestalt-providers/indexeddb/relationaldb
version: 0.0.1-alpha.2
displayName: RelationalDB
description: IndexedDB provider supporting PostgreSQL, MySQL, SQLite, and SQL Server.
spec:
configSchemaPath: ./datastore_config.jsonProvider interface
You implement the IndexedDB contract, which covers object store lifecycle, primary key CRUD, bulk operations, and index queries. Go providers implement gestalt.IndexedDBProvider.
package relationaldb
import (
"context"
"database/sql"
gestalt "github.com/valon-technologies/gestalt/sdk/go"
)
type RelationalDB struct {
db *sql.DB
}
func New() *RelationalDB { return &RelationalDB{} }
func (r *RelationalDB) HealthCheck(ctx context.Context) error {
return r.db.PingContext(ctx)
}
func (r *RelationalDB) CreateObjectStore(ctx context.Context, name string, schema gestalt.ObjectStoreSchema) error {
return nil
}
func (r *RelationalDB) DeleteObjectStore(ctx context.Context, name string) error {
return nil
}
// Get retrieves a single record by primary key.
func (r *RelationalDB) Get(ctx context.Context, req gestalt.IndexedDBObjectStoreRequest) (gestalt.Record, error) {
return gestalt.Record{}, nil
}
// Add inserts a new record. Fails if the key already exists.
func (r *RelationalDB) Add(ctx context.Context, req gestalt.IndexedDBRecordRequest) error {
return nil
}
// Put inserts or replaces the record at the given key.
func (r *RelationalDB) Put(ctx context.Context, req gestalt.IndexedDBRecordRequest) error {
return nil
}
// Delete removes a record by primary key.
func (r *RelationalDB) Delete(ctx context.Context, req gestalt.IndexedDBObjectStoreRequest) error {
return nil
}
// See the W3C IndexedDB specification for the remaining methods:
// https://www.w3.org/TR/IndexedDB/#object-store-interfaceWhat to read next
For the broader server and provider config model, continue to Configuration. For the catalog of first-party packages, see Built-in Providers.