Plugins need to store two types of data:
This guide explains when to use ConfigService (for config) vs custom Drizzle schemas (for data).
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 items
ConfigService provides centralized, type-safe storage for plugin-level configuration with automatic secret encryption and schema migration support.
Use ConfigService when storing:
Key indicator: The data controls how the plugin operates, not what content it manages.
import { coreServices, type ConfigService } from "@checkstack/backend-api";
import { z } from "zod";
// Schema for queue config
const 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
);
}
},
});
},
});
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 undefined
config.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 }, ...]
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)
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 loaded
const config = await configService.get("github", schemaV2, 2, migrations);
Use custom Drizzle schemas when storing:
Key indicator: The data is content managed by users, not settings that control the plugin.
// src/schema.ts
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 configuration
ConfigService is optimized for:
But entities need:
Custom schemas are the right choice for user data.
| 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 |
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 UI
await config.set("platform.registration", platformRegistrationConfigV1, 1, {
allowRegistration: false, // Lock down registration
});
Health Check Backend - Check instances:
// Many user-created health checks
export const healthCheckConfigurations = pgTable(/* ... */);
// Examples: "API health", "DB ping", "Homepage check"
Catalog Backend - Systems and groups:
// User-managed catalog entities
export const systems = pgTable(/* ... */);
export const groups = pgTable(/* ... */);
If you’re currently using custom tables for plugin config, migrate to ConfigService:
// ❌ Old: Custom table for plugin config
export const authStrategy = pgTable("auth_strategy", {
id: text("id").primaryKey(),
enabled: boolean("enabled"),
config: jsonb("config"),
});
// Manual encryption/decryption required
import { configString } from "@checkstack/backend-api";
// ✅ New: Use ConfigService
await config.set("github", schema, 1, {
clientId: "...",
clientSecret: "...", // Auto-encrypted via configString({ "x-secret": true })
enabled: true,
});
// Drop old table in migration
DROP TABLE IF EXISTS "auth_strategy";
// ✅ Good: Plugin behavior config
await config.set("active-theme", themeSchema, 1, { theme: "dark" });
// ✅ Good: User-created entities
await db.insert(healthChecks).values({
name: "API Health Check",
url: "https://api.example.com",
});
import { configString } from "@checkstack/backend-api";
// ✅ Good: Explicit secret marking
const schema = z.object({
apiKey: configString({ "x-secret": true }),
apiUrl: configString({}).url(),
});
// ❌ Bad: Storing secrets without encryption
export const integrations = pgTable("integrations", {
apiKey: text("api_key"), // Not encrypted!
});
// ✅ Good: Use ConfigService for secrets
const schema = z.object({
webhookUrl: configString({ "x-secret": true }),
});
await config.set("slack-integration", schema, 1, {
webhookUrl: "https://...", // Auto-encrypted
});