A dynamic PocketBase CRUD system with full schema introspection, form generation, and reusable UI components for rapid admin interface development. Works with Svelte and React.
npm install pocketcrud pocketbase svelte
# or
bun add pocketcrud pocketbase svelte
npm install pocketcrud pocketbase react react-dom
# or
bun add pocketcrud pocketbase react react-dom
Note: Framework dependencies (Svelte or React) are peer dependencies - install only what you need.
import PocketCrud from 'pocketcrud';
// Initialize with your PocketBase URL
const crud = new PocketCrud({
url: 'http://127.0.0.1:8090',
});
// Get all collections and their schemas
const collections = await crud.getCollections();
console.log(collections);
// Get schema for a specific collection
const userSchema = await crud.getCollectionSchema('users');
console.log(userSchema);
// Perform CRUD operations
const newUser = await crud.create('users', {
email: '[email protected]',
name: 'John Doe',
});
const users = await crud.getList('users', {
filter: 'verified = true',
sort: '-created',
});
await crud.update('users', newUser.id, {
name: 'John Smith',
});
await crud.delete('users', newUser.id);
// Get collection with full schema information
const collection = await crud.getCollection('posts');
console.log(collection.schema);
// [
// {
// name: 'title',
// type: 'text',
// required: true,
// presentable: true,
// options: { max: 200 }
// },
// {
// name: 'category',
// type: 'select',
// required: true,
// options: { values: ['tech', 'design', 'business'] }
// }
// // ... more fields
// ]
import { getFormFields, validateFormData, prepareFormData } from 'pocketcrud';
// Get schema and generate form configuration
const schema = await crud.getCollectionSchema('posts');
const formFields = getFormFields(schema);
// formFields contains everything needed to render a form:
// [
// {
// name: 'title',
// type: 'text',
// required: true,
// label: 'Title',
// placeholder: 'Enter title...'
// },
// {
// name: 'category',
// type: 'select',
// required: true,
// label: 'Category',
// options: ['tech', 'design', 'business']
// }
// ]
// Validate form data
const formData = { title: '', category: 'tech' };
const errors = validateFormData(formData, schema);
// ['title is required']
// Prepare data for submission
const preparedData = prepareFormData(formData, schema);
// Handles type conversion, JSON parsing, etc.
PocketCrud provides complete component libraries for both Svelte and React with identical functionality.
Framework | Import Path | Use Cases |
---|---|---|
Svelte | pocketcrud/svelte |
SvelteKit, Svelte apps |
React | pocketcrud/react |
Next.js, Create React App, any React project |
Utilities | pocketcrud |
Framework-agnostic CRUD utilities |
All five core components are available in both libraries:
Component | Purpose | Features |
---|---|---|
LoginForm | User authentication | Email/password, loading states, error handling |
SetupForm | Admin creation | Password confirmation, validation, success states |
DynamicForm | Schema-driven forms | Auto-generated fields, validation, all field types |
RecordList | Display records | Pagination, responsive table/cards, actions |
CollectionManager | Full CRUD interface | Combines form + list, handles all operations |
Shared Features:
// Import all Svelte components
import { LoginForm, SetupForm, CollectionManager, RecordList, DynamicForm } from 'pocketcrud/svelte';
// Or import specific groups
import { LoginForm, SetupForm } from 'pocketcrud/svelte/auth';
import { CollectionManager } from 'pocketcrud/svelte/collections';
import { RecordList, DynamicForm } from 'pocketcrud/svelte/records';
<script>
import { LoginForm } from 'pocketcrud/svelte';
import { goto } from '$app/navigation';
import PocketCrud from 'pocketcrud';
const crud = new PocketCrud({ url: 'https://your-pb-url.com' });
let email = '';
let password = '';
let isLoading = false;
let error = '';
async function handleLogin(event) {
const { email, password } = event.detail;
isLoading = true;
error = '';
try {
await crud.client.collection('users').authWithPassword(email, password);
goto('/admin');
} catch (err) {
error = err.message;
} finally {
isLoading = false;
}
}
</script>
<LoginForm bind:email bind:password bind:isLoading bind:error on:submit="{handleLogin}" />
<script>
import { SetupForm } from 'pocketcrud/svelte';
import { goto } from '$app/navigation';
import PocketCrud from 'pocketcrud';
const crud = new PocketCrud({ url: 'https://your-pb-url.com' });
let email = '';
let password = '';
let passwordConfirm = '';
let isLoading = false;
let error = '';
let success = '';
async function handleSetup(event) {
const { email, password, passwordConfirm } = event.detail;
if (password !== passwordConfirm) {
error = 'Passwords do not match';
return;
}
if (password.length < 10) {
error = 'Password must be at least 10 characters';
return;
}
isLoading = true;
error = '';
success = '';
try {
await crud.createAdmin(email, password);
success = 'Admin user created successfully!';
setTimeout(() => goto('/admin/login'), 2000);
} catch (err) {
error = err.message;
} finally {
isLoading = false;
}
}
</script>
<SetupForm
bind:email
bind:password
bind:passwordConfirm
bind:isLoading
bind:error
bind:success
on:submit="{handleSetup}"
/>
<script>
import { CollectionManager } from 'pocketcrud/svelte';
import PocketCrud from 'pocketcrud';
export let collectionName;
const crud = new PocketCrud({ url: 'https://your-pb-url.com' });
// Optional: Configure field overrides for specific collections
const fieldOverrides = {
body: { type: 'textarea', rows: 8 },
content: { type: 'textarea', rows: 6 },
};
// Optional: Specify primary display field
const primaryDisplayField = 'title';
</script>
<CollectionManager {crud} {collectionName} {fieldOverrides} {primaryDisplayField} perPage="{20}" />
<script>
import { RecordList } from 'pocketcrud/svelte';
export let records;
export let schema;
let currentPage = 1;
let totalPages = 10;
let totalItems = 100;
let perPage = 20;
function handleEdit(event) {
const record = event.detail;
console.log('Edit record:', record);
}
function handleDelete(event) {
const record = event.detail;
if (confirm('Delete this record?')) {
console.log('Delete record:', record);
}
}
function handlePageChange(event) {
const page = event.detail;
currentPage = page;
// Fetch new page of records
}
</script>
<RecordList
{records}
{schema}
{currentPage}
{totalPages}
{totalItems}
{perPage}
primaryDisplayField="title"
on:edit="{handleEdit}"
on:delete="{handleDelete}"
on:pageChange="{handlePageChange}"
/>
<script>
import { DynamicForm } from 'pocketcrud/svelte';
export let schema;
export let initialData = null;
const fieldOverrides = {
body: { type: 'textarea', rows: 8 },
};
function handleSubmit(event) {
const formData = event.detail;
console.log('Form submitted:', formData);
// Save to PocketBase
}
function handleCancel() {
console.log('Form cancelled');
}
</script>
<DynamicForm
{schema}
{initialData}
{fieldOverrides}
on:submit="{handleSubmit}"
on:cancel="{handleCancel}"
/>
All components support slots for customization:
<script>
import { LoginForm } from 'pocketcrud/svelte';
</script>
<LoginForm on:submit="{handleLogin}">
<div slot="email-input">
<!-- Custom email input using your UI library -->
<input type="email" bind:value="{email}" label="Email" />
</div>
<div slot="password-input">
<!-- Custom password input -->
<input type="password" bind:value="{password}" label="Password" />
</div>
<div slot="submit-button">
<!-- Custom button -->
<button type="submit" disabled="{isLoading}">{isLoading ? 'Logging in...' : 'Login'}</button>
</div>
</LoginForm>
For better organization, create a pocketcrud.config.js
file in your admin routes directory:
// routes/admin/pocketcrud.config.js
export const fieldOverrides = {
posts: {
body: { type: 'textarea', rows: 8 },
content: { type: 'textarea', rows: 6 },
},
snippets: {
body: { type: 'textarea', rows: 10 },
description: { type: 'textarea', rows: 3 },
},
};
export const primaryDisplayFields = {
posts: 'title',
snippets: 'title',
comments: 'comment',
categories: 'name',
};
export const suppressedCollections = ['users'];
export const pocketbase = {
url: 'https://your-app.pockethost.io/',
};
export function getFieldOverrides(collectionName) {
return fieldOverrides[collectionName] || {};
}
export function getPrimaryDisplayField(collectionName) {
return primaryDisplayFields[collectionName];
}
Then use in your components:
<!-- routes/admin/[slug]/+page.svelte -->
<script>
import { CollectionManager } from 'pocketcrud/svelte';
import { getFieldOverrides, getPrimaryDisplayField } from '../pocketcrud.config.js';
export let collectionName;
export let crud;
</script>
<CollectionManager
{crud}
{collectionName}
fieldOverrides="{getFieldOverrides(collectionName)}"
primaryDisplayField="{getPrimaryDisplayField(collectionName)}"
perPage="{20}"
/>
File structure:
app/
โโโ src/
โ โโโ routes/
โ โโโ admin/
โ โโโ pocketcrud.config.js โ Config file
โ โโโ +page.svelte โ Collection list
โ โโโ [slug]/
โ โ โโโ +page.svelte โ Collection manager (uses ../pocketcrud.config.js)
โ โโโ login/
โ โโโ +page.svelte โ Login form
For a complete implementation example, see the GitHub repository for working code samples and configuration patterns.
PocketBase Type | Form Input | Features |
---|---|---|
text |
Text input | Pattern validation, length limits |
editor |
Textarea | Rich text editing area |
number |
Number input | Min/max validation |
bool |
Checkbox | Boolean toggle |
email |
Email input | Email format validation |
url |
URL input | URL format validation |
date |
Date input | Date picker |
select |
Select/Multi-select | Single or multiple options |
file |
File input | Single or multiple files |
relation |
Select dropdown | Related record selection |
json |
Textarea | JSON validation and formatting |
class PocketCrud {
constructor(options: CrudOptions);
// Schema introspection
async getCollections(): Promise<CollectionSchema[]>;
async getCollection(idOrName: string): Promise<CollectionSchema>;
async getCollectionSchema(idOrName: string): Promise<CollectionField[]>;
// CRUD operations
async create<T>(collection: string, data: Record<string, any>): Promise<T>;
async getOne<T>(collection: string, id: string, options?: QueryOptions): Promise<T>;
async getList<T>(collection: string, options?: QueryOptions): Promise<ListResult<T>>;
async getFullList<T>(collection: string, options?: QueryOptions): Promise<T[]>;
async update<T>(collection: string, id: string, data: Record<string, any>): Promise<T>;
async delete(collection: string, id: string): Promise<boolean>;
// Access underlying PocketBase client
get client(): PocketBase;
}
// Generate form field configurations from schema
function getFormFieldConfig(field: CollectionField): FormFieldConfig | null;
function getFormFields(schema: CollectionField[]): FormFieldConfig[];
// Validation and data preparation
function validateFormData(data: Record<string, any>, schema: CollectionField[]): string[];
function prepareFormData(data: Record<string, any>, schema: CollectionField[]): Record<string, any>;
# Unit tests
bun run test
# Integration tests (requires running SvelteKit app)
bun run test:integration
# Test with UI
bun run test:ui
bun run test:integration:ui
The package uses a TypeScript build pipeline that:
bun run build # Full build (clean + tsc + alias resolution + copy)
bun run build:tsc # TypeScript compilation only
bun run build:resolve # Resolve path aliases
bun run build:copy # Copy Svelte/CSS files
For local package development and testing:
# Build the package
bun run build
# Link locally for testing in your projects
yalc publish
# In your test project
yalc add pocketcrud
bun run lint # ESLint
PocketCrud components use CSS variables for easy customization. You can override these variables to match your app's design system.
:root {
/* Colors */
--pc-primary: #3b82f6;
--pc-primary-hover: #2563eb;
--pc-secondary: #6b7280;
--pc-secondary-hover: #4b5563;
--pc-danger: #ef4444;
--pc-danger-hover: #dc2626;
--pc-success: #10b981;
--pc-warning: #f59e0b;
/* Backgrounds */
--pc-bg-base: #ffffff;
--pc-bg-surface: #f9fafb;
--pc-bg-hover: #f3f4f6;
/* Borders */
--pc-border-color: #e5e7eb;
--pc-border-radius: 0.375rem;
--pc-border-width: 1px;
/* Text */
--pc-text-primary: #111827;
--pc-text-secondary: #6b7280;
--pc-text-muted: #9ca3af;
--pc-text-inverse: #ffffff;
/* Spacing */
--pc-spacing-xs: 0.25rem;
--pc-spacing-sm: 0.5rem;
--pc-spacing-md: 1rem;
--pc-spacing-lg: 1.5rem;
--pc-spacing-xl: 2rem;
/* Typography */
--pc-font-family: inherit;
--pc-font-size-sm: 0.875rem;
--pc-font-size-base: 1rem;
--pc-font-size-lg: 1.125rem;
/* Shadows */
--pc-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--pc-shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--pc-shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
/* Transitions */
--pc-transition-speed: 150ms;
--pc-transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
}
Each component has a unique class for targeted styling:
.pocketcrud-login
- Login form wrapper.pocketcrud-setup
- Setup form wrapper.pocketcrud-collection-manager
- Collection manager wrapper.pocketcrud-record-list
- Record list wrapper.pocketcrud-dynamic-form
- Dynamic form wrapperPocketCrud components require the base CSS file. The styles are shared between both Svelte and React components:
// In your app's layout or main component
import 'pocketcrud/styles';
For Next.js App Router:
// app/layout.tsx
import 'pocketcrud/styles';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Note: The base styles use sensible defaults with Tailwind-like colors. You can either:
/* Override in your app's CSS file */
:root {
--pc-primary: #8b5cf6; /* Purple instead of blue */
--pc-border-radius: 0.5rem; /* More rounded corners */
--pc-font-family: 'Inter', sans-serif; /* Custom font */
}
/* Customize specific components */
.pocketcrud-login {
background: linear-gradient(to bottom right, #667eea, #764ba2);
}
.pocketcrud-btn-primary {
text-transform: uppercase;
letter-spacing: 0.05em;
}
File structure for theming:
app/
โโโ src/
โ โโโ routes/
โ โโโ admin/
โ โโโ admin.css โ Your theme overrides
โ โโโ +layout.svelte โ Import admin.css here
โ โโโ +page.svelte
PocketCrud is built with TypeScript and includes both Svelte and React components:
pocketcrud/
โโโ src/ # Source files
โ โโโ utils/
โ โ โโโ crud.js # PocketCrud class for database operations
โ โ โโโ form-utils.js # Form field generation and validation
โ โ โโโ index.d.ts # TypeScript definitions
โ โโโ components/
โ โ โโโ svelte/ # Svelte components
โ โ โ โโโ Auth/
โ โ โ โ โโโ LoginForm.svelte
โ โ โ โ โโโ SetupForm.svelte
โ โ โ โโโ Collections/
โ โ โ โ โโโ CollectionManager.svelte
โ โ โ โโโ Records/
โ โ โ โโโ RecordList.svelte
โ โ โ โโโ DynamicForm.svelte
โ โ โโโ react/ # React components (TypeScript)
โ โ โ โโโ Auth/
โ โ โ โ โโโ LoginForm.tsx
โ โ โ โ โโโ SetupForm.tsx
โ โ โ โโโ Collections/
โ โ โ โ โโโ CollectionManager.tsx
โ โ โ โโโ Records/
โ โ โ โโโ RecordList.tsx
โ โ โ โโโ DynamicForm.tsx
โ โ โโโ styles/ # Shared CSS for all components
โ โ โโโ pocketcrud.css
โ โโโ index.js # Main package entry
โโโ dist/ # Built files (published to npm)
โ โโโ utils/ # Compiled utilities
โ โโโ components/
โ โ โโโ svelte/ # Svelte components (copied as-is)
โ โ โโโ react/ # Compiled React components (.js + .d.ts)
โ โ โโโ styles/ # Shared CSS
โ โโโ index.js
โโโ demo/ # Demo apps (not published to npm)
Published to npm:
dist/
directory only (built files)This package provides everything you need to build a complete admin interface in either Svelte or React:
git clone https://github.com/jackkeller/pocketcrud.git
bun install
bun run test
MIT License - see LICENSE file for details.
All Svelte components have been ported to React with identical functionality and React-friendly APIs (callbacks instead of events, controlled components).
// Import all React components
import { LoginForm, SetupForm, CollectionManager, RecordList, DynamicForm } from 'pocketcrud/react';
// Or import specific groups
import { LoginForm, SetupForm } from 'pocketcrud/react/auth';
import { CollectionManager } from 'pocketcrud/react/collections';
import { RecordList, DynamicForm } from 'pocketcrud/react/records';
'use client'; // For Next.js App Router
import { LoginForm } from 'pocketcrud/react';
import { useRouter } from 'next/navigation';
import PocketCrud from 'pocketcrud';
import { useState } from 'react';
export default function LoginPage() {
const router = useRouter();
const crud = new PocketCrud({ url: 'https://your-pb-url.com' });
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
async function handleLogin(data: { email: string; password: string }) {
setIsLoading(true);
setError('');
try {
await crud.client.collection('users').authWithPassword(data.email, data.password);
router.push('/admin');
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
}
return (
<LoginForm
email={email}
password={password}
isLoading={isLoading}
error={error}
onEmailChange={setEmail}
onPasswordChange={setPassword}
onSubmit={handleLogin}
/>
);
}
'use client';
import { SetupForm } from 'pocketcrud/react';
import { useRouter } from 'next/navigation';
import PocketCrud from 'pocketcrud';
import { useState } from 'react';
export default function SetupPage() {
const router = useRouter();
const crud = new PocketCrud({ url: 'https://your-pb-url.com' });
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
async function handleSetup(data: { email: string; password: string; passwordConfirm: string }) {
if (data.password !== data.passwordConfirm) {
setError('Passwords do not match');
return;
}
if (data.password.length < 10) {
setError('Password must be at least 10 characters');
return;
}
setIsLoading(true);
setError('');
setSuccess('');
try {
await crud.createAdmin(data.email, data.password);
setSuccess('Admin user created successfully!');
setTimeout(() => router.push('/admin/login'), 2000);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
}
return (
<SetupForm
email={email}
password={password}
passwordConfirm={passwordConfirm}
isLoading={isLoading}
error={error}
success={success}
onEmailChange={setEmail}
onPasswordChange={setPassword}
onPasswordConfirmChange={setPasswordConfirm}
onSubmit={handleSetup}
/>
);
}
'use client';
import { CollectionManager } from 'pocketcrud/react';
import PocketCrud from 'pocketcrud';
interface CollectionPageProps {
params: { slug: string };
}
export default function CollectionPage({ params }: CollectionPageProps) {
const crud = new PocketCrud({ url: 'https://your-pb-url.com' });
// Optional: Configure field overrides
const fieldOverrides = {
body: { type: 'textarea', rows: 8 },
content: { type: 'textarea', rows: 6 },
};
return (
<CollectionManager
crud={crud}
collectionName={params.slug}
fieldOverrides={fieldOverrides}
primaryDisplayField="title"
perPage={20}
/>
);
}
'use client';
import { RecordList } from 'pocketcrud/react';
export default function MyRecordList({ records, schema }) {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = 10;
const totalItems = 100;
const perPage = 20;
function handleEdit(record) {
console.log('Edit record:', record);
}
function handleDelete(record) {
if (confirm('Delete this record?')) {
console.log('Delete record:', record);
}
}
function handlePageChange(page) {
setCurrentPage(page);
// Fetch new page of records
}
return (
<RecordList
records={records}
schema={schema}
currentPage={currentPage}
totalPages={totalPages}
totalItems={totalItems}
perPage={perPage}
primaryDisplayField="title"
onEdit={handleEdit}
onDelete={handleDelete}
onPageChange={handlePageChange}
/>
);
}
'use client';
import { DynamicForm } from 'pocketcrud/react';
export default function MyForm({ schema, initialData }) {
const fieldOverrides = {
body: { type: 'textarea', rows: 8 },
};
function handleSubmit(formData) {
console.log('Form submitted:', formData);
// Save to PocketBase
}
function handleCancel() {
console.log('Form cancelled');
}
return (
<DynamicForm
schema={schema}
initialData={initialData}
fieldOverrides={fieldOverrides}
onSubmit={handleSubmit}
onCancel={handleCancel}
/>
);
}
For Next.js 13+ App Router, all React components include the 'use client'
directive and work seamlessly:
// app/admin/[slug]/page.tsx
'use client';
import { CollectionManager } from 'pocketcrud/react';
import PocketCrud from 'pocketcrud';
export default function AdminCollectionPage({ params }: { params: { slug: string } }) {
const crud = new PocketCrud({ url: process.env.NEXT_PUBLIC_POCKETBASE_URL });
return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Manage {params.slug}</h1>
<CollectionManager crud={crud} collectionName={params.slug} />
</div>
);
}
While functionality is identical, the APIs differ to match each framework's patterns:
Feature | Svelte | React |
---|---|---|
Events | Event dispatchers (on:submit ) |
Callback props (onSubmit ) |
Data Binding | Two-way binding (bind:value ) |
Controlled components (value + onChange ) |
Customization | Slots | Render props / children |
Styling | Same CSS variables | Same CSS variables |
Both use the same pocketcrud/styles
CSS file.