IndexedDB and Dexie
Overview
Formbird uses IndexedDB as the browser-based database for offline data storage, with Dexie.js as a wrapper library. The dexie-addon-elasticsearch addon translates ElasticSearch queries to Dexie queries, enabling the same query syntax to work both online and offline.
IndexedDB Fundamentals
IndexedDB is a low-level API for client-side storage of structured data. Key characteristics:
- Asynchronous - All operations are non-blocking
- Transactional - Supports ACID transactions
- Index-based - Uses indexes for efficient querying
- Large capacity - Can store hundreds of megabytes
- Same-origin policy - Data isolated per domain
Dexie.js
Dexie.js provides a cleaner API over IndexedDB with:
- Promise-based interface
- Schema versioning and migration
- Bulk operations support
- Addon system for extensions
- Query builder syntax
Database Schema
Collections
The Formbird IndexedDB database contains these collections:
| Collection | Purpose |
|---|---|
documents |
Cached documents, templates, rulesets, ruleset includes |
offlineFiles |
File attachments stored for offline access |
queue |
Documents waiting to sync to server |
keyValueStorage |
User config, session state, offline settings |
Default Indexes
The documents collection always includes these default indexes:
DOCUMENT_INDEX_PENDING_OPERATION: 'systemHeader.offline.status',
DOCUMENT_INDEX_CURRENT_VERSION_OPERATION: 'systemHeader.offline.currentVersionClient',
DOCUMENT_INDEX_VERSION_ID: 'systemHeader.versionId',
DOCUMENT_INDEX_DOCUMENT_ID: 'documentId',
DOCUMENT_INDEX_SYSTEM_TYPE: 'systemHeader.systemType',
DOCUMENT_INDEX_SUMMARY_NAME: 'systemHeader.summaryName',
DOCUMENT_INDEX_SUMMARY_DESCRIPTION: 'systemHeader.summaryDescription',
DOCUMENT_INDEX_NAME: 'name',
Index Configuration
The offlineIndex Document
Indexes are configured in a single offlineIndex document rather than scattered across templates:
{
"documentId": "9425d82f-b25f-4dba-a370-d2b7c7fc289f",
"systemHeader": {
"systemType": "offlineIndex",
"summaryName": "Offline Index Configuration"
},
"indexedDBIndexes": [
"systemHeader.versionId",
"documentId",
"systemHeader.systemType",
"systemHeader.summaryName",
"name",
"*geohash",
"*appTags",
"systemHeader.templateId",
"status",
"assetNumber"
]
}
Index Types
| Prefix | Type | Example | Use Case |
|---|---|---|---|
| (none) | Single value | "name" |
Simple field index |
* |
Multi-entry (array) | "*appTags" |
Array field index |
____ |
Flattened nested | "*parentsRel____documentId" |
Objects in arrays |
Multi-Key (Compound) Indexes
For queries filtering on multiple fields, use comma-separated compound indexes:
{
"indexedDBIndexes": [
"systemHeader.systemType",
"systemHeader.templateId, systemHeader.systemType", // Compound index
"*appTags"
]
}
Creating the offlineIndex Document
For most use cases, you simply need to create a document with systemType: "offlineIndex" and define an array of indexedDBIndexes:
{
"systemHeader": {
"systemType": "offlineIndex",
"summaryName": "Offline Index Configuration",
"keyIds": ["your-offline-key"]
},
"indexedDBIndexes": [
"documentId",
"systemHeader.templateId",
"status",
"*appTags"
]
}
For consolidating indexes from multiple sources:
If you need to consolidate indexes from many existing templates, you can run the MongoDB script:
// db-scripts/Formbird/00001-00050/00019 - 12575 - offlineIndex/12575-offlineIndex.js
// Aggregates indexedDBIndexes from all templates into single offlineIndex document
After running the script, save the document in Formbird to index it in ElasticSearch.
Schema Versioning
IndexedDB requires schema upgrades when indexes change:
- Adding indexes - Triggers automatic schema upgrade
- Removing indexes - Also triggers upgrade (as of version 4.2.57)
- Version tracking - Stored in
keyValueStorageasidbCurrentVersion
// Dexie handles versioning automatically
db.version(1).stores({ documents: "++id,name" });
db.version(2).stores({ documents: "++id,name,status" }); // Added index
Limitation: You cannot change the primary key of a table. Clearing cache and re-syncing is required.
Dexie ElasticSearch Addon
The @formbird/dexie-addon-elasticsearch addon translates ElasticSearch queries to Dexie:
Installation
The addon is loaded dynamically (no core rebuild needed):
{
"clientConfiguration": {
"offline": {
"dexieAddons": [
{
"fileName": "/vendor/offline/dexie-elasticsearch-addon/dexie-elasticsearch-addon.js",
"exportName": "ElasticIndexedDBAddon"
}
]
}
}
}
Supported Query Types
| ElasticSearch Query | Dexie Translation |
|---|---|
term |
where(field).equals(value) |
terms |
where(field).anyOf(values) |
match |
Text matching with tokenization |
match_phrase |
Phrase matching |
bool.must |
Logical AND |
bool.must_not |
Logical NOT (exclusion) |
bool.should |
Logical OR |
bool.filter |
Same as must (no scoring) |
exists |
Field existence check |
wildcard |
Wildcard pattern matching |
range |
Range queries (gt, gte, lt, lte) |
geo_shape |
Geohash-based spatial query |
geo_bounding_box |
Bounding box spatial query |
Query Flow
ElasticSearch Query → Addon Parser → Indexed Fields Check
↓
Table.where() ← Uses highest priority index (fast)
↓
Collection.filter() ← Applies remaining terms
Query execution process:
- The system identifies the highest priority indexed term based on
indexedDBIndexesPriority(if set), otherwise uses the order ofindexedDBIndexes - Uses
Table.where()on the highest priority available index - Uses
Collection.filter()on the result for all remaining query terms - If no indexes match any query terms, uses
Collection.filter()on all terms (full table scan)
Geospatial Queries
Geohashing
Geospatial queries use geohashing for indexed lookups:
// Libraries used
import geohash from 'ngeohash';
import geohashPoly from 'geohash-poly';
How it works:
1. GeoJSON coordinates are converted to geohashes during caching
2. Bounding box queries generate array of geohashes
3. Table.where().inAnyRange() efficiently queries the index
4. Neighbors are included to handle edge cases
Geohash Index Configuration
{
"indexedDBIndexes": [
"*geohash",
"*locationGeo.features____geohash"
]
}
Geohash Precision
Geohash precision determines the size of each cell:
| Precision | Cell Size |
|---|---|
| 1 | ~5,000 km |
| 4 | ~39 km |
| 6 | ~610 m |
| 8 | ~19 m |
Configure precision based on your map zoom requirements.
Flattened Values for Array Indexing
IndexedDB cannot index object properties within arrays. Formbird uses flattened values:
Problem
{
"parentsRel": [
{ "documentId": "abc-123", "name": "Parent 1" },
{ "documentId": "def-456", "name": "Parent 2" }
]
}
Cannot create index on parentsRel.documentId directly.
Solution
The client preprocessor creates flattened values:
{
"parentsRel": [...],
"parentsRel____documentId": ["abc-123", "def-456"]
}
Now *parentsRel____documentId can be indexed.
Enabling Flattening
For sc-related-document components:
{
"componentName": "sc-related-document",
"name": "parentsRel",
"saveFlattenedValues": true
}
The core automatically flattens values if the field has an entry in indexedDBIndexes.
Query Execution
Table vs Collection Queries
// Table query (uses index - fast)
await db.documents
.where("systemHeader.templateId")
.equals("abc-123")
.toArray();
// Collection query (no index - slower but more flexible)
await db.documents
.filter(doc => doc.status === "Active")
.toArray();
Query Priority
The addon prioritizes indexed fields in Table.where():
- Check if queried field has an index
- If indexed: use
Table.where() - If not indexed: use
Collection.filter() - Log warning if performance could be improved with index
Query Timeout
Large queries can block the UI. Configure timeout:
// Queries have configurable timeout to prevent blocking
const QUERY_TIMEOUT_MS = 30000;
Workers
Dexie Shared Worker
The dexieSharedWorker handles queries across tabs:
- Single worker shared between all tabs
- Prevents concurrent access conflicts
- Falls back to Web Worker on unsupported browsers
Query Queueing
Large queries are queued to prevent blocking:
// High priority queries (user interactions) run first
// Background queries (prefetching) run when idle
Performance Considerations
Best Practices
- Index high-value fields that return small result sets
- Prioritize fields that return the smallest subset of documents
documentIdshould be high priority (returns single document)-
*appTagsandsystemHeader.templateIdshould be low priority (many documents share these values) -
Use compound indexes for multi-field queries
-
Combine fields that are always queried together
-
Limit result sets
- Use pagination in queries
-
Avoid
toArray()on large collections -
Batch inserts
- Use bulk operations during caching
- Avoid inserting documents one-by-one
Troubleshooting
Common Issues
| Issue | Cause | Solution |
|---|---|---|
| Slow queries | Missing index | Add field to indexedDBIndexes |
| Slow queries | High priority index returns large subsets | Reorder indexedDBIndexesPriority to prioritize smaller result sets |
| Schema upgrade failed | Primary key change | Clear cache, re-sync |
| "Dexie search not specified" | Missing addon | Check dexieAddons config |
| Firefox slow caching | Browser limitation | See Performance Optimization doc |
Troubleshooting Slow Queries
- Check if the field is indexed - Missing indexes cause full table scans
- Review index priority - A high priority index returning large subsets (e.g., querying on
systemHeader.templateIdfirst) will return many results before theCollection.filter()step can narrow them down - Add new indexes - Consider adding indexes for frequently filtered fields that return small result sets
- Check for redundant query terms - Unnecessary terms add overhead
Debug Logging
Enable verbose logging:
// In browser console
Dexie.debug = true;
Related Documentation
- 101-Offline-Architecture.md - System overview
- 109-Configuration-Reference.md - Configuration options
- 112-Performance-Optimization.md - Performance tuning