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 🙏

© 2025 – Pkg Stats / Ryan Hefner

pocketbase-zod-schema

v0.2.3

Published

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

Downloads

739

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
  • Permission templates - Built-in templates for common permission patterns
  • Index definitions - Declare indexes alongside your schema
  • 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,
  RelationField,
  RelationsField,
} from "pocketbase-zod-schema/schema";

// Define the Zod schema
export const PostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string(),
  published: z.boolean().default(false),
  
  // 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.


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, RelationField } from "pocketbase-zod-schema/schema";

export const PostCollectionSchema = z.object({
  title: z.string(),
  content: z.string(),
  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)

Note: You can still use withPermissions() and withIndexes() separately if you prefer, but defineCollection() is recommended for new code.

Field Types

The library maps Zod types to PocketBase field types automatically:

| 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) | | RelationField() | relation | author: RelationField({ collection: "users" }) | | RelationsField() | relation | tags: RelationsField({ collection: "tags" }) |

Defining Relations

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

import { RelationField, RelationsField } from "pocketbase-zod-schema/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)

RelationsField(config) - Multiple relations

  • collection: string - Target collection name (required)
  • cascadeDelete?: boolean - Delete related records when this record is deleted (default: false)
  • minSelect?: number - Minimum number of relations required (default: 0)
  • maxSelect?: number - Maximum number of relations allowed (default: 999)

Defining Permissions

Use defineCollection() with permissions, or withPermissions() to attach API rules to your schema:

import { defineCollection, withPermissions } from "pocketbase-zod-schema/schema";

// Using defineCollection (recommended)
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 templates with defineCollection
const ProjectCollection = defineCollection({
  collectionName: "projects",
  schema: z.object({ title: z.string(), owner: RelationField({ collection: "users" }) }),
  permissions: {
    template: "owner-only",
    ownerField: "owner",
  },
});

// Using withPermissions (alternative approach)
const PostSchemaAlt = withPermissions(
  z.object({ title: z.string() }),
  {
    listRule: '@request.auth.id != ""',
    viewRule: "",
    createRule: '@request.auth.id != ""',
    updateRule: "author = @request.auth.id",
    deleteRule: "author = @request.auth.id",
  }
);

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 |

Template with Custom Overrides

// Using defineCollection (recommended)
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
    },
  },
});

// Using withPermissions (alternative)
const PostSchemaAlt = withPermissions(schema, {
  template: "owner-only",
  ownerField: "author",
  customRules: {
    listRule: '@request.auth.id != ""',
    viewRule: "",
  },
});

Defining Indexes

Use defineCollection() with indexes, or withIndexes() to define database indexes:

import { defineCollection, withIndexes, withPermissions } from "pocketbase-zod-schema/schema";

// Using defineCollection (recommended)
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)',
  ],
});

// Using withIndexes (alternative)
const UserSchemaAlt = withIndexes(
  withPermissions(
    z.object({
      email: z.string().email(),
      username: z.string(),
    }),
    { template: "authenticated" }
  ),
  [
    '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

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

Configuration Options

// pocketbase-migrate.config.js
export default {
  schema: {
    // Directory containing your Zod schema files
    directory: "./src/schema",
    
    // Files to exclude from schema discovery
    exclude: ["*.test.ts", "*.spec.ts", "base.ts", "index.ts"],
  },
  migrations: {
    // Directory to output migration files
    directory: "./pocketbase/pb_migrations",
  },
  diff: {
    // Warn when collections or fields would be deleted
    warnOnDelete: true,
    
    // Require --force flag for destructive changes
    requireForceForDestructive: true,
  },
};

Programmatic API

For custom workflows, use the programmatic API:

import {
  parseSchemaFiles,
  compare,
  generate,
  loadSnapshotIfExists,
} from "pocketbase-zod-schema/migration";

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

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

  // Load the last known state from existing migrations
  const previousSnapshot = loadSnapshotIfExists({ 
    migrationsPath: migrationsDir 
  });

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

  // Generate migration file
  if (diff.collectionsToCreate.length > 0 || 
      diff.collectionsToModify.length > 0 || 
      diff.collectionsToDelete.length > 0) {
    const migrationPath = generate(diff, migrationsDir);
    console.log(`Migration created: ${migrationPath}`);
  } else {
    console.log("No changes detected");
  }
}

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 } from "pocketbase-zod-schema/schema";

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

// Database schema (excludes passwordConfirm)
const UserCollectionSchema = z.object({
  name: z.string().optional(),
  email: z.string().email(),
  password: z.string().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",
  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,
  RelationField, 
  RelationsField, 
} from "pocketbase-zod-schema/schema";

// Define the Zod schema
export const PostSchema = z.object({
  title: z.string().min(1).max(200),
  slug: z.string(),
  content: z.string(),
  excerpt: z.string().optional(),
  published: z.boolean().default(false),
  publishedAt: z.date().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, RelationField } from "pocketbase-zod-schema/schema";

// Define the Zod schema
export const CommentSchema = z.object({
  content: z.string().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'",
  },
});

License

MIT