Schema
Small, focused helpers that coerce, validate, and infer types.
Confkit’s schema is intentionally tiny but expressive for configuration needs. Every helper:
- Coerces from untyped input (env/file/cloud) into a typed value
- Validates at runtime and reports precise issues
- Carries metadata for redaction and client exposure
Primitives
s.string()— stringss.int()— integers (coerces from strings like"42")s.number()/s.float()— numbers (finite)s.boolean()— booleans (accepts"true"|"false")s.enum([...])— one of a fixed set of stringss.url()— absolute URL string;.origin()to extract origins.uuid()— UUID (RFC 4122)s.uuid()— UUID (RFC 4122)s.port()— TCP port 1–65535s.email()— simple email format checks.duration()— human/ISO8601 duration to milliseconds (500ms,2s,5m,PT1H)s.nonempty()— non-empty strings.regex(re, label?)— string matching a pattern (label used in error)s.host()— rough hostname validations.ip()— simple IPv4/IPv6 validations.json([inner])— parse JSON strings; optionally validate withinner
Structured
s.object({ ... })— nested object; redaction follows schema shapes.array(inner)— arrays ofinners.record(inner)— record of string keys toinners.union([a,b,...])— first branch that validates wins
Modifiers
.optional()— allowundefined.default(v)— default whenundefined- also accepts a function:
.default(() => 3000)
- also accepts a function:
.client()— mark safe for client bundle exposure.refine(predicate, message)— additional invariant after coercion.transform(fn)— map to a new value after validations.secret(inner)— mark as secret (masked when redacting; auditable on access)
Tip: Deep Redaction
Redaction respects your schema shape all the way down: arrays, objects, records, and unions are redacted by their child schemas. Structure in, structure out — just safer.
Example
import { s } from 'confkit';
const schema = {
PORT: s.port().default(8080),
BASE_URL: s.url(),
ADMIN_EMAILS: s.array(s.email()).default([]),
DB: s.object({
HOST: s.string(),
POOL: s.object({ min: s.int().default(1), max: s.int().default(10) }),
SSL: s.boolean().default(false),
}),
STRIPE_SECRET: s.secret(s.string()),
};Using the Schema
const env = await config.ready(); env.PORT— typed object accessawait config.get('PORT')— typed single valueawait config.toJSON({ redact: true })— secrets masked with•••await config.pickClient()— only.client()keys (and/or accepted prefixes)config.describeSchema()— structured description useful for tooling
Cross-field Validation
Provide additional invariants across multiple keys via validate on defineConfig.
export const config = defineConfig({
sources: [/* ... */],
schema: { A: s.int(), B: s.int() },
validate: (env) => {
const issues: Array<{ path: string; message: string }> = [];
if (env.A > env.B) issues.push({ path: 'A', message: 'A must be <= B' });
return issues;
},
});Client Exposure
defineConfig accepts clientPrefix(es) and requireClientPrefix. By default, keys marked .client() or keys starting with one of PUBLIC_, NEXT_PUBLIC_, EXPO_PUBLIC_ are included by pickClient(). In production, Confkit enforces that .client() keys also match a configured prefix unless you override requireClientPrefix.
Enforce a prefix
Set requireClientPrefix: true and choose clientPrefix(es) to forbid accidental exposure of non‑prefixed keys.