Skip to main content
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' }
})
expiration
number
Unix timestamp (seconds) when the key should expire.
metadata
object
Arbitrary JSON metadata attached to the key.

Get with Metadata

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)
}

Update 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 })
}
prefix
string
Filter keys by prefix (e.g., 'user:').
limit
number
Max keys per page (default 100, max 1000).
cursor
string
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

Value size
1 MB
Maximum size per value.
Keys per list
1000
Maximum keys returned per list() call.
There is no hard limit on total storage per guild, but consider performance for large datasets.

Best Practices

1

Use prefixes for organization

Prefix keys by type: user:123, channel:456, config:timezone.
2

Store JSON for complex data

Serialize objects with JSON.stringify() and parse with JSON.parse().
3

Set expiration for temporary data

Use the expiration option to auto-clean cache and sessions.
4

Handle missing keys

Always check for null when getting values.

Build docs developers (and LLMs) love