npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pocketbase-zod-schema

v1.0.2

Published

PocketBase migration generator using Zod schemas for type-safe database migrations

Readme

pocketbase-zod-schema

Define your PocketBase collections using Zod schemas and automatically generate migration files.

Features

  • Type-safe schema definitions - Use Zod to define your PocketBase collections with full TypeScript support
  • Automatic migrations - Generate PocketBase-compatible migration files from your schema changes
  • Relation support - Easily define single and multiple relations between collections
  • View collections - Read-only collections backed by a SQL query, declared alongside the rest
  • Permission templates - Built-in templates for common permission patterns
  • Index definitions - Declare indexes alongside your schema
  • Execution engine - Current state is reconstructed by executing your migrations in a simulated PocketBase JSVM, so loops, helper functions and computed values are understood
  • Verification & linting - Round-trip up()/down() before writing a migration, and catch JavaScript that PocketBase's goja runtime cannot run
  • CLI & programmatic API - Use the CLI for quick generation or the API for custom workflows

Installation

npm install pocketbase-zod-schema
# or
yarn add pocketbase-zod-schema
# or
pnpm add pocketbase-zod-schema

Quick Start

1. Create a schema file

Create a schema file in your project (e.g., src/schema/post.ts):

import { z } from "zod";
import {
  defineCollection,
  TextField,
  EditorField,
  BoolField,
  RelationField,
  RelationsField,
} from "pocketbase-zod-schema";

// Define the Zod schema
export const PostSchema = z.object({
  title: TextField({ min: 1, max: 200 }),
  content: EditorField(),
  published: BoolField(),
  
  // Single relation to users collection
  author: RelationField({ collection: "users" }),
  
  // Multiple relations to tags collection
  tags: RelationsField({ collection: "tags", maxSelect: 10 }),
});

// Define the collection with permissions
export const PostCollection = defineCollection({
  collectionName: "posts",
  schema: PostSchema,
  permissions: {
    listRule: '@request.auth.id != ""',
    viewRule: "",
    createRule: '@request.auth.id != ""',
    updateRule: "author = @request.auth.id",
    deleteRule: "author = @request.auth.id",
  },
});

2. Configure the CLI

Create pocketbase-migrate.config.js at your project root:

export default {
  schema: {
    directory: "./src/schema",
    exclude: ["*.test.ts", "*.spec.ts"],
  },
  migrations: {
    directory: "./pocketbase/pb_migrations",
  },
};

3. Generate migrations

npx pocketbase-migrate generate

This will create a migration file in your PocketBase migrations directory.

TypeScript Support: You can use TypeScript (.ts) schema files directly - no compilation needed! The tool automatically handles TypeScript files using tsx.

4. Generate TypeScript types

Generate type-safe TypeScript definitions from your schemas:

# Generate types to pocketbase-types.ts
npx pocketbase-migrate generate-types

# Or specify a custom output path
npx pocketbase-migrate generate-types --output ./src/types/pocketbase.ts

This creates a pocketbase-types.ts file with:

  • Type-safe record interfaces for each collection (e.g., PostsRecord, UsersRecord)
  • Response types with expand support (e.g., PostsResponse, UsersResponse)
  • A TypedPocketBase interface for type-safe PocketBase client usage

Usage in your application:

import PocketBase from "pocketbase";
import { TypedPocketBase } from "./pocketbase-types";

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

// Full type safety with autocomplete!
const post = await pb.collection("posts").getOne("post-id");
// post is typed as PostsResponse with full autocomplete

// Expand relations with type safety
const postWithAuthor = await pb.collection("posts").getOne("post-id", {
  expand: "author"
});
// postWithAuthor.expand?.author is typed as UsersResponse

Schema Definition

High-Level Collection Definition

The recommended way to define collections is using defineCollection(), which provides a single entry point for collection name, schema, permissions, indexes, and future features:

import { z } from "zod";
import { defineCollection, TextField, EditorField, RelationField } from "pocketbase-zod-schema";

export const PostCollectionSchema = z.object({
  title: TextField({ min: 1, max: 200 }),
  content: EditorField(),
  author: RelationField({ collection: "users" }),
});

export const PostCollection = defineCollection({
  collectionName: "posts",
  schema: PostCollectionSchema,
  permissions: {
    template: "owner-only",
    ownerField: "author",
  },
  indexes: [
    "CREATE INDEX idx_posts_author ON posts (author)",
  ],
});

Benefits of defineCollection():

  • Explicit collection name - No need to rely on filename conventions
  • All metadata in one place - Schema, permissions, indexes together
  • Future-proof - Easy to extend with new features
  • Cleaner syntax - No nested function calls

Export Pattern: It's recommended to export both the schema and collection definition:

// Define the Zod schema (for type inference and validation)
export const PostSchema = z.object({
  title: z.string(),
  content: z.string(),
  author: RelationField({ collection: "users" }),
});

// Define the collection (used by migration generator, includes metadata)
export const PostCollection = defineCollection({
  collectionName: "posts",
  schema: PostSchema,
  permissions: { /* ... */ },
});

This pattern allows:

  • PostSchema - Used for type inference (z.infer<typeof PostSchema>) and validation
  • PostCollection - Used by the migration generator (has collection metadata)

Field Types

The library provides explicit field helper functions for all PocketBase field types. These helpers embed PocketBase-specific metadata and provide type-safe configuration options.

Field Helper Functions

| Field Helper | PocketBase Type | Description | Example | |--------------|-----------------|-------------|---------| | BoolField() | bool | Boolean field | active: BoolField() | | NumberField(options?) | number | Number field with optional constraints | price: NumberField({ min: 0 }) | | TextField(options?) | text | Text field with optional constraints | name: TextField({ min: 1, max: 200 }) | | EmailField() | email | Email field with validation | email: EmailField() | | URLField() | url | URL field with validation | website: URLField() | | EditorField() | editor | Rich text editor field | content: EditorField() | | DateField(options?) | date | Date field with optional constraints | birthdate: DateField() | | AutodateField(options?) | autodate | Auto-managed timestamp field | createdAt: AutodateField({ onCreate: true }) | | SelectField(values, options?) | select | Single or multiple select field | status: SelectField(["draft", "published"]) | | FileField(options?) | file | Single file upload field | avatar: FileField({ mimeTypes: ["image/*"] }) | | FilesField(options?) | file | Multiple file upload field | images: FilesField({ maxSelect: 5 }) | | JSONField(schema?, options?) | json | JSON field with optional schema and size cap | metadata: JSONField({ maxSize: "5M" }) | | GeoPointField() | geoPoint | Geographic coordinates field | location: GeoPointField() | | RelationField(config) | relation | Single relation field | author: RelationField({ collection: "users" }) | | RelationsField(config) | relation | Multiple relation field | tags: RelationsField({ collection: "tags" }) |

Field Options

BoolField()

  • No options
  • Returns: z.ZodBoolean

NumberField(options?)

  • min?: number - Minimum value constraint
  • max?: number - Maximum value constraint
  • noDecimal?: boolean - Disallow decimal values (integers only)
  • Returns: z.ZodNumber

TextField(options?)

  • min?: number - Minimum length constraint
  • max?: number - Maximum length constraint
  • pattern?: RegExp | string - Pattern constraint (regex)
  • autogeneratePattern?: string - Auto-generate pattern (e.g., "[A-Z]{3}-[0-9]{6}")
  • Returns: z.ZodString

EmailField()

  • No options (includes email validation)
  • Returns: z.ZodString

URLField()

  • No options (includes URL validation)
  • Returns: z.ZodString

EditorField()

  • No options
  • Returns: z.ZodString

DateField(options?)

  • min?: Date | string - Minimum date constraint
  • max?: Date | string - Maximum date constraint
  • Returns: z.ZodString

AutodateField(options?)

  • onCreate?: boolean - Set date automatically on record creation
  • onUpdate?: boolean - Update date automatically on record update
  • Returns: z.ZodString

SelectField(values, options?)

  • values: [string, ...string[]] - Array of allowed values (required)
  • maxSelect?: number - Maximum selections (default: 1, >1 enables multiple selection)
  • Returns: z.ZodEnum<T> or z.ZodArray<z.ZodEnum<T>>

FileField(options?)

  • mimeTypes?: string[] - Allowed MIME types (e.g., ["image/*", "application/pdf"])
  • maxSize?: ByteSize - Maximum file size: a number of bytes, or a string with a K/M/G suffix ("5M", "1G"; max "8G")
  • thumbs?: string[] - Thumbnail sizes to generate (e.g., ["100x100", "200x200"])
  • protected?: boolean - Whether file requires auth to access
  • Returns: z.ZodType<string, File | string> — accepts a File on input, parses to the stored filename string

FilesField(options?)

  • All FileField options plus:
  • minSelect?: number - Minimum number of files required
  • maxSelect?: number - Maximum number of files allowed
  • Returns: z.ZodType<string[], (File | string)[]>

JSONField(schema?, options?)

  • schema?: z.ZodTypeAny - Optional Zod schema for JSON structure validation
  • options?.maxSize?: number | "5M" - Maximum serialized size, in bytes or with a K/M/G suffix. PocketBase applies a 1MB default when unset, so larger payloads must set it. Either argument may be passed alone: JSONField({ maxSize: "5M" })
  • Returns: T | z.ZodRecord<z.ZodString, z.ZodAny>

GeoPointField()

  • No options
  • Returns: z.ZodObject<{ lon: z.ZodNumber; lat: z.ZodNumber }>

Field Helper Examples

import { z } from "zod";
import {
  defineCollection,
  BoolField,
  NumberField,
  TextField,
  EmailField,
  URLField,
  EditorField,
  DateField,
  AutodateField,
  SelectField,
  FileField,
  FilesField,
  JSONField,
  GeoPointField,
  RelationField,
  RelationsField,
} from "pocketbase-zod-schema";

const ProductSchema = z.object({
  // Text fields
  name: TextField({ min: 1, max: 200 }),
  sku: TextField({ autogeneratePattern: "[A-Z]{3}-[0-9]{6}" }),
  description: EditorField(),
  website: URLField().optional(),
  
  // Number fields
  price: NumberField({ min: 0 }),
  quantity: NumberField({ min: 0, noDecimal: true }),
  rating: NumberField({ min: 0, max: 5 }).optional(),
  
  // Boolean field
  active: BoolField(),
  featured: BoolField().optional(),
  
  // Date fields
  releaseDate: DateField().optional(),
  createdAt: AutodateField({ onCreate: true }),
  updatedAt: AutodateField({ onUpdate: true }),
  
  // Select fields
  status: SelectField(["draft", "published", "archived"]),
  categories: SelectField(["electronics", "clothing", "food"], { maxSelect: 3 }),
  
  // File fields
  thumbnail: FileField({ 
    mimeTypes: ["image/*"], 
    maxSize: 5242880, // 5MB
    thumbs: ["100x100", "200x200"],
  }),
  images: FilesField({ 
    mimeTypes: ["image/*"], 
    maxSelect: 5,
  }),
  
  // JSON field
  metadata: JSONField(),
  settings: JSONField(z.object({
    theme: z.string(),
    notifications: z.boolean(),
  })).optional(),
  
  // GeoPoint field
  location: GeoPointField().optional(),
  
  // Relation fields
  vendor: RelationField({ collection: "vendors" }),
  tags: RelationsField({ collection: "tags", maxSelect: 10 }),
});

export const ProductCollection = defineCollection({
  collectionName: "products",
  schema: ProductSchema,
  permissions: {
    listRule: "",
    viewRule: "",
    createRule: '@request.auth.id != ""',
    updateRule: "vendor.owner = @request.auth.id",
    deleteRule: "vendor.owner = @request.auth.id",
  },
});

Backward Compatibility

The library still supports plain Zod types for backward compatibility. The migration generator will infer PocketBase field types from Zod types when field helpers are not used:

| Zod Type | PocketBase Type | Example | |----------|-----------------|---------| | z.string() | text | title: z.string() | | z.string().email() | email | email: z.string().email() | | z.string().url() | url | website: z.string().url() | | z.number() | number | price: z.number() | | z.boolean() | bool | active: z.boolean() | | z.date() | date | birthdate: z.date() | | z.enum([...]) | select | status: z.enum(["draft", "published"]) | | z.instanceof(File) | file | avatar: z.instanceof(File) |

Recommendation: Use field helpers for new schemas to get explicit field type declarations and access to PocketBase-specific options.

Defining Relations

Use RelationField() for single relations and RelationsField() for multiple relations:

import { RelationField, RelationsField } from "pocketbase-zod-schema";

const ProjectSchema = z.object({
  name: z.string(),
  
  // Single relation (maxSelect: 1)
  owner: RelationField({ collection: "users" }),
  
  // Single relation with cascade delete
  category: RelationField({ 
    collection: "categories",
    cascadeDelete: true,
  }),
  
  // Multiple relations (maxSelect: 999 by default)
  collaborators: RelationsField({ collection: "users" }),
  
  // Multiple relations with constraints
  tags: RelationsField({ 
    collection: "tags",
    minSelect: 1,
    maxSelect: 5,
  }),
});

Relation Options

RelationField(config) - Single relation

  • collection: string - Target collection name (required)
  • cascadeDelete?: boolean - Delete related records when this record is deleted (default: false)
  • displayFields?: string[] | null - Fields to display in the PocketBase admin UI

RelationsField(config) - Multiple relations

  • All RelationField options plus:
  • minSelect?: number - Minimum number of relations required (default: 0)
  • maxSelect?: number - Maximum number of relations allowed (default: 999)

Relations are explicit-only. There is no naming-convention fallback — a field is a relation only when it's built with RelationField()/RelationsField(); a bare z.array(z.string()) maps to a json field, not a relation.

Defining View Collections

Use defineView() for a read-only collection backed by a SQL query. PocketBase derives the collection's fields by running the query, so the Zod schema describes the row shape for TypeScript only:

import { z } from "zod";
import { baseSchema, defineView, sql } from "pocketbase-zod-schema";

export const ProductStatsSchema = z
  .object({
    vendor: z.string(),
    productCount: z.number(),
  })
  .extend(baseSchema);

export default defineView({
  collectionName: "ProductStats",
  schema: ProductStatsSchema,
  viewQuery: sql`
    SELECT p.vendor AS id,
           p.vendor AS vendor,
           COUNT(*) AS productCount
      FROM products p
     GROUP BY p.vendor
  `,
  permissions: {
    listRule: "vendor.owner = @request.auth.id",
    viewRule: "vendor.owner = @request.auth.id",
  },
});
  • Views are read-only: defineView() accepts only listRule and viewRule, and rejects indexes.
  • The outermost SELECT must expose a unique id column.
  • The generated migration contains the query and no fields/indexes array.
  • Editing the SQL produces an in-place update that keeps the collection id stable; re-indenting it produces no migration at all.
  • Deleting a view is not a destructive change — a view stores no data.

defineCollection({ type: "view", viewQuery }) is equivalent, but defineView() turns those constraints into compile errors instead of apply-time failures.

Defining Permissions

Attach API rules to a collection through defineCollection({ permissions }):

import { defineCollection } from "pocketbase-zod-schema";

// Explicit rules
const PostCollection = defineCollection({
  collectionName: "posts",
  schema: z.object({ title: z.string() }),
  permissions: {
    listRule: '@request.auth.id != ""',     // Authenticated users can list
    viewRule: "",                            // Anyone can view (public)
    createRule: '@request.auth.id != ""',   // Authenticated users can create
    updateRule: "author = @request.auth.id", // Only author can update
    deleteRule: "author = @request.auth.id", // Only author can delete
  },
});

// Using a template
const ProjectCollection = defineCollection({
  collectionName: "projects",
  schema: z.object({ title: z.string(), owner: RelationField({ collection: "users" }) }),
  permissions: {
    template: "owner-only",
    ownerField: "owner",
  },
});

Permission Templates

| Template | Description | |----------|-------------| | "public" | All operations are public (no auth required) | | "authenticated" | All operations require authentication | | "owner-only" | Only the owner can perform operations (ownerField, default "User") | | "admin-only" | Requires @request.auth.<roleField> = "admin" (roleField, default "role") | | "read-public" | Public read, authenticated write | | "custom" | No base rules — customRules supplies everything |

Two more have no template name; call them and pass the result as permissions:

import { PermissionTemplates } from "pocketbase-zod-schema";

PermissionTemplates.locked();                 // every rule null (superusers only)
PermissionTemplates.readOnlyAuthenticated();  // authenticated read, writes locked

Rule values: null = superusers only, "" = public, any other string = a PocketBase filter expression. manageRule applies to auth collections only.

Template with Custom Overrides

const PostCollection = defineCollection({
  collectionName: "posts",
  schema: z.object({ title: z.string(), author: RelationField({ collection: "users" }) }),
  permissions: {
    template: "owner-only",
    ownerField: "author",
    customRules: {
      listRule: '@request.auth.id != ""',  // Override just the list rule
      viewRule: "",                         // Make viewing public
    },
  },
});

Defining Indexes

Declare database indexes through defineCollection({ indexes }):

import { defineCollection } from "pocketbase-zod-schema";

const UserCollection = defineCollection({
  collectionName: "users",
  schema: z.object({
    email: z.string().email(),
    username: z.string(),
  }),
  permissions: {
    template: "authenticated",
  },
  indexes: [
    'CREATE UNIQUE INDEX idx_users_email ON users (email)',
    'CREATE INDEX idx_users_username ON users (username)',
  ],
});

CLI Reference

Commands

# Generate migration from schema changes
npx pocketbase-migrate generate

# Show what would be generated without writing files
npx pocketbase-migrate status

# Compare the files on disk against PocketBase's _migrations table
npx pocketbase-migrate status --verify

# Check migrations against PocketBase's goja runtime (Node-only globals, async, ...)
npx pocketbase-migrate lint

# Generate TypeScript definitions from schemas
npx pocketbase-migrate generate-types

# Force generation even with destructive changes
npx pocketbase-migrate generate --force

# Round-trip up() and down() before writing; refuse a migration that doesn't roll back
npx pocketbase-migrate generate --verify

# Restrict the diff to matching collection or field names (regex supported)
npx pocketbase-migrate generate Posts Comments

generate Command

pocketbase-migrate generate [filters...] [options]

Options:
  -o, --output <directory>  Output directory for migration files
  -f, --force               Force generation even with destructive changes or duplicates
  --dry-run                 Show what would be generated without writing files
  --schema-dir <directory>  Directory containing Zod schema files
  --verify                  Execute up() and down() before writing
  --no-verify               Skip verification even when enabled in the config file

One file is written per collection operation.

status Command

pocketbase-migrate status [options]

Options:
  --schema-dir <directory>  Directory containing Zod schema files
  --json                    Output status as JSON
  --verify                  Compare disk against PocketBase's _migrations table; exit non-zero on drift
  --pb-data <path>          PocketBase data directory or data.db file

--verify needs Node >= 22.5 (node:sqlite).

lint Command

pocketbase-migrate lint [files...] [options]

Options:
  -o, --output <directory>  Directory containing migration files
  --no-execute              Static checks only

Catches JavaScript that runs in Node but not in goja: require, process, fetch, setTimeout, async/await, import/export, class fields. Exits non-zero on any error-severity finding.

Global Options

  -c, --config <path>   Configuration file path
  -v, --version         Print the version   ← -v is version, not verbose
      --verbose         Enable verbose logging
      --quiet           Suppress non-essential output
      --no-color        Disable colored output

generate-types Command

Generate type-safe TypeScript definitions from your Zod schemas.

pocketbase-migrate generate-types [options]

Options:
  -o, --output <path>        Output file path (default: pocketbase-types.ts)
  --schema-dir <directory>   Directory containing Zod schema files

What it generates:

The generate-types command analyzes your Zod schemas and generates a TypeScript file (default: pocketbase-types.ts) containing:

  1. Record interfaces - Type-safe interfaces for each collection's data structure

    export interface PostsRecord {
      id: string;
      created: string;
      updated: string;
      collectionId: string;
      collectionName: "posts";
      title: string;
      content: string;
      // ... other fields
    }
  2. Response types - Extended types with expand support for relations

    export interface PostsResponse extends PostsRecord {
      expand?: {
        author?: UsersResponse;
        tags?: TagsResponse[];
      };
    }
  3. TypedPocketBase interface - Type-safe PocketBase client wrapper

    export interface TypedPocketBase extends PocketBase {
      collection(idOrName: "posts"): RecordService<PostsResponse>;
      collection(idOrName: "users"): RecordService<UsersResponse>;
      // ... other collections
    }

Example usage:

import PocketBase from "pocketbase";
import { TypedPocketBase } from "./pocketbase-types";

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

// Full type safety!
const post = await pb.collection("posts").getOne("post-id");
// Type: PostsResponse

// Expand relations with type safety
const postWithAuthor = await pb.collection("posts").getOne("post-id", {
  expand: "author"
});
// postWithAuthor.expand?.author is typed as UsersResponse | undefined

Configuration Options

// pocketbase-migrate.config.js
export default {
  schema: {
    // Directory containing your Zod schema files
    directory: "./src/schema",

    // Files to exclude from schema discovery (replaces the default list)
    exclude: ["*.test.ts", "*.spec.ts", "base.ts", "index.ts"],
  },
  migrations: {
    // Directory to output migration files, and to reconstruct state from
    directory: "./pocketbase/pb_migrations",

    // Round-trip up()/down() before writing a migration (default: false)
    verify: false,

    // PocketBase data directory, for reading the _migrations table.
    // "" means the pb_data directory next to the migrations directory.
    dataDirectory: "",
  },
  diff: {
    // Warn when collections or fields would be deleted
    warnOnDelete: true,

    // Require --force flag for destructive changes
    requireForceForDestructive: true,
  },
  typeGen: {
    // Default output for `generate-types`
    outPath: "pocketbase-types.ts",
  },
};

There is no snapshot file to configure. The current database state is reconstructed by executing the migration files: the newest *_collections_snapshot.js plus everything after it.

Environment overrides: MIGRATION_SCHEMA_DIR, MIGRATION_SCHEMA_EXCLUDE, MIGRATION_OUTPUT_DIR, MIGRATION_VERIFY, MIGRATION_DATA_DIR, MIGRATION_REQUIRE_FORCE. Precedence is CLI > environment > config file > defaults.


Programmatic API

For custom workflows, use the programmatic API from pocketbase-zod-schema/server:

import {
  parseSchemaFiles,
  compare,
  generate,
  loadSnapshotWithMigrations,
} from "pocketbase-zod-schema/server";

async function generateMigration() {
  const schemaDir = "./src/schema";
  const migrationsDir = "./pocketbase/pb_migrations";

  // Parse all schema files
  const currentSchema = await parseSchemaFiles({ schemaDir });

  // Reconstruct the current database state by executing the existing migrations.
  // Returns null when there is nothing to replay; throws SnapshotError if a
  // migration cannot be executed.
  const previousSnapshot = loadSnapshotWithMigrations({
    migrationsPath: migrationsDir,
  });

  // Compare schemas and detect changes
  const diff = compare(currentSchema, previousSnapshot);

  // Write one migration file per collection operation; returns the paths written
  const paths = generate(diff, migrationsDir);

  console.log(paths.length ? `Migrations created:\n${paths.join("\n")}` : "No changes detected");
}

The execution engine, round-trip verification and the goja lint are also available directly from pocketbase-zod-schema/server — see the Execution Engine guide.


Complete Example

Here's a complete example of a blog schema with users, posts, and comments:

// src/schema/user.ts
import { z } from "zod";
import { baseSchema, defineCollection, TextField, EmailField } from "pocketbase-zod-schema";

// Input schema for forms (includes passwordConfirm for validation)
export const UserInputSchema = z.object({
  name: TextField({ max: 100 }).optional(),
  email: EmailField(),
  password: TextField({ min: 8 }),
  passwordConfirm: z.string(),
  avatar: z.instanceof(File).optional(),
});

// Database schema (excludes passwordConfirm)
const UserCollectionSchema = z.object({
  name: TextField({ max: 100 }).optional(),
  email: EmailField(),
  password: TextField({ min: 8 }),
  avatar: z.instanceof(File).optional(),
});

// Full schema with base fields for type inference (includes id, collectionId, etc.)
export const UserSchema = UserCollectionSchema.extend(baseSchema);

// Collection definition with permissions and indexes
export const UserCollection = defineCollection({
  collectionName: "Users",
  type: "auth", // auth collections must opt in explicitly; it is never inferred
  schema: UserSchema,
  permissions: {
    listRule: "id = @request.auth.id",
    viewRule: "id = @request.auth.id",
    createRule: "",
    updateRule: "id = @request.auth.id",
    deleteRule: "id = @request.auth.id",
  },
  indexes: [
    'CREATE UNIQUE INDEX idx_users_email ON users (email)',
  ],
});
// src/schema/post.ts
import { z } from "zod";
import { 
  defineCollection,
  TextField,
  EditorField,
  BoolField,
  DateField,
  RelationField, 
  RelationsField, 
} from "pocketbase-zod-schema";

// Define the Zod schema
export const PostSchema = z.object({
  title: TextField({ min: 1, max: 200 }),
  slug: TextField({ pattern: /^[a-z0-9-]+$/ }),
  content: EditorField(),
  excerpt: TextField({ max: 500 }).optional(),
  published: BoolField(),
  publishedAt: DateField().optional(),
  
  // Relations
  author: RelationField({ collection: "users" }),
  category: RelationField({ collection: "categories" }),
  tags: RelationsField({ collection: "tags", maxSelect: 10 }),
});

// Define the collection with permissions
export const PostCollection = defineCollection({
  collectionName: "posts",
  schema: PostSchema,
  permissions: {
    listRule: 'published = true || author = @request.auth.id',
    viewRule: 'published = true || author = @request.auth.id',
    createRule: '@request.auth.id != ""',
    updateRule: "author = @request.auth.id",
    deleteRule: "author = @request.auth.id",
  },
});
// src/schema/comment.ts
import { z } from "zod";
import { defineCollection, TextField, RelationField } from "pocketbase-zod-schema";

// Define the Zod schema
export const CommentSchema = z.object({
  content: TextField({ min: 1 }),
  
  // Relations with cascade delete
  post: RelationField({ collection: "posts", cascadeDelete: true }),
  author: RelationField({ collection: "users" }),
});

// Define the collection with permissions
export const CommentCollection = defineCollection({
  collectionName: "comments",
  schema: CommentSchema,
  permissions: {
    listRule: "",
    viewRule: "",
    createRule: '@request.auth.id != ""',
    updateRule: "author = @request.auth.id",
    deleteRule: "author = @request.auth.id || @request.auth.role = 'admin'",
  },
});

Documentation

Full guides live in the repository:

Requirements

Node.js 20 or higher. status --verify, which reads PocketBase's _migrations table, additionally requires Node 22.5+ for node:sqlite.

License

MIT