Skip to main content
flora is intentionally designed for guild contexts only. All Discord events without a guild_id are dropped, and direct messages (DMs) are not supported.

Design Philosophy

This limitation is by design, not a bug:
  • Simplified mental model: Developers only need to think about one context (guilds)
  • Natural isolation: Guild ID provides a natural boundary for isolates and storage
  • Security: Guild-scoped permissions align with Discord’s permission model
  • Resource management: Guild ID is used for routing, caching, and rate limiting
The guild-only scope is documented in AGENTS.md:8 and enforced throughout the runtime.

Event Filtering

Events without a guild_id are dropped at the Discord handler level (apps/runtime/src/discord_handler.rs):
FullEvent::Message { new_message: msg, .. } => {
    let payload = EventMessage::from(msg);
    let guild_id = msg.guild_id.map(|guild| guild.get().to_string());
    if guild_id.is_none() {
        return; // Drop event
    }
    // ...
}
This pattern is repeated for all event types:
  • MessageCreate (apps/runtime/src/discord_handler.rs:74-77)
  • MessageUpdate (apps/runtime/src/discord_handler.rs:97-99)
  • MessageDelete (apps/runtime/src/discord_handler.rs:128-130)
  • InteractionCreate (apps/runtime/src/discord_handler.rs:195-197)
  • ReactionAdd (apps/runtime/src/discord_handler.rs:264-266)
  • ReactionRemove (apps/runtime/src/discord_handler.rs:288-290)

What This Means

No Direct Messages

DMs to the bot are silently ignored:
// This will NEVER fire for DMs
on('messageCreate', async (msg) => {
  await msg.reply('Hello!') // Only works in guild channels
})
If you need DM support, flora is not the right framework for your use case.

No Global Commands

Slash commands must be registered per-guild:
slash({
  name: 'ping',
  description: 'Pong!',
  handler: async (interaction) => {
    // Only available in the guild where this script is deployed
    await interaction.reply({ content: 'Pong!' })
  }
})
Global slash commands (available in all guilds and DMs) are not supported.

No User/Bot-Scoped Events

Events that don’t have a guild context are dropped:
  • User updates (username changes, avatar changes)
  • Presence updates outside guilds
  • Voice state updates in DM calls
  • Group DM events

Benefits of Guild-Only Scope

1. Natural Isolation

Guild ID provides a natural isolation boundary:
// apps/runtime/src/runtime/mod.rs:90-102
pub async fn deploy_guild_script(&self, deployment: Deployment) -> Result<(), AnyError> {
    let worker_idx = {
        let mut routes = self.guild_routes.lock();
        match routes.get(&deployment.guild_id).copied() {
            Some(worker_idx) => worker_idx,
            None => {
                let worker_idx = self.default_worker_for_guild(&deployment.guild_id);
                routes.insert(deployment.guild_id.clone(), worker_idx);
                worker_idx
            }
        }
    };
    // ...
}
Every deployment, event, and storage operation is keyed by guild ID.

2. Simplified Storage

KV storage is naturally scoped per guild:
// Keys are automatically scoped to the guild
await kv.set('config', { prefix: '!' })

// Different guilds have independent key spaces
// Guild A's 'config' key is separate from Guild B's 'config' key
No need for manual namespacing or cross-guild data isolation logic.

3. Clear Permission Model

Guild-scoped commands align with Discord’s permission system:
slash({
  name: 'ban',
  description: 'Ban a user',
  handler: async (interaction) => {
    // Discord's permission checks work naturally
    // because the command is guild-scoped
  }
})

4. Easier Debugging

Logs, metrics, and errors are naturally grouped by guild:
# View logs for a specific guild
flora logs --guild 123456789

# All events, ops, and errors include guild context

Workarounds for DM Use Cases

If you absolutely need to handle DMs, consider these alternatives:

1. Redirect to Guild Channel

Use a separate bot (outside flora) to listen for DMs and forward them to a guild channel:
// Not in flora - separate bot
client.on('messageCreate', async (msg) => {
  if (msg.channel.type === 'DM') {
    // Forward to modmail channel in a guild
    const channel = await client.channels.fetch('CHANNEL_ID')
    await channel.send(`DM from ${msg.author.tag}: ${msg.content}`)
  }
})

2. Use Interactions in Guilds

Instead of DMs, use ephemeral interactions:
slash({
  name: 'private',
  description: 'Private response',
  handler: async (interaction) => {
    // Only the user who triggered the interaction can see this
    await interaction.reply({
      content: 'This is private!',
      flags: MessageFlags.Ephemeral
    })
  }
})

3. Use a Different Framework

If DMs are a core requirement, consider: flora is optimized for guild-centric bots, not general-purpose Discord bots.

Impact on API Design

The guild-only scope influences every part of the flora API:

Event Payloads

All event payloads include guild_id (apps/runtime/src/discord_handler.rs:426-440):
#[expose_payload]
pub struct EventMessage {
    id: String,
    channel_id: String,
    guild_id: Option<String>, // Always Some() for flora events
    content: String,
    author: EventUser,
    member: Option<EventMember>,
}
The Option<String> is for Discord API compatibility, but flora guarantees it’s always Some().

KV Ops

KV ops extract guild ID from OpState (apps/runtime/src/ops/kv.rs):
#[op2(async)]
pub async fn op_kv_get(
    state: Rc<RefCell<OpState>>,
    #[string] key: String,
) -> Result<Option<serde_json::Value>, JsErrorBox> {
    let guild_id = {
        let state = state.borrow();
        state.borrow::<String>().clone() // Guild ID
    };
    // KV operations are scoped to this guild
}

Secrets Scope

Secrets can be runtime-scoped or guild-scoped (apps/runtime/src/runtime/secrets.rs):
pub enum SecretScope {
    Runtime,        // Shared across all guilds
    Guild(String),  // Scoped to a specific guild
}
Guild-scoped secrets are the default for security.

Technical Enforcement

The guild-only scope is enforced at multiple levels:
  1. Event handler - Non-guild events are dropped (apps/runtime/src/discord_handler.rs)
  2. Runtime dispatch - guild_id is required for event dispatch (apps/runtime/src/runtime/mod.rs:179-207)
  3. Deployment service - Deployments are keyed by guild ID (apps/runtime/src/services/deployments/mod.rs)
  4. KV service - Storage is partitioned by guild ID (apps/runtime/src/services/kv/service.rs)
  5. Logging - All log entries include guild context (apps/runtime/src/log_sink.rs)
Attempting to work around the guild-only scope by hacking the runtime is not supported and will likely break in future versions.

Future Considerations

While the guild-only scope is a core design principle, the flora team may consider:
  • Opt-in DM support via a separate event namespace (e.g., dmMessageCreate)
  • Global command registration as a compile-time option
  • User-scoped storage alongside guild-scoped KV
However, these features would need to maintain backward compatibility and the simplicity of the current model. For now, if you need DM support, flora is not the right choice for your project.

Build docs developers (and LLMs) love