Plugin Configuration Storage
Overview
Section titled “Overview”Plugins need to store two types of data:
- Plugin-level configuration - Settings that control how the plugin behaves
- User data/entities - Content created and managed by users
This guide explains when to use ConfigService (for config) vs custom Drizzle schemas (for data).
Quick Decision Tree
Section titled “Quick Decision Tree”Is this data used to configure/control plugin behavior? ├─ YES → Use ConfigService │ Examples: Active queue provider, enabled auth strategies, plugin settings │ └─ NO → Is this user-created content? └─ YES → Use custom Drizzle schema Examples: Health check instances, catalog systems, user-created itemsConfigService: For Plugin Configuration
Section titled “ConfigService: For Plugin Configuration”What is ConfigService?
Section titled “What is ConfigService?”ConfigService provides centralized, type-safe storage for plugin-level configuration with automatic secret encryption and schema migration support.
When to Use ConfigService
Section titled “When to Use ConfigService”Use ConfigService when storing:
- ✅ Plugin behavior settings (e.g., which queue provider to use)
- ✅ Strategy/provider selections (e.g., active auth strategies)
- ✅ Plugin-level toggles and preferences
- ✅ Especially: Any config containing secrets (API keys, tokens, passwords)
Key indicator: The data controls how the plugin operates, not what content it manages.
Example: Queue Configuration
Section titled “Example: Queue Configuration”import { coreServices, type ConfigService } from "@checkstack/backend-api";import { z } from "zod";
// Schema for queue configconst queueConfigSchema = z.object({ pluginId: z.string(), // "memory", "bullmq", etc. config: z.record(z.string(), z.unknown()),});
export default createBackendPlugin({ pluginId: "backend", register(env) { env.registerInit({ deps: { config: coreServices.config, // Inject ConfigService queueManager: coreServices.queueManager, }, init: async ({ config, queueManager }) => { // Load active queue provider configuration const queueConfig = await config.get( "active", queueConfigSchema, 1 );
if (queueConfig) { await queueManager.setActiveBackend( queueConfig.pluginId, queueConfig.config ); } }, }); },});ConfigService API
Section titled “ConfigService API”config.get(configId, schema, version, migrations?)
Load a configuration:
const strategy = await config.get( "github", githubStrategySchema, 1, migrations);
if (strategy?.enabled) { // Use the strategy}config.getRedacted(configId, schema, version, migrations?)
Load configuration with secrets removed (safe for frontend):
const redacted = await config.getRedacted( "github", githubStrategySchema, 1);// redacted.clientSecret is undefinedconfig.set(configId, schema, version, data)
Save a configuration:
await config.set( "github", githubStrategySchema, 1, { clientId: "abc123", clientSecret: "secret", // Automatically encrypted! enabled: true, });config.delete(configId)
Delete a configuration:
await config.delete("github");config.list()
List all configurations for this plugin:
const configs = await config.list();// Returns: [{ configId: "github", updatedAt: Date }, ...]Secret Handling
Section titled “Secret Handling”ConfigService automatically encrypts/decrypts secrets marked with configString({ "x-secret": true }):
import { configString, configBoolean, configNumber } from "@checkstack/backend-api";
const githubStrategySchema = z.object({ clientId: configString({}), clientSecret: configString({ "x-secret": true }), // Marked as secret enabled: configBoolean({}),});
// When you save:await config.set("github", schema, 1, { clientId: "abc123", clientSecret: "my-secret", // Stored encrypted in database enabled: true,});
// When you load:const strategy = await config.get("github", schema, 1);// strategy.clientSecret = "my-secret" (decrypted automatically)
// When frontend loads:const redacted = await config.getRedacted("github", schema, 1);// redacted.clientSecret is undefined (removed)Schema Versioning
Section titled “Schema Versioning”ConfigService supports schema migrations (see Versioned Configurations):
const migrations = [ { fromVersion: 1, toVersion: 2, migrate: (data: V1) => ({ ...data, newField: "default" }), },];
// Old configs are automatically migrated when loadedconst config = await configService.get("github", schemaV2, 2, migrations);Custom Drizzle Schemas: For User Data
Section titled “Custom Drizzle Schemas: For User Data”When to Use Custom Schemas
Section titled “When to Use Custom Schemas”Use custom Drizzle schemas when storing:
- ✅ User-created entities (health checks, catalog systems, etc.)
- ✅ User content (incidents, maintenance windows, etc.)
- ✅ Relational data with foreign keys
- ✅ Data that users CRUD via the UI
- ✅ Historical data (health check runs, audit logs)
Key indicator: The data is content managed by users, not settings that control the plugin.
Example: Health Check Instances
Section titled “Example: Health Check Instances”export const healthCheckConfigurations = pgTable("health_check_configurations", { id: uuid("id").primaryKey().defaultRandom(), name: text("name").notNull(), strategyId: text("strategy_id").notNull(), // "http", "ping", etc. config: jsonb("config").notNull(), intervalSeconds: integer("interval_seconds").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(),});
// These are USER-CREATED health check instances// Examples: "Check example.com homepage", "Ping database server"// NOT plugin configurationWhy Not ConfigService for Entities?
Section titled “Why Not ConfigService for Entities?”ConfigService is optimized for:
- Plugin-level settings (one active config per plugin)
- Automatic secret encryption
- Schema migrations
But entities need:
- Many instances per plugin (hundreds of health checks)
- Complex foreign key relationships
- Efficient querying and pagination
- User-friendly CRUD operations
Custom schemas are the right choice for user data.
Comparison Table
Section titled “Comparison Table”| Aspect | ConfigService | Custom Drizzle Schema |
|---|---|---|
| Purpose | Plugin behavior settings | User-created content |
| Scope | Plugin-level | User-level |
| Cardinality | One or few per plugin | Many instances |
| Secrets | Automatic encryption | Manual if needed |
| Migrations | Built-in versioning | Drizzle migrations |
| Queries | Simple get/set by ID | Complex SQL queries |
| UI | Settings pages | CRUD interfaces |
| Examples | Queue provider, auth strategies | Health checks, systems, users |
Real-World Examples
Section titled “Real-World Examples”✅ ConfigService Examples
Section titled “✅ ConfigService Examples”Auth Backend - Strategy configurations:
import { configString } from "@checkstack/backend-api";
// Stores: "Which auth strategies are enabled?"await config.set("github", githubSchema, 1, { clientId: "...", clientSecret: "...", // Auto-encrypted via configString({ "x-secret": true }) enabled: true,});Queue Backend - Active provider:
// Stores: "Which queue provider is active?"await config.set("active", queueSchema, 1, { pluginId: "bullmq", config: { redis: { host: "localhost" } },});Auth Backend - Platform Registration Settings:
// Controls whether new user registration is allowed platform-wide.// When disabled, only existing users can sign in - useful for private deployments.const platformRegistrationConfigV1 = z.object({ allowRegistration: z .boolean() .default(true) .describe( "When enabled, new users can create accounts. When disabled, only existing users can sign in." ),});
// The schema's describe() is automatically shown in DynamicForm settings UIawait config.set("platform.registration", platformRegistrationConfigV1, 1, { allowRegistration: false, // Lock down registration});✅ Custom Schema Examples
Section titled “✅ Custom Schema Examples”Health Check Backend - Check instances:
// Many user-created health checksexport const healthCheckConfigurations = pgTable(/* ... */);// Examples: "API health", "DB ping", "Homepage check"Catalog Backend - Systems and groups:
// User-managed catalog entitiesexport const systems = pgTable(/* ... */);export const groups = pgTable(/* ... */);Migration Guide
Section titled “Migration Guide”If you’re currently using custom tables for plugin config, migrate to ConfigService:
Before (Custom Table):
Section titled “Before (Custom Table):”// ❌ Old: Custom table for plugin configexport const authStrategy = pgTable("auth_strategy", { id: text("id").primaryKey(), enabled: boolean("enabled"), config: jsonb("config"),});
// Manual encryption/decryption requiredAfter (ConfigService):
Section titled “After (ConfigService):”import { configString } from "@checkstack/backend-api";
// ✅ New: Use ConfigServiceawait config.set("github", schema, 1, { clientId: "...", clientSecret: "...", // Auto-encrypted via configString({ "x-secret": true }) enabled: true,});
// Drop old table in migrationDROP TABLE IF EXISTS "auth_strategy";Best Practices
Section titled “Best Practices”1. Use ConfigService for Plugin Settings
Section titled “1. Use ConfigService for Plugin Settings”// ✅ Good: Plugin behavior configawait config.set("active-theme", themeSchema, 1, { theme: "dark" });2. Use Custom Schema for User Content
Section titled “2. Use Custom Schema for User Content”// ✅ Good: User-created entitiesawait db.insert(healthChecks).values({ name: "API Health Check", url: "https://api.example.com",});3. Mark Secrets in Schemas
Section titled “3. Mark Secrets in Schemas”import { configString } from "@checkstack/backend-api";
// ✅ Good: Explicit secret markingconst schema = z.object({ apiKey: configString({ "x-secret": true }), apiUrl: configString({}).url(),});4. Don’t Store Secrets in Custom Schemas
Section titled “4. Don’t Store Secrets in Custom Schemas”// ❌ Bad: Storing secrets without encryptionexport const integrations = pgTable("integrations", { apiKey: text("api_key"), // Not encrypted!});
// ✅ Good: Use ConfigService for secretsconst schema = z.object({ webhookUrl: configString({ "x-secret": true }),});await config.set("slack-integration", schema, 1, { webhookUrl: "https://...", // Auto-encrypted});