Skip to content

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.


Fails if the value is null, undefined, or ''.

rules.required()
rules.required('Ce champ est obligatoire')

Standard email format validation.

Must be a valid http:// or https:// URL.

Must be a valid UUID (v1–v5).

E.164-compatible phone number (10–15 digits, optional + prefix, ignores spaces/dashes/parentheses).

Must be a valid IPv4 or IPv6 address.

Must be a valid hex color: #RGB, #RRGGBB, or #RRGGBBAA.

Luhn-validated credit card number.

Must be a valid JSON string.

rules.regex(/^\+?[0-9]{10,14}$/, 'Invalid phone format')

Only letters and digits.

Only letters and spaces.


rules.startsWith('https://', 'Must be a secure URL')
rules.endsWith('.pdf', 'Only PDF files allowed')

  • 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')
  • For strings: maximum character length
  • For numbers: maximum value

Number must be >= min and <= max.

rules.between(1, 100, 'Score must be between 1 and 100')

Must be a whole number (Number.isInteger).

Must be > 0.

Must be < 0.


Must be a parseable date string.

Date must be in the future.

Date must be in the past.


Value must be exactly true.

Value must be exactly false.


Array must have at least n items.

Array must have at most n items.

Array must not contain duplicate values.


Value must be one of the allowed values.

rules.enum(['draft', 'published', 'archived'], 'Invalid status')

Value must strictly equal (===) the expected value.


Run any custom validation function.

rules.custom(
(value) => value.startsWith('PB-'),
'Reference must start with PB-'
)

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.

Assert that fieldA < fieldB (numbers).

crossRules.lessThan('min_price', 'max_price')

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')

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);
}

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 }
// }
// }