useAppsDbStore
import { useAppsDb } from '@bbc/front-end-kit/vue/showpad'
A Pinia-based composable factory for creating reactive AppsDB store instances. It provides a clean, Vue-native interface to Showpad's AppsDB for persistent key-value storage.
Overview
AppsDB allows for rich offline storage and secure data protection. This composable creates singleton store instances per unique storeId and storeType combination, ensuring shared reactive state across components.
Store Scopes
- Local (User-scoped): Entries are tied to individual users. Accessible offline.
- Global: Entries are accessible to all users. Requires specific OAuth2 permissions. Read-only while offline.
More information about AppsDB on their website.
Technical explanation about AppsDb.
Note
This store uses Showpad's SDK rather than the Showpad API directly, for two key reasons:
- Entry size limit: The SDK supports entries up to 10 MB, compared to the API's 250 KB cap.
- No authentication required: SDK calls do not require OAuth2 read/write operations. Authentication is only needed when performing write operations on global-scoped stores.
Getting started
import { useAppsDb } from '@bbc/front-end-kit/vue/showpad'
// Direct usage — storeType defaults to 'local' if omitted
const settingsStore = useAppsDb('app-settings', 'local')
// It is recommended to wrap usage in domain-specific composables
export function useNotes() {
return useAppsDb('user-notes', 'local', {
autoInit: true,
autoFetch: true
})
}
Transition from AppsDb.js
useAppsDbStore is the official Pinia-based replacement for the legacy AppsDb.js class. While the original class focused on procedural interaction with the Showpad SDK, this store is designed specifically for modern Vue applications.
Warning
This store should be used for all new Vue applications going into development. The legacy AppsDb.js class should be considered deprecated for new projects.
Shared Reactivity
Unlike the original class which required manual data fetching and local state management in every component, useAppsDbStore is reactive. If one component saves an entry, all other components using the same store ID see the update instantly.
Legacy AppsDb.js:
// Component A
const myAppDb = new AppsDb({ id: 'settings', type: 'local' });
await myAppDb.init();
await myAppDb.setEntry({ id: 'theme', value: 'dark' });
// Component B won't know about this update automatically
useAppsDbStore:
// Component A
const settingsStore = useAppsDb('settings');
settingsStore.save({ id: 'theme', value: 'dark' });
// Component B automatically reacts to the change
const settingsStore = useAppsDb('settings');
watchEffect(async () => {
const allEntries = await settingsStore.byId();
theme.value = allEntries.theme;
});
Singleton Factory
The composable ensures that calling useAppsDb multiple times for the same store ID returns the same reactive instance. You no longer need to pass an AppsDb instance down through props or use Vue's provide/inject.
Automatic Action Queuing
The store internally queues saves and deletes per entry ID. You can fire multiple updates without worrying about race conditions or out-of-order execution in the underlying Showpad SDK.
Integrated Initialization
With autoInit and autoFetch options, you can reduce boilerplate code in your component lifecycle hooks and let the store handle its own readiness.
API
Configuration
useAppsDb (storeId, storeType, options)
The factory function used to retrieve or create a singleton store instance.
Parameters:
storeId(String): Unique identifier for the store.storeType('local'|'global'): Scope of the store. Default:'local'.options.accessToken(String): Access token for global stores (and optionally local stores).options.autoInit(Boolean): Automatically initialize on creation. Default:true.options.autoFetch(Boolean): Automatically fetch all entries after init. Default:false.options.maxAge(Number): Time in ms after which cached entries are considered stale. Default:10000.options.pollingInterval(Number): Interval in ms for background re-fetches when polling is enabled. Default:10000.
State & Properties
entries
Reactive array of all loaded entries.
Values: Ref<Array>
isLoading
Aggregate boolean — true if any internal async operation is currently active (init, fetch, save, remove, or clear).
Values: ComputedRef<Boolean>
isInitialized
Boolean indicating the store has successfully connected to Showpad AppsDB.
Values: Ref<Boolean>
error
Contains the last error object encountered during an operation.
Values: Ref<any | null>
polling
Reactive boolean flag. Set to true to start a background polling interval that re-fetches entries every pollingInterval ms. Set to false to stop it.
Values: Ref<Boolean>
lastFetchedAt
Timestamp (ms) of the last successful fetch. Used internally to determine staleness.
Values: Ref<Number | null>
Granular loading flags
Fine-grained loading states for specific operations:
Values: Ref<Boolean>
isInitializing— store is being created/connected.isFetching— a fetch is in progress.isSaving— a save operation is in progress.isRemoving— a remove operation is in progress.isClearing— a clear operation is in progress.
Per-entry state maps
Reactive objects keyed by entry ID, useful for driving per-item loading UI:
Values: Ref<Record<string, Boolean>>
queuedById— entry has pending queued work.savingById— entry is currently being saved.removingById— entry is currently being removed.
count
Computed number of entries currently loaded in the cache.
Values: ComputedRef<Number>
isEmpty
Computed boolean that is true when no entries are loaded.
Values: ComputedRef<Boolean>
storeId / storeType
Read-only meta properties exposing the store's own identifier and scope type.
Values: String
Methods
init ()
Connects to Showpad AppsDB and ensures the store exists. Typically called automatically via autoInit. Concurrent calls share the same in-flight promise.
Returns: Promise<void>
fetch ()
Retrieves all entries from the SDK, updates the reactive entries state, and records lastFetchedAt.
Returns: Promise<void>
get (id)
Retrieves a specific entry directly from the SDK, bypassing the local cache. Useful for reading the latest value of a critical entry.
Returns: Promise<any>
save (data)
Saves an entry. Generates a UUID if id is missing. Queues the operation per entry ID to prevent race conditions. Sanitizes data before saving.
Returns: Promise<{ id: string, value: any }> — the saved entry as stored in AppsDB.
remove (id)
Deletes the entry with the specified ID. Queued per entry ID.
Returns: Promise<void>
clear ()
Removes all entries from the store by removing each one individually.
Returns: Promise<void>
duplicateEntry (itemOrId)
Creates a deep copy of an entry, stripping its original id, created_at, and updated_at, then calls save() without an id so the store generates a fresh UUID internally.
Source note
duplicateEntry internally calls save(newId, cleaned) with two arguments, but save() only accepts one. The first argument (newId) is silently ignored — the UUID is generated inside save() because id was deleted from the payload. The net result is correct (a new entry with a fresh UUID), but the newId computed in duplicateEntry is unused.
Returns: Promise<{ id: string, value: any }>
invalidate ()
Marks the current cache as stale. The next call to any query helper (byId, find, filter, findWhere, map) will trigger a re-fetch before returning data.
Query Helpers
All query helpers are async and automatically re-fetch from the SDK if the cache is stale before returning results.
byId ()
Returns a { [id]: value } dictionary of all entries. Re-fetches if the cache is older than maxAge.
Returns: Promise<Record<string, any>>
find (predicate)
Returns the value of the first entry where predicate(value, id) returns true.
Returns: Promise<any | undefined>
filter (predicate)
Returns an array of values for all entries where predicate(value, id) returns true.
Returns: Promise<Array>
findWhere (attributes)
Shortcut to find the first entry where all properties match the provided attributes object.
Returns: Promise<any | undefined>
map (callback)
Returns an array of callback(value, id) results for every entry.
Returns: Promise<Array>
Per-entry Helper
isEntryBusy (id)
Returns true if the given entry ID has any queued, saving, or removing activity in progress. Useful for disabling UI controls while an entry is being written.
Returns: Boolean
Utility Exports
These are module-level utility functions exported alongside useAppsDb.
clearAppsDbRegistry ()
Clears the internal singleton registry. Primarily useful for testing or hot-reload scenarios where a fresh store instance is required.
getRegisteredStores ()
Returns an array of all currently registered store keys (in the format storeId-storeType). Useful for debugging.
Best Practices
1. Domain Composables
Always wrap useAppsDb in a domain-specific composable to centralise the store ID, scope, and options in one place. This prevents magic strings from spreading through the codebase and makes it trivial to reconfigure later.
// composables/useUserNotes.js
import { useAppsDb } from '@bbc/front-end-kit/vue/showpad'
export function useUserNotes() {
return useAppsDb('user-notes', 'local', {
autoInit: true,
autoFetch: true,
maxAge: 30000
})
}
<!-- In any component -->
<script setup>
import { useUserNotes } from '@/composables/useUserNotes'
const notesStore = useUserNotes()
</script>
2. Queued Saves
The store serialises concurrent writes to the same entry ID. You can call save multiple times in quick succession without wrapping them in sequential awaits — they will execute in order.
const store = useAppsDb('tasks', 'local')
// These are safe to fire simultaneously — internally queued per ID
store.save({ id: 'task-1', title: 'Buy groceries', done: false })
store.save({ id: 'task-2', title: 'Walk the dog', done: false })
store.save({ id: 'task-1', title: 'Buy groceries', done: true }) // ← will run after the first task-1 save
3. Staleness Awareness
The query helpers (byId, find, filter, findWhere, map) are all async. They will automatically re-fetch from the SDK if the local cache is older than maxAge. Always await them, and use ref or reactive to store the result for template binding.
<script setup>
import { ref, onMounted } from 'vue'
import { useAppsDb } from '@bbc/front-end-kit/vue/showpad'
const store = useAppsDb('settings', 'local', { autoInit: true, autoFetch: true })
const entriesById = ref({})
onMounted(async () => {
// byId() re-fetches from SDK if cache is stale
entriesById.value = await store.byId()
})
</script>
<template>
<p>Theme: {{ entriesById.theme }}</p>
</template>
4. Polling for Live Data
For stores shared across multiple users or sessions (particularly global stores), enable polling so the local cache stays fresh automatically.
const leaderboardStore = useAppsDb('leaderboard', 'global', {
autoInit: true,
autoFetch: true,
pollingInterval: 30000 // re-fetch every 30 seconds
})
// Start polling
leaderboardStore.polling = true
// Stop polling when the component is unmounted
onUnmounted(() => {
leaderboardStore.polling = false
})
5. Per-entry UI Feedback
Use isEntryBusy(id) to disable buttons or show spinners while a specific entry is being written or deleted, rather than disabling the entire UI via isLoading.
<script setup>
import { useAppsDb } from '@bbc/front-end-kit/vue/showpad'
const store = useAppsDb('tasks', 'local')
async function completeTask(task) {
await store.save({ ...task, done: true })
}
</script>
<template>
<ul>
<li v-for="task in store.entries" :key="task.id">
{{ task.title }}
<button
:disabled="store.isEntryBusy(task.id)"
@click="completeTask(task)"
>
{{ store.isEntryBusy(task.id) ? 'Saving…' : 'Mark done' }}
</button>
</li>
</ul>
</template>
Tips & Tricks
Global stores need accessToken for writes
Reads from a global store work without any additional configuration. However, write operations (save, remove, clear) on a global store require an OAuth2 access token.
If accessToken is omitted from the options, reads will work normally but any writes will silently fail with an authorization error.
Always centralise this in your domain composable so the token is never accidentally omitted:
// composables/useSharedLeaderboard.js
export function useSharedLeaderboard(accessToken) {
return useAppsDb('leaderboard', 'global', {
accessToken, // required for write access
autoInit: true,
autoFetch: true
})
}
store.entries is a cache — do not use it for critical reads
Warning
Accessing store.entries directly reads from the local in-memory cache. It does not check whether that cache is still fresh. If entries were updated by another user or session since the last fetch, your component will display stale data without any indication.
Use the async query helpers (byId(), find(), filter(), etc.) or call fetch() explicitly whenever freshness matters.
These automatically re-fetch from the SDK if the cache has exceeded maxAge.
// ❌ Stale — reads whatever is cached, regardless of age
const entry = store.entries.find(e => e.id === 'my-id')
// ✅ Fresh — re-fetches from SDK if cache is stale
const entry = await store.find((value, id) => id === 'my-id')
save() with no id always creates a new entry
Calling save() without an id property in the payload instructs the store to generate a new UUID and always insert a new entry, never update an existing one.
This makes it the right pattern for append-only data like activity logs, audit trails, or event queues.
const activityStore = useAppsDb('activity-log', 'local')
// Each call creates a brand new entry — no `id` means always new
activityStore.save({ action: 'page_viewed', page: 'home', timestamp: Date.now() })
activityStore.save({ action: 'button_clicked', label: 'Get Started', timestamp: Date.now() })
If instead you want to update an existing entry, always pass its exact id:
// ✅ Updates the existing entry with this ID
activityStore.save({ id: 'existing-uuid', action: 'page_viewed', page: 'updated' })
duplicateEntry strips identity metadata automatically
When you call duplicateEntry(itemOrId), it deep-clones the entry's value and automatically removes the id, created_at, and updated_at fields before saving the copy.
The result is a clean, independent entry with a fresh UUID.
This is useful for cloning templates, preset configurations, or cards without having to manually strip those fields yourself.
const templateStore = useAppsDb('slide-templates', 'local')
// Duplicate an existing template — the copy gets a new ID automatically
const original = await templateStore.get('my-template-id')
const copy = await templateStore.duplicateEntry('my-template-id')
// copy.id !== original.id ✓
// copy.created_at and copy.updated_at are stripped from the clone ✓
Always stop polling in onUnmounted
polling = true starts a setInterval inside the store. If your component is destroyed without setting polling = false, the interval continues running in the background, causing unnecessary SDK calls and potential memory leaks.
Always pair the two together:
import { onMounted, onUnmounted } from 'vue'
const liveStore = useAppsDb('live-data', 'global', {
autoInit: true,
autoFetch: true,
pollingInterval: 15000
})
onMounted(() => { liveStore.polling = true })
onUnmounted(() => { liveStore.polling = false })
findWhere for simple attribute lookups
When you need to find an entry by one or more known properties, prefer findWhere over a manual find predicate.
It is more readable, communicates intent clearly, and requires less boilerplate.
const itemsStore = useAppsDb('items', 'local')
// ❌ Manual predicate — works, but noisy
const active = await itemsStore.find(value => value.status === 'active' && value.category === 'featured')
// ✅ findWhere — reads like plain English
const active = await itemsStore.findWhere({ status: 'active', category: 'featured' })
Reserve find for complex conditions that findWhere's strict equality matching can't express, such as range checks or pattern matching.
Use storeId and storeType for debugging
Every store instance exposes its own storeId and storeType as readable properties.
These are handy when logging, building devtools overlays, or distinguishing between multiple store instances on the same page.
const notesStore = useAppsDb('user-notes', 'local')
const sharedStore = useAppsDb('shared-notes', 'global')
console.log(notesStore.storeId, notesStore.storeType) // "user-notes", "local"
console.log(sharedStore.storeId, sharedStore.storeType) // "shared-notes", "global"
You can also use getRegisteredStores() to see all active store instances at once — useful for tracking down unexpected singletons:
import { getRegisteredStores } from '@bbc/front-end-kit/vue/showpad'
console.log(getRegisteredStores())
// ["user-notes-local", "shared-notes-global"]