Persistent storage

Use KV for data your bot learns or changes while it runs: server settings, counters, warnings, user preferences, and other small JSON records.

KV data belongs to one deployment. It survives publishes, rollbacks, runtime restarts, and isolate recycling. Another deployment cannot read it.

Use project files for configuration shipped with a revision, and module variables for temporary caches that are safe to lose. KV is not a file store, event archive, or place for credentials.

Namespaces, keys, and values

A namespace groups one kind of data. A key identifies one record inside it:

guild-settings
├── 123456789012345678 → { "welcomeChannelId": "234567890123456789" }
└── 345678901234567890 → { "welcomeChannelId": "456789012345678901" }

Both records are in the guild-settings namespace, and each Discord server ID is its key.

Values can be strings, numbers, booleans, null, arrays, or plain objects containing those types. get() returns undefined when the key does not exist.

Store settings for each server

Create the namespace once at the top level of your script:

type GuildSettings = {
  welcomeChannelId: string;
};

const guildSettings = new weeble.KVNamespace("guild-settings");

Write a server’s settings after validating them in a command:

async function setWelcomeChannel(guildId: string, channelId: string) {
  await guildSettings.put(guildId, { welcomeChannelId: channelId });
}

Read the same record when an event arrives:

discord.on(discord.events.GUILD_MEMBER_ADD, async (member) => {
  const settings = await guildSettings.get<GuildSettings>(member.guildId);
  if (!settings) return;

  const channel = await discord.fetchGuildTextChannel(
    settings.welcomeChannelId,
  );

  if (channel) {
    await channel.send(`Welcome ${member.toMention()}!`);
  }
});

Publishing new code does not reset these settings. They remain until the key is deleted, its TTL expires, the namespace is cleared, or the deployment is deleted.

Update shared values safely

Discord events can run concurrently. A separate read followed by a write can lose an update when two handlers read the same value:

// Do not use this for a shared counter.
const commandUses = new weeble.KVNamespace("command-uses");

async function recordCommandUse(guildId: string) {
  const current = (await commandUses.get<number>(guildId)) ?? 0;
  await commandUses.put(guildId, current + 1);
}

Use the atomic counter operation instead:

const commandUses = new weeble.KVNamespace("command-uses");

async function recordCommandUse(guildId: string) {
  return commandUses.increment(guildId);
}

For an object, use a transaction:

type GuildStats = {
  commands: number;
  lastUsedAt: number;
};

const guildStats = new weeble.KVNamespace("guild-stats");

async function recordCommandUse(guildId: string) {
  return guildStats.transact<GuildStats>(guildId, (current) => ({
    commands: (current?.commands ?? 0) + 1,
    lastUsedAt: Date.now(),
  }));
}

The transaction callback may run again if another execution changes the key first. Keep it free of messages, network requests, logging, and other side effects.

For a lock or another create-only record, check the result of a conditional write:

const locks = new weeble.KVNamespace("locks");
const acquired = await locks.put("daily-report", Date.now(), {
  ifNotExists: true,
  ttl: 60_000,
});

if (!acquired) return;

put() returns false only when ifNotExists blocks the write. delete() also returns a boolean, including when you pass prevValue to delete only a matching value.

Conditional value checks compare the serialized JSON. For objects, property order must match.

Expire temporary data

Set a TTL for values that should remove themselves. TTL is measured in milliseconds:

const cooldowns = new weeble.KVNamespace("cooldowns");

async function startCooldown(userId: string) {
  await cooldowns.put(
    `user:${userId}`,
    true,
    { ttl: 10 * 60 * 1000 },
  );
}

After ten minutes, cooldowns.get() returns undefined for that key.

Writing without ttl removes any previous expiry. Passing a TTL to increment() resets the counter’s expiry on every increment; omitting it preserves an existing expiry.

Inspect data in VS Code

Expand KV in the Weeble sidebar, then expand a namespace. Select a key to open its JSON value and optional TTL in an editor.

Saving the document writes the new value. Invalid JSON or metadata is rejected with an editor diagnostic. The context menu can create or delete keys and clear an entire namespace.

A guild-settings namespace in the Weeble sidebar and one value open in the KV editor.

KV values can be inspected and edited without changing the deployed source.

Important

KV values are application data, not secret storage. Do not put bot tokens, API keys, webhook URLs, or other credentials in KV.

Limits and larger reads

One event can make up to 100 KV operations. getMany() accepts up to 100 keys, and batch() accepts up to 100 writes or deletes in one atomic operation.

Listings return 100 records by default. For a larger namespace, use listPage() or itemsPage() and pass the returned cursor into the next call. A page can contain at most 1,000 records. Keys are returned in lexicographic order, and cursors should be treated as opaque.

See Limits for the other execution budgets and KVNamespace for the complete API.