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
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()
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
Automatic Detection
Authrim automatically detects and encrypts fields matching these patterns:
apiKey,apiSecretsecretKey,clientSecretpassword,tokenauthToken,accessToken,refreshTokenprivateKey,credential
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
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
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
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
Plugin configuration is resolved in the following order:
┌─────────────────────────────────────────┐│ 1. In-Memory Cache (60s TTL) │ ← Fastest├─────────────────────────────────────────┤│ 2. KV Storage (per-tenant override) │├─────────────────────────────────────────┤│ 3. KV Storage (global config) │├─────────────────────────────────────────┤│ 4. Environment Variables │├─────────────────────────────────────────┤│ 5. Zod Schema Default Values │ ← Fallback└─────────────────────────────────────────┘Environment Variable Convention
# Format: PLUGIN_{PLUGIN_ID}_CONFIG=<JSON>
# Plugin IDs with hyphens become underscoresPLUGIN_AUTHENTICATOR_TOTP_CONFIG='{"issuer":"MyApp","digits":6}'Multi-tenant Configuration
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
// 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
# Get tenant-specific configcurl "/api/admin/plugins/notifier-resend/config?tenant_id=tenant_123"
# Set tenant-specific configcurl -X PUT "/api/admin/plugins/notifier-resend/config" \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
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
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
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
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
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
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
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
- Deployment & Distribution - Package and publish your plugin
- Admin UI Management - Managing plugins via the Admin Console
- Plugin Management API - API reference