Skip to Content
ProvidersIndexedDB

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_hub

Then inside the plugin:

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) } _ = record

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

recent, err := tasks.GetAll(ctx, &gestalt.KeyRange{ Lower: "task-100", Upper: "task-199", }) if err != nil { log.Fatal(err) } _ = recent

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

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 _ = nextAfter

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

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, activeKeys

Transactions

Use an explicit transaction when a group of reads and writes should commit or roll back together.

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

For 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:8000
providers: indexeddb: main: source: package: github.com/valon-technologies/gestalt-providers/indexeddb/mongodb version: 0.0.1-alpha.1 config: uri: ${MONGODB_URI} database: gestalt

Operational 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.json

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

For the broader server and provider config model, continue to Configuration. For the catalog of first-party packages, see Built-in Providers.