Validation Rules
Validation rules run client-side, inside the SDK, before the request is sent to pulsabase-core. They are a developer-experience feature — they let you catch obvious mistakes early (bad email format, missing required field, etc.) without a round-trip to the server.
import { rules, crossRules, ModelSchema } from 'pulsabase';
static schema: ModelSchema = { tableName: 'users', validation: { email: [rules.required(), rules.email()], name: [rules.required(), rules.min(3, 'Name too short'), rules.max(100)], age: [rules.integer(), rules.positive()], }, crossValidation: [ crossRules.dateAfter('end_date', 'start_date', 'End must be after start'), crossRules.matches('password', 'password_confirm'), ],};All rules accept an optional custom error message as the last argument.
rules.* — Per-Field Rules
Section titled “rules.* — Per-Field Rules”Presence
Section titled “Presence”rules.required(msg?)
Section titled “rules.required(msg?)”Fails if the value is null, undefined, or ''.
rules.required()rules.required('Ce champ est obligatoire')String Format
Section titled “String Format”rules.email(msg?)
Section titled “rules.email(msg?)”Standard email format validation.
rules.url(msg?)
Section titled “rules.url(msg?)”Must be a valid http:// or https:// URL.
rules.uuid(msg?)
Section titled “rules.uuid(msg?)”Must be a valid UUID (v1–v5).
rules.phone(msg?)
Section titled “rules.phone(msg?)”E.164-compatible phone number (10–15 digits, optional + prefix, ignores spaces/dashes/parentheses).
rules.ip(msg?)
Section titled “rules.ip(msg?)”Must be a valid IPv4 or IPv6 address.
rules.hexColor(msg?)
Section titled “rules.hexColor(msg?)”Must be a valid hex color: #RGB, #RRGGBB, or #RRGGBBAA.
rules.creditCard(msg?)
Section titled “rules.creditCard(msg?)”Luhn-validated credit card number.
rules.json(msg?)
Section titled “rules.json(msg?)”Must be a valid JSON string.
rules.regex(pattern, msg?)
Section titled “rules.regex(pattern, msg?)”rules.regex(/^\+?[0-9]{10,14}$/, 'Invalid phone format')rules.alphanumeric(msg?)
Section titled “rules.alphanumeric(msg?)”Only letters and digits.
rules.string(msg?)
Section titled “rules.string(msg?)”Only letters and spaces.
String Content
Section titled “String Content”rules.startsWith(prefix, msg?)
Section titled “rules.startsWith(prefix, msg?)”rules.startsWith('https://', 'Must be a secure URL')rules.endsWith(suffix, msg?)
Section titled “rules.endsWith(suffix, msg?)”rules.endsWith('.pdf', 'Only PDF files allowed')rules.contains(substring, msg?)
Section titled “rules.contains(substring, msg?)”Size / Range
Section titled “Size / Range”rules.min(n, msg?)
Section titled “rules.min(n, msg?)”- For strings: minimum character length
- For numbers: minimum value
rules.min(8, 'Password must be at least 8 characters')rules.min(0, 'Must be non-negative')rules.max(n, msg?)
Section titled “rules.max(n, msg?)”- For strings: maximum character length
- For numbers: maximum value
rules.between(min, max, msg?)
Section titled “rules.between(min, max, msg?)”Number must be >= min and <= max.
rules.between(1, 100, 'Score must be between 1 and 100')Number
Section titled “Number”rules.integer(msg?)
Section titled “rules.integer(msg?)”Must be a whole number (Number.isInteger).
rules.positive(msg?)
Section titled “rules.positive(msg?)”Must be > 0.
rules.negative(msg?)
Section titled “rules.negative(msg?)”Must be < 0.
rules.date(msg?)
Section titled “rules.date(msg?)”Must be a parseable date string.
rules.futureDate(msg?)
Section titled “rules.futureDate(msg?)”Date must be in the future.
rules.pastDate(msg?)
Section titled “rules.pastDate(msg?)”Date must be in the past.
Boolean
Section titled “Boolean”rules.isTrue(msg?)
Section titled “rules.isTrue(msg?)”Value must be exactly true.
rules.isFalse(msg?)
Section titled “rules.isFalse(msg?)”Value must be exactly false.
rules.minItems(n, msg?)
Section titled “rules.minItems(n, msg?)”Array must have at least n items.
rules.maxItems(n, msg?)
Section titled “rules.maxItems(n, msg?)”Array must have at most n items.
rules.uniqueItems(msg?)
Section titled “rules.uniqueItems(msg?)”Array must not contain duplicate values.
rules.enum(values, msg?)
Section titled “rules.enum(values, msg?)”Value must be one of the allowed values.
rules.enum(['draft', 'published', 'archived'], 'Invalid status')Equality
Section titled “Equality”rules.equals(expected, msg?)
Section titled “rules.equals(expected, msg?)”Value must strictly equal (===) the expected value.
Custom
Section titled “Custom”rules.custom(fn, message)
Section titled “rules.custom(fn, message)”Run any custom validation function.
rules.custom( (value) => value.startsWith('PB-'), 'Reference must start with PB-')crossRules.* — Cross-Field Rules
Section titled “crossRules.* — Cross-Field Rules”Cross-field rules receive the entire payload object and validate relationships between fields.
crossRules.dateAfter(fieldA, fieldB, msg?)
Section titled “crossRules.dateAfter(fieldA, fieldB, msg?)”Assert that fieldA is after fieldB (both are date strings).
crossRules.dateAfter('end_date', 'start_date', 'End date must be after start date')crossRules.dateBefore(fieldA, fieldB, msg?)
Section titled “crossRules.dateBefore(fieldA, fieldB, msg?)”Assert that fieldA is before fieldB.
crossRules.lessThan(fieldA, fieldB, msg?)
Section titled “crossRules.lessThan(fieldA, fieldB, msg?)”Assert that fieldA < fieldB (numbers).
crossRules.lessThan('min_price', 'max_price')crossRules.matches(fieldA, fieldB, msg?)
Section titled “crossRules.matches(fieldA, fieldB, msg?)”Assert that both fields have the same value. Common use: password confirmation.
crossRules.matches('password', 'password_confirm', 'Passwords do not match')crossRules.requiredIf(field, conditionField, conditionValue, msg?)
Section titled “crossRules.requiredIf(field, conditionField, conditionValue, msg?)”Make field required when conditionField === conditionValue.
crossRules.requiredIf('vat_number', 'is_company', true, 'VAT number required for companies')Error Shape
Section titled “Error Shape”When validation fails, the SDK does not send any request to the server. It throws a PulsabaseValidationError immediately:
try { await pb.from(User).insert({ email: 'invalid' }); // Never reaches the server} catch (err) { if (err instanceof PulsabaseValidationError) { err.errors.forEach(({ field, message }) => { console.log(`${field}: ${message}`); // e.g. 'email: Must be a valid email address' }); }}Or use .safeInsert() / .safeUpdate() for a Go-style result instead of throwing:
const result = await pb.from(User).safeInsert({ email: 'invalid' });if (!result.ok) { // No request was sent. Handle errors in the UI: result.errors.forEach(({ field, message }) => setFieldError(field, message));} else { // Request was sent and succeeded console.log(result.data);}Form Metadata Introspection
Section titled “Form Metadata Introspection”getFormMeta() reads _meta tags from validation rules to generate dynamic form schemas:
const meta = pb.from(User).getFormMeta();// {// fields: {// email: { required: true, format: 'email' },// name: { min: 3, max: 100 },// age: { type: 'integer' },// password: { min: 8 }// }// }