# Configuration

URL: https://zerodev.ca/docs/medusa/developer-documentation/configuration

> Define zero-code configuration forms, conditional field visibility, and audit logging streams.



Medusa configuration is schema-driven. You do not need to write React or HTML components to configure a module. Instead, your module declares configuration forms and audit logging streams inside the `resources/` directory.

## Configuration Schemas (`resources/config/`)

Configuration forms define settings that server administrators manage in the web dashboard. Classes extend `Configs` from `#medusa/modules` and reside in `resources/config/`.

```js title="modules/example/resources/config/settings.js"
import { Configs } from "#medusa/modules";

export class SettingsConfig extends Configs {
    constructor(medusa) {
        super(medusa, {
            name: "settings",
            label: "Example Settings",
            description: "Manage core settings, channels, and operational limits."
        });
    }

    schema() {
        return {
            enabled: {
                type: "boolean",
                label: "Module Enabled",
                default: true
            },
            alertChannel: {
                type: "channel",
                label: "Alert Channel",
                default: ""
            },
            adminRole: {
                type: "role",
                label: "Manager Role",
                default: ""
            },
            allowedRoles: {
                type: "roles",
                label: "Authorized Staff Roles",
                default: []
            },
            currencySymbol: {
                type: "string",
                label: "Currency Symbol",
                default: "$"
            },
            maxStock: {
                type: "number",
                label: "Maximum Stock Limit",
                default: 100
            },
            themeColor: {
                type: "color",
                label: "Accent Color",
                default: "#22c55e"
            },
            deliveryMode: {
                type: "select",
                label: "Delivery Mode",
                default: "instant",
                options: [
                    { value: "instant", label: "Instant Delivery" },
                    { value: "manual", label: "Manual Approval" }
                ]
            },
            rewards: {
                type: "list",
                label: "Milestone Rewards",
                default: [],
                item: {
                    level: { type: "number", label: "Level", default: 1 },
                    role: { type: "role", label: "Reward Role", default: "" },
                    coins: { type: "number", label: "Coins Awarded", default: 50 }
                }
            }
        };
    }
}
```

### Supported Field Types

Medusa supports 18 field types:

| Type           | Stored Format   | Description                                                                                                                                                                  |
| -------------- | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"boolean"`    | `boolean`       | On/off toggle switch.                                                                                                                                                        |
| `"switch"`     | `boolean`       | Two-label toggle switch with custom state labels (`labels: [off, on]`) and optional entitlement gating (`addon: "<name>"`).                                                  |
| `"string"`     | `string`        | Single-line text input.                                                                                                                                                      |
| `"text"`       | `string`        | Multi-line textarea.                                                                                                                                                         |
| `"number"`     | `number`        | Numeric input with increment controls.                                                                                                                                       |
| `"select"`     | `string`        | Dropdown selector with options array.                                                                                                                                        |
| `"color"`      | `string`        | Hex color picker control (e.g. `"#22c55e"`).                                                                                                                                 |
| `"channel"`    | `string`        | Single Discord channel picker.                                                                                                                                               |
| `"channels"`   | `Array<string>` | Multi-select Discord channel picker.                                                                                                                                         |
| `"category"`   | `string`        | Single Discord category picker.                                                                                                                                              |
| `"categories"` | `Array<string>` | Multi-select Discord category picker.                                                                                                                                        |
| `"role"`       | `string`        | Single Discord role selector.                                                                                                                                                |
| `"roles"`      | `Array<string>` | Multi-select Discord role selector.                                                                                                                                          |
| `"emoji"`      | `string`        | Discord emoji picker (custom and Unicode).                                                                                                                                   |
| `"questions"`  | `Array<object>` | Modal builder. Holds up to five items: questions the member answers, and `info` text blocks (`{ type: "info", content }`) shown as read-only text.                           |
| `"list"`       | `Array<object>` | Dynamic repeating list with nested `item` schema.                                                                                                                            |
| `"canvas"`     | `object`        | Drag-and-drop card designer that stores a canvas layout document. See [Canvas Cards](/docs/medusa/developer-documentation/canvas).                                           |
| `"embed"`      | `object`        | Full embed builder with a live preview. Stores an embed template plus a `lists` map of per-item templates. See [Embed Fields](#embed-fields-embed).                          |
| `"server"`     | `string`        | Picks another server the bot is in. Pair it with resource fields through `server` to connect them to that server. See [Cross-Server Connections](#cross-server-connections). |

### Sections (`type: "section"`)

Split a long configuration card into headed sections with a `section` entry. Fields inside a section are stored flat on the group like any other field, so `visibleWhen`, `server` connections and `values()` work unchanged, and existing saved values keep loading. Keys must be unique across all sections of one group.

```js
schema() {
    return {
        enabled: { type: "boolean", default: true, label: "Enable Reminders" },
        limits: {
            type: "section",
            label: "Limits",
            fields: {
                maxPerUser: { type: "number", default: 10, label: "Maximum Active Reminders Per Member" },
                maxDays: { type: "number", default: 365, label: "Longest Reminder in Days" }
            }
        }
    };
}
```

Fields outside any section render first, then each section as an expandable block. Section labels translate through `schema.<module>.config.<group>.section.<section>.label` and `.desc` in `common.json`.

### Dynamic Select Sources (`source`)

A `"select"` field or list item subfield can take its options from a `list` field instead of a fixed `options` array. Each list entry becomes an option, keyed by its `id`, `key` or `name`.

| Form                        | Reads From                                          |
| --------------------------- | --------------------------------------------------- |
| `source: "<field>"`         | A `list` field in the same group.                   |
| `source: "<group>.<field>"` | A `list` field in another group of the same module. |

Set `empty` to the label shown when the source list has no entries. The dashboard keeps the dropdown and shows that label instead of falling back to a text input. Translate it with the `schema.<module>.config.<group>.<field>.<subfield>.empty` key.

```js
tier: {
    type: "select",
    source: "tiers.levels",
    empty: "No support tiers found",
    default: "",
    label: "Starting Support Tier"
}
```

### Placeholder Buttons (`placeholders`)

`string` and `text` fields, including list item subfields, accept a `placeholders` array of `{ key, label, sample }`. The dashboard renders one button per entry under the input that inserts `%key%` at the cursor. See [Placeholders](/docs/medusa/developer-documentation/placeholders#field-placeholders).

```js
naming: {
    type: "string",
    default: "ticket-%number%",
    label: "Channel Naming",
    placeholders: [{ key: "number", label: "Ticket Number", sample: "42" }]
}
```

### Embed Fields (`embed`)

An `embed` field renders the dashboard embed builder (author, title, description, fields, image, thumbnail, footer) with a live preview, and stores the embed template as an object. Pass the value straight to `this.medusa.embeds.resolve`. It works as a top-level field and as a list item subfield.

`placeholders` adds insert buttons under the description. `lists` declares list placeholders: each entry `{ key, label, placeholders }` adds `%key%` to those buttons and a textarea for the item template, stored under `value.lists[key]`. When the handler resolves the embed with an array under the same token, the item template is filled once per array entry and the results replace `%key%`. Give list placeholder `sample` values as arrays to preview several items.

To preview live rows instead of samples, set `source` to a sibling list field in the same group and `match` to `{ rowField: itemField }`. The preview fills the template once per row whose `rowField` equals the embed item's `itemField`, so a panel shows the categories assigned to it as they are edited. Each placeholder then takes `field`, the row key it reads, in place of `sample`. `format: "role"` shows a role id as `@Role Name`. `%custom_emoji_<name>%` tokens render as the bot emoji in the preview.

```js
embed: {
    type: "embed",
    default: {
        title: "Open a Ticket",
        description: "%categories%\nPick a category below.",
        lists: { categories: "> * %category_emoji% %category%\n-# %category_description%\n" }
    },
    label: "Panel Embed",
    lists: [
        {
            key: "categories",
            label: "Categories",
            source: "categories",
            match: { panel: "id" },
            placeholders: [
                { key: "category", label: "Category Name", field: "label" },
                { key: "category_emoji", label: "Category Emoji", field: "emoji" },
                { key: "category_description", label: "Category Description", field: "description" }
            ]
        }
    ]
}
```

```js
this.medusa.embeds.resolve(panel.embed, {
    categories: panel.categories.map(category => ({
        category: category.label,
        category_emoji: category.emoji,
        category_description: category.description
    }))
});
```

Translate the labels with `schema.<module>.config.<group>.<field>.lists.<key>`, `...lists.<key>.placeholders.<placeholder>` and `...placeholders.<placeholder>`.

### Migrating Old Values (`migrate`)

Any field or list item subfield can take a `migrate` function. When a stored group (or list entry) has saved values but none for that field, the config reader calls `migrate(stored)` and uses the result in place of the default. Use it when replacing old fields with a new one, so existing setups keep their content. The result is written back the next time the group is saved from the dashboard.

```js
embed: {
    type: "embed",
    default: this.embed(),
    label: "Panel Embed",
    migrate: item => ({ title: item.title, description: item.description })
}
```

### Conditional Visibility (`visibleWhen`)

Fields and list item subfields can be shown or hidden conditionally using `visibleWhen`. The field only renders in the dashboard when the specified condition is met.

```js
visibleWhen: { field: "<targetField>", value: "<expectedValue>" }
```

#### Dropdown Match

Show a field when a dropdown selects a specific option:

```js
action: {
    type: "select",
    default: "ban",
    label: "Trap Action",
    options: [
        { value: "ban", label: "Ban Member" },
        { value: "kick", label: "Kick Member" },
        { value: "timeout", label: "Timeout Member" },
        { value: "delete", label: "Delete Message Only" }
    ]
},
timeoutMinutes: {
    type: "number",
    default: 60,
    label: "Timeout Duration in Minutes",
    visibleWhen: { field: "action", value: "timeout" }
}
```

You can also match any option in a list:

```js
visibleWhen: { field: "action", in: ["mute", "timeout"] }
```

#### Switch Match

Show a field when a switch is turned on:

```js
enabled: {
    type: "boolean",
    default: false,
    label: "Enable Anti-Raid"
},
joinThreshold: {
    type: "number",
    default: 10,
    label: "Join Threshold",
    visibleWhen: { field: "enabled", value: true }
}
```

#### Textbox Typed In

Show a field only when a textbox has text entered:

```js
apiKey: {
    type: "string",
    default: "",
    label: "API Key"
},
apiEndpoint: {
    type: "string",
    default: "",
    label: "Custom Endpoint",
    visibleWhen: { field: "apiKey", notEmpty: true }
}
```

You can also check for field presence shorthand:

```js
visibleWhen: { field: "apiKey" }
```

### Live Configuration Previews

To render a live preview beside your configuration form, pass `preview: "<id>"` in your `Configs` constructor:

```js
export class SettingsConfig extends Configs {
    constructor(medusa) {
        super(medusa, {
            name: "settings",
            label: "Template Settings",
            description: "Manage core module settings.",
            preview: "template"
        });
    }
}
```

Then create a React preview component in `resources/dashboard/previews/<id>.tsx`:

```tsx title="modules/example/resources/dashboard/previews/template.tsx"
"use client";

import type { PreviewProps } from "@/components/pages/types";

export default function TemplateConfigPreview({ values, user, module }: PreviewProps) {
    const accentColor = typeof values.embedColor === "string" ? values.embedColor : "#5865f2";

    return (
        <div className="rounded-xl border border-border bg-card p-4">
            <h4 className="text-sm font-semibold">Live Preview</h4>
            <p className="text-xs text-muted-foreground">Accent: {accentColor}</p>
        </div>
    );
}
```

### Reading Configuration in Code

Configuration is saved per server from the dashboard, so always pass the server the code is acting in. Use the `guildId` of the interaction, message, member, channel, or stored record in front of you:

```js
const isEnabled = this.medusa.modules.config.value(this.module, "settings", "enabled", interaction.guildId);
const alertChannel = this.medusa.modules.config.value(this.module, "settings", "alertChannel", member.guild.id);

const allSettings = this.medusa.modules.config.values(this.module, "settings", message.guildId);
```

Each group resolves in three layers, per key: the schema default, then the shared value (`module:<id>`), then the server's own value (`module:<id>:<guildId>`). A server that has never saved a group reads the shared value, and saving one group never hides the shared values of another.

Clocks run once for the whole bot. Loop over servers and read config inside the loop:

```js
for (const guild of this.medusa.discord.client.guilds.cache.values()) {
    if (!this.medusa.modules.enabled(this.module, guild.id)) continue;
    if (!this.medusa.modules.config.value(this.module, "settings", "enabled", guild.id)) continue;
}
```

To save from code, pass the server as the last argument: `this.medusa.modules.save(this.module, "settings", values, interaction.guildId)`.

### Shared Configuration Groups

Some settings belong to the bot, not a server, such as a Lavalink node or an external API endpoint. Mark the group `global: true` and it always reads and saves the shared value, whatever server is selected. The dashboard labels it **All servers**.

```js
super(medusa, {
    name: "lavalink",
    label: "Lavalink Connection",
    global: true
});
```

### Multi-Server Compatibility

Medusa supports operating across multiple authorized Discord servers simultaneously. The primary server ID is declared in `GUILD_ID`, and additional authorized servers are declared in `SERVERS` as an object mapping server IDs to server metadata (`{ [guildId]: { name?: string } }`). Backward compatibility with `ALLOWED_GUILDS` is preserved.

* `this.medusa.modules.config.value(module, group, key, guildId)` and `values(module, group, guildId)` read configuration for one server.
* `this.medusa.discord.servers()` returns all currently connected authorized servers.
* `this.medusa.discord.connection(serverId)` returns one authorized server the bot is in, or `null`. Use it for [cross-server connections](#cross-server-connections).
* `this.medusa.logs.post(module, event, { guildId, ... })` and `this.medusa.logs.wants(module, event, guildId)` target the log channel configured for that server. Pass `guildId` in the payload whenever the payload has no `guild`, `channel`, `member`, `message`, or `role` to infer it from, otherwise the log goes to the primary server.
* Module routes called from the dashboard read the selected server with `this.guild(request)`.

### Cross-Server Connections

A group can connect to a second server. Add a `"server"` field, then point any `channel`, `channels`, `category`, `categories`, `role` or `roles` field at it with `server: "<field key>"`. That field lists the connected server's resources instead of the selected server's. Until a server is chosen, it shows **Select a server first**.

```js
schema() {
    return {
        server: {
            type: "server",
            default: "",
            label: "Appeal Server"
        },
        channel: {
            type: "channel",
            server: "server",
            default: "",
            label: "Panel Channel"
        },
        pingRoles: {
            type: "roles",
            server: "server",
            default: [],
            label: "Ping Roles"
        }
    };
}
```

* The server list only shows servers the bot is in that the dashboard user can access, minus the server being configured.
* `server` must name a `"server"` field in the same group, and only top-level resource fields can use it, not `list` item fields. Anything else throws when the module loads.
* Configuration import only remaps IDs from the selected server, so connected IDs are kept as they are.

Resolve the connected server in code with `this.medusa.discord.connection(serverId)`. It returns the `Guild` when the bot is in that server and the server is authorized, otherwise `null`. Look connected IDs up on that guild so a stale value can never reach another server:

```js
const settings = this.medusa.modules.config.values(this.module, "panel", interaction.guildId);
const server = this.medusa.discord.connection(settings.server);
const channel = server ? server.channels.cache.get(settings.channel) : null;
```

## Audit Logging Schemas (`resources/logs/`)

The logging system sends Discord audit embeds to channels selected by server administrators. Classes extend `Logs` from `#medusa/modules` and reside in `resources/logs/`.

```js title="modules/example/resources/logs/itemLogs.js"
import { Logs } from "#medusa/modules";

export class ItemLogs extends Logs {
    constructor(medusa) {
        super(medusa, {
            name: "itemLogs",
            label: "Item Logs",
            description: "Audit trail for item creation, updates, and purchases.",
            icon: "box",
            colour: "#22c55e"
        });
    }

    events() {
        return {
            created: {
                label: "Item Created",
                title: "%custom_emoji_check% • <server> » Item Created",
                summary: "%subject% was created by %actor%.",
                verb: "created",
                color: "#22c55e",
                default: true
            },
            deleted: {
                label: "Item Deleted",
                title: "%custom_emoji_cross% • <server> » Item Deleted",
                summary: "%subject% was deleted by %actor%.",
                verb: "deleted",
                color: "#ef4444",
                default: true
            },
            purchased: {
                label: "Item Purchased",
                title: "<server> » Item Purchased",
                summary: "%actor% purchased %subject%.",
                verb: "executed",
                color: "#3b82f6",
                default: true
            }
        };
    }
}
```

### Standard Action Verbs and Colors

When an event specifies a `verb`, Medusa assigns colors automatically if not explicitly overridden:

| Verb                                                | Default Color | Standard Hex Code |
| --------------------------------------------------- | ------------- | ----------------- |
| `created`, `added`, `unbanned`, `unmuted`, `joined` | Green         | `#22c55e`         |
| `deleted`, `removed`, `banned`                      | Red           | `#ef4444`         |
| `updated`, `changed`, `edited`                      | Amber         | `#f59e0b`         |
| `muted`                                             | Orange        | `#f97316`         |
| `moved`, `executed`                                 | Blue          | `#3b82f6`         |
| `left`                                              | Grey          | `#64748b`         |

### Dispatching Log Events

Dispatch logs from handlers using `this.medusa.logs.post()`:

```js
await this.medusa.logs.post(this.module, "purchased", {
    actor: interaction.user,
    subject: item.name,
    fields: [
        { name: "Price", value: `$${item.price}`, inline: true },
        { name: "Remaining Stock", value: `${item.quantity}`, inline: true }
    ],
    tokens: {
        item_id: item.identifier
    }
});
```

If the administrator has disabled the event or has not set a logging channel, Medusa skips dispatching without errors.

### Log Server

Every module's **Log Channels** group starts with a **Log Server** picker (a `type: "server"` field keyed `server`). Each category channel field carries `server: "server"`, so its dropdown lists the chosen server's channels. Leaving it on **This Server** keeps logs in the server where the event happened.

When a log server is set, `this.medusa.logs.post()` sends to the category channel in that server instead. The embed is still built in the source server's scope (title `<server>` token, thumbnail, footer), and gets an author line `Sent from <source server>` with the source server's icon. Modules need no changes: keep passing the source `guildId`.

* Stored per source server in the `logs:<module>:<guildId>` bucket as `server` next to `channels` and `events`. An empty value means the source server.
* The log server must be an authorized server the bot is in (`this.medusa.discord.connection(serverId)`), and the dashboard user must have access to it.
* Channels that do not belong to the log server are dropped on save and ignored at send time.
* `LogInterface` exposes `server(guildId)` (the configured log server or `""`) and `target(guildId)` (the log server, else `guildId`).
* The `sentFrom` key in the `logs` translation group localizes the author line (`%server%` is the source server name).
* Bulk-applying a channel from the Logging index resets the selected modules' log server to the current server, since the bulk picker lists current-server channels.
