Validation
ElyOS uses two different validation layers:
- Varlock — typesafe validation of environment variables at application startup
- Valibot — data validation for server action inputs and client-side forms
Environment Schema Validation (Varlock)
Section titled “Environment Schema Validation (Varlock)”ElyOS uses Varlock for typesafe validation of all environment variables. Varlock runs at application startup — before node server.js — so configuration errors are caught immediately.
The Env_Schema File
Section titled “The Env_Schema File”The type and validation rules for all environment variables are defined in apps/web/.env.schema. This is the single source of truth — Varlock generates TypeScript types from this.
# @generateTypes(lang=ts, path=src/env.d.ts)# @defaultRequired=false
# Bootstrap credentials (required, comes from local .env file)# @required @type=infisicalClientIdINFISICAL_CLIENT_ID=
# @required @type=infisicalClientSecret @sensitiveINFISICAL_CLIENT_SECRET=
# @required @type=enum(development, production, test)NODE_ENV=development
# @required @type=url @sensitiveDATABASE_URL=
# @type=portELYOS_PORT=3000
# @sensitive @requiredBETTER_AUTH_SECRET=
# @required @type=urlBETTER_AUTH_URL=The @generateTypes decorator causes Varlock to automatically generate a src/env.d.ts TypeScript types file — don’t edit this manually.
Supported Types and Decorators
Section titled “Supported Types and Decorators”| Decorator / Type | Description |
|---|---|
@required | Required variable — missing it stops the app |
@sensitive | Sensitive data — not logged |
@type=string | String type |
@type=number / @type=port | Number / port type |
@type=boolean | Boolean type |
@type=url | URL validation |
@type=enum(a, b, c) | Enumerated values |
@type=string(startsWith=prefix_) | Prefix validation (e.g., re_ for Resend API key) |
@type=number(min=1, max=100) | Number range validation |
@type=infisicalClientId | Infisical Machine Identity client ID |
@type=infisicalClientSecret | Infisical Machine Identity client secret |
Accessing Environment Values in Application
Section titled “Accessing Environment Values in Application”The src/lib/env.ts re-exports Varlock-validated process.env values in a typesafe way. All existing imports remain unchanged:
import { env } from '$lib/env';
// Typesafe, already validated by Varlock at startupconst dbUrl = env.DATABASE_URL;const port = env.ELYOS_PORT;Error Messages
Section titled “Error Messages”If validation fails, Varlock logs the specific error and stops the application:
[Varlock] ERROR: Missing bootstrap credential: INFISICAL_CLIENT_ID[Varlock] ERROR: Missing required secret: DATABASE_URL[Varlock] ERROR: Type validation failed: SMTP_PORT — expected: number, got: "invalid"[Varlock] 42 secrets loaded successfully (production/elyos-core)Data Validation (Valibot)
Section titled “Data Validation (Valibot)”ElyOS uses Valibot for validating all server action inputs and client-side data. Valibot is a lightweight, TypeScript-first validation library that’s tree-shaking-friendly and provides excellent type inference.
Installation
Section titled “Installation”Valibot is already part of ElyOS dependencies:
import * as v from 'valibot';Basic Schemas
Section titled “Basic Schemas”// Primitivesv.string()v.number()v.boolean()
// Modifiersv.optional(v.string()) // undefined is acceptedv.nullable(v.string()) // null is acceptedv.nullish(v.string()) // null and undefined are accepted
// Complex typesv.array(v.string())v.object({ name: v.string(), age: v.number() })v.union([v.string(), v.number()])v.picklist(['admin', 'user', 'guest'])Chaining Validators (v.pipe)
Section titled “Chaining Validators (v.pipe)”const nameSchema = v.pipe( v.string(), v.minLength(2), v.maxLength(100), v.trim());
const emailSchema = v.pipe(v.string(), v.email());const positiveInt = v.pipe(v.number(), v.integer(), v.minValue(1));Type Inference
Section titled “Type Inference”const userSchema = v.object({ name: v.string(), email: v.pipe(v.string(), v.email()), role: v.picklist(['admin', 'user'])});
type User = v.InferOutput<typeof userSchema>;// → { name: string; email: string; role: 'admin' | 'user' }Server-Side Usage (Server Actions)
Section titled “Server-Side Usage (Server Actions)”The command wrapper automatically validates input against the provided schema. If validation fails, the handler doesn’t run.
import { command } from '$app/server';import * as v from 'valibot';
const schema = v.object({ title: v.pipe(v.string(), v.minLength(1)), priority: v.picklist(['low', 'medium', 'high'])});
export const createTask = command(schema, async (input) => { // input type: v.InferOutput<typeof schema> // only validated data reaches here});See Server Actions for more details.
Client-Side Usage
Section titled “Client-Side Usage”import * as v from 'valibot';
const loginSchema = v.object({ email: v.pipe(v.string(), v.email('Invalid email address')), password: v.pipe(v.string(), v.minLength(8, 'At least 8 characters required'))});
function validateLogin(data: unknown) { const result = v.safeParse(loginSchema, data);
if (!result.success) { const errors = result.issues.map(i => i.message); return { valid: false, errors }; }
return { valid: true, data: result.output };}parse vs safeParse
Section titled “parse vs safeParse”| Function | On Validation Failure | Return Value |
|---|---|---|
v.parse(schema, data) | Throws exception | validated data |
v.safeParse(schema, data) | Doesn’t throw | { success, output, issues } |
On the client side, safeParse is generally recommended; on the server, the command wrapper handles validation automatically.
Custom Error Messages
Section titled “Custom Error Messages”const schema = v.object({ name: v.pipe( v.string('Name must be text'), v.minLength(2, 'Name must be at least 2 characters'), v.maxLength(100, 'Name can be at most 100 characters') ), email: v.pipe( v.string(), v.email('Invalid email address format') )});