IndexedDB

Query notes through an index

Add a title index in a versioned upgrade and query a bounded subset instead of loading the entire store.

The primary key answers one question: give me the note with this ID. The moment you want “all notes titled Trail conditions”, the key is useless. An index lets IndexedDB do that lookup instead of you loading every record and filtering in JavaScript.

Add the index in version 2

Indexes are schema, so they’re created inside an upgrade. Bump the version to 2 and add an index on a titleKey property:

export const db = await openDB('field-notes', 2, {
  upgrade(db, oldVersion, newVersion, tx) {
    if (oldVersion < 1) {
      db.createObjectStore('notes', { keyPath: 'id' })
    }
    if (oldVersion < 2) {
      tx.objectStore('notes').createIndex('by-title', 'titleKey')
    }
  },
})

const matches = await db.getAllFromIndex('notes', 'by-title', 'trail', 20)

The index is called by-title and reads the titleKey property from each record. getAllFromIndex() returns every note whose titleKey is exactly 'trail'.

Why titleKey and not title? Index lookups are exact and case-sensitive. 'Trail conditions' and 'trail conditions' are different keys. So we store a lowercased, trimmed copy and index that. title keeps the original for display.

The oldVersion checks

Look at the two if blocks. They’re not decoration.

A brand new user has oldVersion 0. Both blocks run. A user coming from version 1 has oldVersion 1. Only the second block runs. If you called db.createObjectStore('notes') unconditionally, that user would hit a ConstraintError because the store already exists, and the upgrade would abort.

Never assume the user is coming from the version right before this one. Someone opened your app in January, went hiking for six months, and comes back three versions later. The chain of if blocks carries them from any old version to the current one.

Existing records need the property

An index can’t invent values. Notes saved under version 1 have no titleKey, so the index skips them. Searching for 'trail' won’t find last week’s note.

Fix it during the upgrade with a small data migration. Open a cursor on the store inside the same transaction, set titleKey from title on each note, and put() it back. Migrations belong in the upgrade so they run exactly once.

Indexes cost writes

Each index is one more structure the browser updates on every put(). Cheap for one or two. Twenty indexes on a store you write on every keystroke will hurt.

My rule is to add an index only when the interface makes that query, not because a property looks useful someday.

Bound your reads

The fourth argument, 20, caps the result at twenty records. That’s a start. Real search wants more: prefix matching with an IDBKeyRange, pagination, and cursors that stop when the screen is full. Design that before the list gets long.

Upgrade to version 2 with the migration in place and query the index. Test it twice: on a fresh profile, and on a profile that already holds version 1 data. Both must end at version 2 with every note searchable, and DevTools should show by-title under the notes store.

Lesson completed