Flora provides a per-guild key-value storage API powered by Sled. Each guild gets isolated storage namespaced into named stores.
Basic Usage
const store = kv.store('settings')
await store.set('prefix', '!')
const prefix = await store.get('prefix') // '!'
Creating a Store
Stores are namespaced by name within each guild:
const settings = kv.store('settings')
const counters = kv.store('counters')
const cache = kv.store('cache')
Store names are arbitrary strings. Use descriptive names to organize your data.
Operations
Get
Retrieve a value by key:
const value = await store.get('key')
if (value === null) {
console.log('Key not found')
} else {
console.log('Value:', value)
}
Set
Store a value:
await store.set('key', 'value')
Values are stored as strings. Maximum size: 1 MB per value.
Set with Options
Add expiration and metadata:
const expiresAt = Math.floor(Date.now() / 1000) + 3600 // 1 hour from now
await store.set('session', JSON.stringify({ userId: '123' }), {
expiration: expiresAt,
metadata: { source: 'login', ip: '192.168.1.1' }
})
Unix timestamp (seconds) when the key should expire.
Arbitrary JSON metadata attached to the key.
Retrieve both value and metadata:
const result = await store.getWithMetadata('session')
if (result.value === null) {
console.log('Not found')
} else {
console.log('Value:', result.value)
console.log('Metadata:', result.metadata)
}
Change metadata without touching the value:
await store.updateMetadata('session', {
lastAccessed: new Date().toISOString()
})
Delete
Remove a key:
await store.delete('key')
List Keys
Paginate through all keys:
const page = await store.list({ limit: 100 })
console.log('Keys:', page.keys)
console.log('Complete?', page.list_complete)
if (!page.list_complete) {
const nextPage = await store.list({ cursor: page.cursor })
}
Filter keys by prefix (e.g., 'user:').
Max keys per page (default 100, max 1000).
Cursor from previous page for pagination.
Patterns
JSON Storage
const store = kv.store('data')
// Store object
const user = { id: '123', name: 'Alice', level: 5 }
await store.set('user:123', JSON.stringify(user))
// Retrieve object
const raw = await store.get('user:123')
if (raw) {
const user = JSON.parse(raw)
console.log(user.name) // 'Alice'
}
Counter
const store = kv.store('counters')
async function increment(key: string): Promise<number> {
const current = await store.get(key)
const count = current ? parseInt(current, 10) : 0
const newCount = count + 1
await store.set(key, String(newCount))
return newCount
}
const count = await increment('messages')
await ctx.reply(`Message count: ${count}`)
Temporary Cache
const cache = kv.store('cache')
async function cachedFetch(url: string): Promise<string> {
// Check cache first
const cached = await cache.get(url)
if (cached) return cached
// Fetch and cache for 5 minutes
const response = await fetch(url)
const data = await response.text()
const expiresAt = Math.floor(Date.now() / 1000) + 300
await cache.set(url, data, { expiration: expiresAt })
return data
}
User Preferences
const prefs = kv.store('preferences')
async function setPreference(userId: string, key: string, value: string) {
const prefKey = `${userId}:${key}`
await prefs.set(prefKey, value)
}
async function getPreference(userId: string, key: string): Promise<string | null> {
const prefKey = `${userId}:${key}`
return await prefs.get(prefKey)
}
await setPreference('123', 'timezone', 'America/New_York')
const tz = await getPreference('123', 'timezone')
List by Prefix
const store = kv.store('users')
// Store multiple keys with prefix
await store.set('user:123', JSON.stringify({ name: 'Alice' }))
await store.set('user:456', JSON.stringify({ name: 'Bob' }))
await store.set('admin:789', JSON.stringify({ name: 'Eve' }))
// List only user keys
const result = await store.list({ prefix: 'user:' })
console.log(result.keys) // ['user:123', 'user:456']
Complete Example
const counter = slash({
name: 'counter',
description: 'A simple counter using KV storage',
subcommands: [
{
name: 'get',
description: 'Get current count',
run: async (ctx) => {
const store = kv.store('counters')
const count = await store.get('main')
await ctx.reply(`Current count: ${count || 0}`)
}
},
{
name: 'increment',
description: 'Increment the counter',
run: async (ctx) => {
const store = kv.store('counters')
const current = parseInt(await store.get('main') || '0', 10)
const newCount = current + 1
await store.set('main', String(newCount))
await ctx.reply(`Count is now: ${newCount}`)
}
},
{
name: 'reset',
description: 'Reset the counter',
run: async (ctx) => {
const store = kv.store('counters')
await store.set('main', '0')
await ctx.reply('Counter reset to 0')
}
}
]
})
createBot({ slashCommands: [counter] })
Type Definitions
export class KvStore {
async get(key: string): Promise<string | null>
async getWithMetadata(key: string): Promise<{
value: string | null
metadata?: Record<string, unknown>
}>
async set(
key: string,
value: string,
options?: {
expiration?: number // Unix timestamp
metadata?: JsonValue // Arbitrary JSON
}
): Promise<void>
async updateMetadata(
key: string,
metadata: JsonValue | undefined
): Promise<void>
async delete(key: string): Promise<void>
async list(options?: {
prefix?: string
limit?: number // Default 100, max 1000
cursor?: string
}): Promise<{
keys: RawKvKeyInfo[]
list_complete: boolean
cursor?: string
}>
}
export function store(name: string): KvStore
export const kv = { store }
Limits
Maximum keys returned per list() call.
There is no hard limit on total storage per guild, but consider performance for large datasets.
Best Practices
Use prefixes for organization
Prefix keys by type: user:123, channel:456, config:timezone.
Store JSON for complex data
Serialize objects with JSON.stringify() and parse with JSON.parse().
Set expiration for temporary data
Use the expiration option to auto-clean cache and sessions.
Handle missing keys
Always check for null when getting values.