Skip to content

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 TypeUI 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 namePassword input

Secret Fields

Automatic Detection

Authrim automatically detects and encrypts fields matching these patterns:

  • apiKey, apiSecret
  • secretKey, clientSecret
  • password, token
  • authToken, accessToken, refreshToken
  • privateKey, 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:

Terminal window
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 schema
const 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

Terminal window
# Format: PLUGIN_{PLUGIN_ID}_CONFIG=<JSON>
PLUGIN_NOTIFIER_RESEND_CONFIG='{"apiKey":"re_xxx","defaultFrom":"[email protected]"}'
# Plugin IDs with hyphens become underscores
PLUGIN_AUTHENTICATOR_TOTP_CONFIG='{"issuer":"MyApp","digits":6}'

Multi-tenant Configuration

KV Key Structure

Key PatternDescription
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 config
const globalConfig = {
apiKey: 'default-api-key',
defaultFrom: '[email protected]',
};
// Tenant A overrides just the from address
const tenantAConfig = {
defaultFrom: '[email protected]',
};
// Resolved config for Tenant A (merged)
{
apiKey: 'default-api-key', // From global
defaultFrom: '[email protected]' // From tenant
}

API Usage

Terminal window
# Get tenant-specific config
curl "/api/admin/plugins/notifier-resend/config?tenant_id=tenant_123"
# Set tenant-specific config
curl -X PUT "/api/admin/plugins/notifier-resend/config" \
-d '{"config": {"defaultFrom": "[email protected]"}, "tenant_id": "tenant_123"}'

Nested Configuration

Keep configuration structures flat or shallow:

// ✅ Good: Flat or shallow nesting
const 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 nesting
const 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.'),
});
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