Configuration Schema
Plugin configuration in Authrim uses Zod schemas for validation and automatic Admin UI generation. This guide covers best practices for defining configuration schemas.
Zod Schema Basics
Section titled “Zod Schema Basics”Every plugin must define a configSchema using Zod:
import { z } from 'zod';
const configSchema = z.object({ apiKey: z.string().min(1), timeout: z.number().int().min(1000).max(30000).default(10000), retries: z.number().int().min(0).max(5).default(3), enabled: z.boolean().default(true),});
type MyConfig = z.infer<typeof configSchema>;UI Hints with describe()
Section titled “UI Hints with describe()”Use .describe() to provide labels and help text for the Admin UI:
const configSchema = z.object({ apiKey: z .string() .min(1) .describe('API key from your provider dashboard'),
endpoint: z .string() .url() .default('https://api.example.com') .describe('API endpoint URL. Change only if using a custom server.'),
timeout: z .number() .int() .min(1000) .max(30000) .default(10000) .describe('Request timeout in milliseconds (1000-30000)'),
logLevel: z .enum(['debug', 'info', 'warn', 'error']) .default('info') .describe('Logging verbosity level'),});The Admin UI automatically generates forms based on these descriptions:
| Field Type | UI Component |
|---|---|
z.string() | Text input |
z.string().url() | URL input |
z.number() | Number input |
z.boolean() | Toggle switch |
z.enum([...]) | Dropdown select |
z.string() with secret name | Password input |
Secret Fields
Section titled “Secret Fields”Automatic Detection
Section titled “Automatic Detection”Authrim automatically detects fields matching these patterns and encrypts them when PLUGIN_ENCRYPTION_KEY or KEY_MANAGER_SECRET is configured:
apiKey,apiSecretsecretKey,clientSecretpassword,tokenauthToken,accessToken,refreshTokenprivateKey,credential
If no encryption key is available, Admin API writes continue and the value is stored unencrypted with a warning log. Configure an encryption key before storing production secrets.
const configSchema = z.object({ // Automatically detected and encrypted apiKey: z.string().describe('Your API key'), clientSecret: z.string().describe('OAuth client secret'), authToken: z.string().describe('Authentication token'),
// NOT encrypted (doesn't match patterns) endpoint: z.string().url(), timeout: z.number(),});Manual Secret Declaration
Section titled “Manual Secret Declaration”For custom secret field names, specify them in the API request:
curl -X PUT "/api/admin/plugins/my-plugin/config" \ -H "Content-Type: application/json" \ -d '{ "config": { "customCredential": "super-secret-value", "endpoint": "https://api.example.com" }, "secret_fields": ["customCredential"] }'Masking in API Responses
Section titled “Masking in API Responses”Secret values are masked in all API responses:
{ "config": { "apiKey": "sk_l****XYZ1", "clientSecret": "cs_a****bcde", "endpoint": "https://api.example.com" }}Masking format:
- First 4 and last 4 characters shown (e.g.,
sk_l****XYZ1) - Short values completely masked as
****
JSON Schema Conversion
Section titled “JSON Schema Conversion”Plugin schemas are automatically converted to JSON Schema for the Admin UI:
// Zod schemaconst configSchema = z.object({ apiKey: z.string().min(1).describe('Your API key'), timeout: z.number().min(1000).max(30000).default(10000), algorithm: z.enum(['sha1', 'sha256', 'sha512']).default('sha256'),});
// Converted to JSON Schema{ "type": "object", "properties": { "apiKey": { "type": "string", "minLength": 1, "description": "Your API key" }, "timeout": { "type": "number", "minimum": 1000, "maximum": 30000, "default": 10000 }, "algorithm": { "type": "string", "enum": ["sha1", "sha256", "sha512"], "default": "sha256" } }, "required": ["apiKey"]}Configuration Priority
Section titled “Configuration Priority”The Plugin Management Admin API resolves configuration in this order:
┌─────────────────────────────────────────┐│ 1. Environment Variables + built-in env │├─────────────────────────────────────────┤│ 2. KV Storage (global config) │├─────────────────────────────────────────┤│ 3. KV Storage (per-tenant override) │├─────────────────────────────────────────┤│ 4. Plugin schema defaults during parse │└─────────────────────────────────────────┘Runtime plugin loaders may cache resolved plugin registry and configuration values. Admin API writes to config, enable, and disable endpoints invalidate the plugin runtime cache for the affected plugin and tenant scope.
Environment Variable Convention
Section titled “Environment Variable Convention”# Format: PLUGIN_{PLUGIN_ID}_CONFIG=<JSON>
# Plugin IDs with hyphens become underscoresBuilt-in notifier plugins can also bootstrap from deployment env values. For example, notifier-resend reads RESEND_API_KEY and EMAIL_FROM, and notifier-cloudflare reads EMAIL_FROM and EMAIL_FROM_NAME before KV overrides are applied.
Multi-tenant Configuration
Section titled “Multi-tenant Configuration”KV Key Structure
Section titled “KV Key Structure”| Key Pattern | Description |
|---|---|
plugins:config:{pluginId} | Global configuration |
plugins:config:{pluginId}:tenant:{tenantId} | Tenant-specific override |
plugins:enabled:{pluginId} | Global enable/disable |
plugins:enabled:{pluginId}:tenant:{tenantId} | Tenant enable/disable |
Tenant Override Example
Section titled “Tenant Override Example”// Global configconst globalConfig = { apiKey: 'default-api-key',};
// Tenant A overrides just the from addressconst tenantAConfig = {};
// Resolved config for Tenant A (merged){ apiKey: 'default-api-key', // From global}API Usage
Section titled “API Usage”# Get tenant-specific config from a platform settings screencurl "/api/admin/platform/plugins/notifier-resend/config?tenant_id=tenant_123"
# Set tenant-specific config from a platform settings screencurl -X PUT "/api/admin/platform/plugins/notifier-resend/config" \Tenant-facing /api/admin/plugins/... routes use the current tenant context. Use /api/admin/platform/plugins/... when you need to pass tenant_id explicitly.
Nested Configuration
Section titled “Nested Configuration”Keep configuration structures flat or shallow:
// ✅ Good: Flat or shallow nestingconst configSchema = z.object({ apiKey: z.string(), smtp: z.object({ host: z.string(), port: z.number(), secure: z.boolean(), }), retries: z.object({ max: z.number().default(3), delay: z.number().default(1000), }),});// ❌ Bad: Excessively deep nestingconst badConfigSchema = z.object({ level1: z.object({ level2: z.object({ level3: z.object({ // ... 20+ levels deep - secrets won't be masked! }), }), }),});Advanced Schema Patterns
Section titled “Advanced Schema Patterns”Optional with Defaults
Section titled “Optional with Defaults”const configSchema = z.object({ // Required (no default) apiKey: z.string().min(1),
// Optional with default timeout: z.number().default(10000),
// Optional without default webhook: z.string().url().optional(),
// Optional with nullable fallbackUrl: z.string().url().nullable().default(null),});Union Types
Section titled “Union Types”const configSchema = z.object({ // Different authentication methods auth: z.union([ z.object({ type: z.literal('api_key'), apiKey: z.string(), }), z.object({ type: z.literal('oauth'), clientId: z.string(), clientSecret: z.string(), }), ]),});Conditional Validation
Section titled “Conditional Validation”const configSchema = z.object({ useTLS: z.boolean().default(true), tlsCert: z.string().optional(), tlsKey: z.string().optional(),}).refine( (data) => !data.useTLS || (data.tlsCert && data.tlsKey), { message: 'TLS certificate and key are required when TLS is enabled' });Array Configurations
Section titled “Array Configurations”const configSchema = z.object({ endpoints: z .array(z.string().url()) .min(1) .max(5) .describe('List of API endpoints (1-5)'),
allowedDomains: z .array(z.string()) .default([]) .describe('Domains allowed for callbacks'),});Best Practices
Section titled “Best Practices”Use Sensible Defaults
Section titled “Use Sensible Defaults”const configSchema = z.object({ // Secure defaults timeout: z.number().default(10000), // 10 seconds retries: z.number().default(3), validateCerts: z.boolean().default(true), // Security: always validate
// Production-safe defaults logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'), rateLimit: z.number().default(60),});Document Constraints
Section titled “Document Constraints”const configSchema = z.object({ timeout: z .number() .int() .min(1000) .max(30000) .default(10000) .describe('Request timeout in ms (1000-30000). Increase for slow networks.'),
batchSize: z .number() .int() .min(1) .max(100) .default(10) .describe('Items per batch (1-100). Higher values use more memory.'),});Group Related Settings
Section titled “Group Related Settings”const configSchema = z.object({ // Connection settings host: z.string().default('smtp.example.com'), port: z.number().default(587), secure: z.boolean().default(true),
// Authentication username: z.string().optional(), password: z.string().optional(),
// Message defaults defaultFrom: z.string().email(), defaultReplyTo: z.string().email().optional(),});Next Steps
Section titled “Next Steps”- Deployment & Distribution - Package and publish your plugin
- Admin UI Management - Managing plugins via the Admin Console
- Plugin Management API - API reference