Back to projects
https://github.com/zenaxo/valibase

Valibase

Hannes Sjölander Feb 17, 2026

A Valibot Schema Generator For PocketBase

3 minutes read Valibase

Project Overview

Valibase is a lightweight Go package that connects to a PocketBase instance a automatically generates fully-typed Valibot schemas based on your database collections.

These schemas can then be used directly in “frontend” frameworks like SvelteKit to provide:

  • Runtime form validation

  • API payload validation

  • Strong TypeScript inference

  • Single-source-of-truth data models

The goal was simple: remove repetitive backend schema definitions and make database-driven apps safer and faster to build.

The Problem

PocketBase already provides a powerful admin UI and schema system, which makes spinning up a backend incredibly fast. However, when building production-ready frontends, I consistently ran into the same friction points:

  • Re-implementing validation logic in SvelteKit

  • Keeping frontend schemas in sync with PocketBase collections

  • Manually updating types after database changes

  • Writing repetitive form validation rules

While tools like pocketbase-typegen solve the TypeScript typing problem, they stop short of runtime validation. For user input, API routes, and form submission, static types alone aren’t enough, you still need robust schemas that can actually validate data at runtime.

That gap is what Valibase aims to fill.

The Solution

Valibase introspects a PocketBase instance and produces:

  • Valibot object schemas for each collection

  • Field-level validators based on PocketBase rules

  • Required/optional flags

  • Type-safe outputs ready for frontend use

For example, the generated input schema for the users collection could look like below:

export const userInput = v.object({
  avatar: v.optional(
    v.pipe(
      fileSchema,
      v.mimeType(
        ["image/jpeg", "image/png", "image/svg+xml", "image/gif", "image/webp"],
        "Please select one of the following file types: JPEG or PNG or SVG+XML or GIF or WEBP",
      ),
    ),
  ),
  name: v.optional(
    v.pipe(
      v.string(),
      v.maxLength(255, "Input must be at most 255 characters"),
    ),
  ),
  username: v.pipe(
    v.string(),
    v.minLength(3, "Input must be at least 3 characters"),
    v.maxLength(80, "Input must be at most 80 characters"),
    v.regex(/^[\w][\w\.\-]*$/, "Invalid format"),
  ),
  tasks: v.optional(
    v.pipe(v.array(v.string()), v.brand("RelationMultiple")),
    [],
  ),
  languages: v.optional(stringEnum("Swedish", "English", "Spanish")),
  address: v.optional(geoPointSchema),
  email: emailSchema,
  emailVisibility: v.optional(v.boolean()),
  verified: v.optional(v.boolean()),
});

This example provides you with min/max length, regex/patterns, picklists and so on.

For retrieving users, the schema excludes the specific validations because usually, the database would get updated at some point. name instead becomes:

export const userResponse = v.object({
  ...
  // PocketBase returns undefined fields as "" so we have this helper schema
  name: optionalTextResponse(v.string()),
  ... 
});

Further, when querying the database, you get type inference for free:

import PocketBase from 'pocketbase';
import { Collections, type TypedPocketBase } from '$lib/database/database.ts';

const pb = new PocketBase('http://localhost:8090') as TypedPocketBase;

// returns User[]
const users = await pb.collection(Collections.Users).getFullList()

Because Valibot is both lightweight and strongly typed, the generated schemas integrate cleanly into modern TypeScript apps without adding unnecessary overhead.

Challenges & Design Decisions

One of the main challenges was mapping PocketBase’s schema system to Valibot’s API in a way that stayed flexible but predictable.

PocketBase supports multiple field types and constraints, each of which needs to be translated into the appropriate Valibot validators. The generator also has to product code that is:

  • Human-readble

  • Easy to extend

  • Safe to commit to version control

  • Deterministic between runs

I focused on keeping the output minimal and idiomatic rather than auto-generating overly complex logic.

One design decision that was made was to exclude validation of expanded fields.

For example, if the users collection has a relation tasks which is a reference to the collection todos, the PocketBase response would look like this:

{
  name: 'Hannes Sjölander',
  tasks: ['id_1', 'id_2'],
  expand: {
    tasks: [
      {
        id:   'id_1',
        name: 'Clean the kitchen',
        done: true 
      },
      {
        id:   'id_2',
        name: 'Write this case study',
        done: false 
      }
    ]
  }
}

In this example, the expand field would not be validated by Valibot, only the outer tasks entry. Validating the relation fields would either cause circular dependencies in more complex designs of alternatively, increase the file size by reconstructing each expanded type.

All though, you still get full IntelliSense for the TypeScript types:

(alias) type User = {
    name: string | undefined;
    avatar: (string & Brand<"FileName">) | undefined;
    id: string & Brand<"RecordId">;
    ...
    expand: {
      tasks: Todo[] | undefined  
    } | undefined
}
import User

Usage

To use Valibot, please refer to the README.md