);
}
```
#### FieldProps
| Prop | Type | Description |
|------|------|-------------|
| `value` | `T` | Current field value |
| `setValue` | `(value: T) => void` | Update the field value |
| `error` | `string` | Validation error message |
| `showError` | `boolean` | Whether to display the error |
| `isSubmitting` | `boolean` | Form is being saved |
| `property` | `Property` | The property configuration |
| `context` | `FormContext` | Full form context with all entity values |
| `disabled` | `boolean` | Field is readonly |
| `tableMode` | `boolean` | Rendering inside the spreadsheet (compact mode) |
### Registering a Custom Field
#### Per-Property
Register on a single property:
```typescript
properties: {
brand_color: {
type: "string",
name: "Brand Color",
Field: ColorPickerField
}
}
```
#### Global Property Config
Register a reusable field type:
```typescript
const colorPropertyConfig: PropertyConfig = {
key: "color_picker",
name: "Color Picker",
Field: ColorPickerField,
property: {
type: "string"
}
};
// Register globally
```
Then use it in any collection:
```typescript
properties: {
color: {
type: "string",
name: "Color",
propertyConfig: "color_picker"
}
}
```
### Accessing Form Context
Custom fields can access the full entity values:
```tsx
function PriceWithTaxField({ value, setValue, context }: FieldProps) {
const taxRate = context.values.tax_rate ?? 0.1;
const priceWithTax = value ? value * (1 + taxRate) : 0;
return (
setValue(Number(e.target.value))}
/>
With tax: ${priceWithTax.toFixed(2)}
);
}
```
### Table Mode
When rendering inside the spreadsheet view, fields should be compact. Check `tableMode`:
```tsx
function MyField({ value, setValue, tableMode }: FieldProps) {
if (tableMode) {
return { /* open editor */ }}>{value};
}
return (
);
}
```
### Custom Previews
For custom rendering in the table (non-editing mode), use the `Preview` component:
```tsx
function ColorPreview({ value }: { value: string }) {
return (
{value}
);
}
// Register it
properties: {
color: {
type: "string",
name: "Color",
Field: ColorPickerField,
Preview: ColorPreview
}
}
```
### Next Steps
- **[Entity Views](/docs/frontend/entity-views)** — Custom tabs in the entity editor
- **[Entity Actions](/docs/frontend/entity-actions)** — Custom action buttons
- **[Additional Columns](/docs/frontend/additional-columns)** — Computed table columns
## Entity Views
### Overview
Entity views let you add custom **tabs** to the entity detail page alongside the default form. Use them for:
- Live **previews** (website preview, rendered content)
- **Related data** views (order items, child entities)
- **Analytics** or charts
- **Custom editors** (rich text, map editors)
### Adding Entity Views
```typescript
const articlesCollection: EntityCollection = {
slug: "articles",
name: "Articles",
entityViews: [
{
key: "preview",
name: "Preview",
Builder: ArticlePreview
},
{
key: "related",
name: "Related Articles",
Builder: RelatedArticlesView
}
],
properties: { /* ... */ }
};
```
### Building an Entity View
```tsx
function ArticlePreview({
entity,
modifiedValues,
formContext
}: EntityCustomViewParams) {
// modifiedValues has the unsaved, live form values
const title = modifiedValues?.title ?? entity?.values?.title;
const content = modifiedValues?.content ?? entity?.values?.content;
return (
{title}
);
}
```
#### EntityCustomViewParams
| Prop | Type | Description |
|------|------|-------------|
| `entity` | `Entity` | The saved entity (null for new entities) |
| `modifiedValues` | `EntityValues` | Current unsaved form values (live as user types) |
| `formContext` | `FormContext` | Full form context |
| `collection` | `EntityCollection` | Collection definition |

### Controlling Position
Views appear as tabs. You can configure their position:
```typescript
entityViews: [
{
key: "preview",
name: "Preview",
Builder: ArticlePreview,
position: "start" // Appears before the default form tab
}
]
```
### Next Steps
- **[Custom Fields](/docs/frontend/custom-fields)** — Build custom form fields
- **[Entity Actions](/docs/frontend/entity-actions)** — Custom action buttons
## Entity Actions
### Overview
Entity actions are custom buttons that appear on individual entities. Use them for operations like publishing, archiving, cloning, or triggering external workflows.
### Defining Entity Actions
```typescript
const articlesCollection: EntityCollection = {
slug: "articles",
entityActions: [
{
name: "Publish",
icon: "publish",
onClick: async ({ entity, context }) => {
await context.dataSource.saveEntity({
path: entity.path,
entityId: entity.id,
values: { status: "published", published_at: new Date() },
collection: articlesCollection
});
context.snackbarController.open({
message: "Article published!"
});
}
},
{
name: "Clone",
icon: "content_copy",
onClick: async ({ entity, context }) => {
const { id, ...values } = entity.values;
await context.dataSource.saveEntity({
path: entity.path,
values: { ...values, name: values.name + " (Copy)" },
collection: articlesCollection
});
}
}
],
properties: { /* ... */ }
};
```
### Collection Actions
For toolbar-level actions that work on the collection or selected entities:
```tsx
function PublishSelectedAction({ selectionController, context }: CollectionActionsProps) {
const handlePublish = async () => {
const selected = selectionController.selectedEntities;
for (const entity of selected) {
await context.dataSource.saveEntity({
path: entity.path,
entityId: entity.id,
values: { status: "published" },
collection: context.collection
});
}
};
return (
);
}
// Register
const collection: EntityCollection = {
Actions: PublishSelectedAction,
// ...
};
```

### Next Steps
- **[Additional Columns](/docs/frontend/additional-columns)** — Computed table columns
- **[Custom Fields](/docs/frontend/custom-fields)** — Custom form fields
## Additional Columns
### Overview
Additional columns let you display computed or derived data in the collection table without storing it in the database.
### Defining Additional Columns
```typescript
const ordersCollection: EntityCollection = {
slug: "orders",
additionalFields: [
{
key: "total_display",
name: "Total",
Builder: ({ entity }) => {
const total = entity.values.items?.reduce(
(sum, item) => sum + (item.price * item.quantity), 0
) ?? 0;
return ${total.toFixed(2)};
}
},
{
key: "status_badge",
name: "Status",
Builder: ({ entity }) => {
const color = entity.values.status === "completed" ? "green" : "orange";
return (
{entity.values.status}
);
},
dependencies: ["status"] // Re-render when these fields change
}
],
properties: { /* ... */ }
};
```
### Builder Props
| Prop | Type | Description |
|------|------|-------------|
| `entity` | `Entity` | The entity for this row |
| `context` | `RebaseContext` | Full Rebase context |
### Next Steps
- **[Entity Actions](/docs/frontend/entity-actions)** — Custom action buttons
- **[Custom Fields](/docs/frontend/custom-fields)** — Custom form fields
## Hooks Reference
### Overview
Rebase provides React hooks to access framework functionality from any component within the `` provider tree.
### `useRebaseContext`
The master hook — access everything:
```typescript
function MyComponent() {
const context = useRebaseContext();
context.dataSource // Data operations
context.storageSource // File operations
context.authController // Auth state
context.navigation // Navigation state
context.sideEntityController // Side panel control
context.snackbarController // Toast notifications
}
```
### `useAuthController`
Access authentication state:
```typescript
function UserMenu() {
const auth = useAuthController();
auth.user // Current user (or null)
auth.initialLoading // Loading initial session
auth.signOut() // Log out
auth.getAuthToken() // Get JWT for API calls
auth.extra // Additional user data (roles, etc.)
}
```
### `useSideEntityController`
Programmatically open entities in a side panel:
```typescript
function OpenProductButton({ productId }) {
const sideEntityController = useSideEntityController();
return (
);
}
```
Methods:
| Method | Description |
|--------|-------------|
| `open({ path, entityId, collection })` | Open an entity in a side panel |
| `close()` | Close the current side panel |
| `replace({ path, entityId, collection })` | Replace the current side panel content |
### `useSnackbarController`
Show toast notifications:
```typescript
function SaveButton() {
const snackbar = useSnackbarController();
const handleSave = async () => {
try {
await saveData();
snackbar.open({ type: "success", message: "Saved successfully!" });
} catch (error) {
snackbar.open({ type: "error", message: "Save failed" });
}
};
}
```
### `useStorageSource`
Access file storage operations:
```typescript
function FileUploader() {
const storage = useStorageSource();
const upload = async (file: File) => {
const result = await storage.uploadFile({
file,
fileName: file.name,
path: "documents"
});
const url = await storage.getDownloadURL(result.path);
return url;
};
}
```
### `useModeController`
Control light/dark theme:
```typescript
function ThemeToggle() {
const mode = useModeController();
return (
);
}
```
### `useReferenceDialog`
Open a reference selection dialog:
```typescript
function SelectProduct() {
const referenceDialog = useReferenceDialog({
path: "products",
collection: productsCollection,
onSingleEntitySelected: (entity) => {
console.log("Selected:", entity);
}
});
return ;
}
```
### `useNavigationController`
Access navigation state and resolved collections:
```typescript
function MyComponent() {
const navigation = useNavigationController();
navigation.collections // All registered collections
navigation.views // Custom views
navigation.adminViews // Admin-mode views
navigation.getCollection(path) // Get collection for a path
}
```
### Next Steps
- **[Frontend Overview](/docs/frontend)** — React framework reference
- **[Client SDK](/docs/sdk)** — Data operations SDK
## Plugin System
### Overview
Plugins are the primary extension mechanism in Rebase. They can:
- Wrap the entire app with a **provider** (context, state management)
- Add **home page actions** and widgets
- Inject **collection view** components (toolbar, column builders)
- Add **form** components (field builders, additional panels)
- **Inject or modify collections** dynamically
### Plugin Interface
```typescript
interface RebasePlugin {
key: string; // Unique identifier
loading?: boolean; // Show loading state while initializing
// Wrap the app with a provider
provider?: {
Component: React.ComponentType;
};
// Home page customization
homePage?: {
additionalActions?: React.ReactNode;
additionalChildrenStart?: React.ReactNode;
additionalChildrenEnd?: React.ReactNode;
};
// Collection view customization
collectionView?: {
showTextSearchBar?: boolean;
CollectionActions?: React.ComponentType[];
AddColumnComponent?: React.ComponentType;
onCellValueChange?: (params) => void;
};
// Entity form customization
form?: {
Actions?: React.ComponentType;
provider?: { Component: React.ComponentType };
fieldBuilder?: (params) => React.ReactNode | null;
};
// Collection injection/modification
collection?: {
injectCollections?: (params) => EntityCollection[];
modifyCollection?: (params) => EntityCollection;
};
}
```
### Using Plugins
Pass plugin instances to the navigation controller:
```typescript
const dataEnhancementPlugin = useDataEnhancementPlugin();
const collectionEditorPlugin = useCollectionEditorPlugin({
collectionConfigController
});
const plugins = [dataEnhancementPlugin, collectionEditorPlugin];
const navigationStateController = useBuildNavigationStateController({
plugins,
collections: () => collections,
// ...
});
```
### Building a Plugin
Here's a minimal plugin that adds a toolbar action to every collection:
```typescript
function useMyPlugin(): RebasePlugin {
return {
key: "my_plugin",
collectionView: {
CollectionActions: [MyToolbarAction]
},
form: {
fieldBuilder: ({ property, ...rest }) => {
// Return a custom field for specific property configs
if (property.propertyConfig === "my_custom_field") {
return ;
}
return null; // Use default field
}
}
};
}
```
### Built-in Plugins
#### Collection Editor Plugin
Visual schema editing — add/remove fields, change types, rearrange properties:
```typescript
const configController = useLocalCollectionsConfigController(client, collections, {
getAuthToken: authController.getAuthToken
});
const editorPlugin = useCollectionEditorPlugin({ collectionConfigController: configController });
```
#### Data Enhancement Plugin
AI-powered field autocompletion:
```typescript
const enhancementPlugin = useDataEnhancementPlugin();
```

### Collection Injection
Plugins can dynamically add new collections:
```typescript
collection: {
injectCollections: ({ collections, user }) => {
// Add an audit log collection for admins
if (user?.roles?.includes("admin")) {
return [auditLogCollection];
}
return [];
}
}
```
### Collection Modification
Plugins can modify existing collections:
```typescript
collection: {
modifyCollection: ({ collection }) => {
// Add a "last_modified_by" field to every collection
return {
...collection,
properties: {
...collection.properties,
last_modified_by: {
type: "string",
name: "Modified By",
readOnly: true
}
}
};
}
}
```
### Next Steps
- **[Studio Tools](/docs/studio)** — SQL console, JS console, RLS editor
- **[Custom Fields](/docs/frontend/custom-fields)** — Building custom form fields
## Studio Tools
### Overview
Rebase has two modes:
- **Content Mode** — For content editors and operations teams. Shows collections and data management.
- **Studio Mode** — For developers. Unlocks developer-facing tools.
Toggle between modes using the admin mode controller or the UI toggle in the app bar.
### Built-in Studio Tools
#### Collection Editor
A visual schema editor that lets you create and modify collections through a drag-and-drop UI. When you save changes, it uses [ts-morph](https://ts-morph.com/) to update your TypeScript source files via AST manipulation — preserving all existing code and custom logic.

```tsx
const configController = useLocalCollectionsConfigController(client, collections, {
getAuthToken: authController.getAuthToken
});
// Add as a custom view
{
slug: "schema",
name: "Edit Collections",
view:
}
```
#### SQL Console
Run raw SQL queries against your PostgreSQL database and see results in a table:
```tsx
{ slug: "sql", name: "SQL Console", view: }
```
#### JS Console
Write and execute JavaScript using the Rebase SDK:
```tsx
{ slug: "js", name: "JS Console", view: }
```
#### RLS Policy Editor
Visualize and manage Row Level Security policies for your PostgreSQL tables:
```tsx
{ slug: "rls", name: "RLS Policies", view: }
```
#### Storage Browser
Browse, upload, and manage files in your storage backends:
```tsx
{ slug: "storage", name: "Storage", view: }
```
### Adding Studio Views
Add studio tools as custom views in your `App.tsx`:
```typescript
const devViews: CMSView[] = [
{
slug: "sql",
name: "SQL Console",
group: "Database",
icon: "terminal",
view:
},
{
slug: "rls",
name: "RLS Policies",
group: "Database",
icon: "security",
view:
},
{
slug: "schema",
name: "Edit Collections",
group: "Schema",
icon: "view_list",
nestedRoutes: true,
view:
},
{
slug: "storage",
name: "Storage",
group: "Storage",
icon: "cloud",
view:
}
];
```
These views appear in the sidebar navigation when Studio mode is active.
### Next Steps
- **[Plugins](/docs/plugins)** — Extend the framework with plugins
- **[Collections](/docs/collections)** — Collection configuration
## CLI Reference
### Overview
The Rebase CLI (`rebase`) manages your project from scaffolding to deployment.
### Installation
```bash
npm install -g @rebasepro/cli
```
Or use via `npx`:
```bash
npx @rebasepro/cli
```
### Commands
#### `rebase init`
Initialize a new Rebase project:
```bash
rebase init [directory]
```
Sets up the project structure with frontend, backend, and shared packages.
#### `rebase dev`
Start the development server:
```bash
rebase dev
```
Starts both frontend and backend with hot reloading.
#### `rebase schema generate`
Generate Drizzle ORM schema from your TypeScript collections:
```bash
rebase schema generate
```
This reads your collections from `shared/collections/` and generates `backend/src/schema.generated.ts` with Drizzle table definitions, enums, and relations.
#### `rebase db push`
Push schema changes directly to the database (development only):
```bash
rebase db push
```
:::caution
`db push` modifies the database directly without migration files. Use `db generate` + `db migrate` for production.
:::
#### `rebase db generate`
Generate SQL migration files from schema changes:
```bash
rebase db generate
```
Creates timestamped migration files in `drizzle/` that can be reviewed and committed.
#### `rebase db migrate`
Run pending database migrations:
```bash
rebase db migrate
```
Applies all unapplied migrations to the database.
#### `rebase db studio`
Open Drizzle Studio to browse your database visually:
```bash
rebase db studio
```
#### `rebase generate_sdk`
Generate a typed client SDK from your collection definitions:
```bash
rebase generate_sdk
```
Creates TypeScript types and a type-safe client for all your collections.
#### `rebase auth`
Authentication management commands:
```bash
rebase auth create-user --email admin@example.com --password secret
rebase auth reset-password --email admin@example.com
```
### Migration Workflow
The typical workflow for schema changes:
```bash
## 1. Edit your collection in shared/collections/
## 2. Generate the Drizzle schema
rebase schema generate
## 3. Generate SQL migration
rebase db generate
## 4. Review the generated SQL in drizzle/
## 5. Apply the migration
rebase db migrate
```
### Next Steps
- **[Schema as Code](/docs/architecture/schema-as-code)** — How schema generation works
- **[Quickstart](/docs/getting-started/quickstart)** — Get started
## Data Import
### Overview
Rebase supports importing data from:
- **CSV** files
- **JSON** files
- **Excel** (`.xlsx`) files
The import wizard handles column mapping, data type coercion, and validation.
### How to Import
1. Open a collection in the admin panel
2. Click the **Import** button in the toolbar
3. Select or drag-drop your file
4. Map file columns to collection properties
5. Preview the data and resolve any validation errors
6. Click **Import** to save all entities

### Configuration
Enable/disable import per collection:
```typescript
const productsCollection: EntityCollection = {
slug: "products",
// Import is enabled by default
// To disable:
// importable: false
properties: { /* ... */ }
};
```
### Next Steps
- **[Data Export](/docs/features/data-export)** — Export data to CSV/JSON
## Data Export
### Overview
Export data from any collection to CSV or JSON format.
### How to Export
1. Open a collection
2. Click the **Export** button in the toolbar
3. Choose format (CSV or JSON)
4. Optionally filter before exporting to export a subset
### Configuration
```typescript
const productsCollection: EntityCollection = {
slug: "products",
exportable: true, // Enable (default: true)
// Or with config:
exportable: {
additionalFields: [
{
key: "computed_margin",
title: "Margin",
builder: ({ entity }) => {
return entity.values.price - entity.values.cost;
}
}
]
},
properties: { /* ... */ }
};
```
### Next Steps
- **[Data Import](/docs/features/data-import)** — Import data from files
## Recipe: Blog CMS
### Overview
Build a blog backend with:
- **Articles** with markdown content and cover images
- **Authors** with profiles
- **Categories** with a many-to-many relation
### Collections
#### Authors
```typescript
export const authorsCollection: EntityCollection = {
slug: "authors",
name: "Authors",
singularName: "Author",
dbPath: "authors",
icon: "person",
properties: {
name: {
type: "string",
name: "Name",
validation: { required: true }
},
email: {
type: "string",
name: "Email",
email: true,
validation: { required: true, unique: true }
},
avatar: {
type: "string",
name: "Avatar",
storage: {
storagePath: "avatars",
acceptedFiles: ["image/*"],
maxSize: 2 * 1024 * 1024
}
},
bio: {
type: "string",
name: "Bio",
multiline: true
}
}
};
```
#### Categories
```typescript
export const categoriesCollection: EntityCollection = {
slug: "categories",
name: "Categories",
singularName: "Category",
dbPath: "categories",
icon: "label",
properties: {
name: {
type: "string",
name: "Name",
validation: { required: true }
},
slug: {
type: "string",
name: "Slug",
validation: { required: true, unique: true }
},
color: {
type: "string",
name: "Color",
enum: [
{ id: "blue", label: "Blue", color: "blueDark" },
{ id: "green", label: "Green", color: "greenDark" },
{ id: "red", label: "Red", color: "pinkDark" },
{ id: "orange", label: "Orange", color: "orangeDark" }
]
}
}
};
```
#### Articles
```typescript
export const articlesCollection: EntityCollection = {
slug: "articles",
name: "Articles",
singularName: "Article",
dbPath: "articles",
icon: "article",
defaultViewMode: "table",
history: true,
properties: {
title: {
type: "string",
name: "Title",
validation: { required: true }
},
slug: {
type: "string",
name: "URL Slug",
validation: { required: true, unique: true }
},
author: {
type: "relation",
name: "Author",
relationName: "author"
},
status: {
type: "string",
name: "Status",
enum: [
{ id: "draft", label: "Draft", color: "grayDark" },
{ id: "review", label: "In Review", color: "orangeDark" },
{ id: "published", label: "Published", color: "greenDark" }
],
defaultValue: "draft"
},
cover_image: {
type: "string",
name: "Cover Image",
storage: {
storagePath: "articles/covers",
acceptedFiles: ["image/*"]
}
},
content: {
type: "string",
name: "Content",
markdown: true
},
excerpt: {
type: "string",
name: "Excerpt",
multiline: true,
validation: { max: 300 }
},
published_at: {
type: "date",
name: "Published At"
},
created_at: {
type: "date",
name: "Created At",
autoValue: "on_create",
readOnly: true
}
},
relations: [
{
relationName: "author",
target: () => authorsCollection,
cardinality: "one",
localKey: "author_id"
},
{
relationName: "categories",
target: () => categoriesCollection,
cardinality: "many",
through: {
table: "article_categories",
sourceColumn: "article_id",
targetColumn: "category_id"
}
}
],
callbacks: {
beforeSave: async ({ values, status }) => {
// Auto-generate slug
if (values.title && !values.slug) {
values.slug = values.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-");
}
// Set published_at when publishing
if (values.status === "published" && !values.published_at) {
values.published_at = new Date();
}
return values;
}
},
securityRules: [
{ operation: "select", access: "public", using: "{status} = 'published'" },
{ operation: "select", ownerField: "author_id" },
{ operations: ["insert", "update"], ownerField: "author_id" },
{ operation: "delete", roles: ["admin"] }
]
};
```
### Setup
1. Add all three collections to your `shared/collections/index.ts`
2. Run `rebase schema generate`
3. Run `rebase db push`
4. Restart the dev server
You now have a fully functional blog CMS with:
- Author management with avatar uploads
- Category tagging via many-to-many relations
- Markdown content editing
- Draft → Review → Published workflow
- Auto-generated URL slugs
- RLS policies limiting authors to their own posts
- Full audit trail via entity history
## Recipe: Custom Dashboard
### Overview
Build a custom dashboard view that displays analytics alongside your admin panel.
### Create the Dashboard Component
```tsx
function DashboardView() {
const context = useRebaseContext();
const [stats, setStats] = useState({
totalOrders: 0,
totalRevenue: 0,
activeProducts: 0,
recentOrders: []
});
useEffect(() => {
async function loadStats() {
// Use the data source to fetch aggregate data
const orders = await context.dataSource.fetchCollection({
path: "orders",
collection: ordersCollection,
limit: 1000
});
const products = await context.dataSource.fetchCollection({
path: "products",
collection: productsCollection,
filter: { active: ["==", true] }
});
setStats({
totalOrders: orders.length,
totalRevenue: orders.reduce((sum, o) => sum + (o.values.total ?? 0), 0),
activeProducts: products.length,
recentOrders: orders.slice(0, 5)
});
}
loadStats();
}, []);
return (
Dashboard
Recent Orders
{stats.recentOrders.map(order => (
Order #{order.id} — ${order.values.total}
))}
);
}
function StatCard({ title, value }: { title: string; value: string | number }) {
return (
{title}
{value}
);
}
```
### Register as a Custom View
```typescript
const views: CMSView[] = [
{
slug: "dashboard",
name: "Dashboard",
icon: "dashboard",
group: "Analytics",
view:
}
];
```
Pass it to the navigation controller:
```typescript
const navigationStateController = useBuildNavigationStateController({
views,
collections: () => collections,
// ...
});
```
The dashboard now appears in the sidebar under "Analytics" and is accessible at `/dashboard`.
### Adding Charts
Install a charting library:
```bash
npm install recharts
```
Then use it in your dashboard:
```tsx
function RevenueChart({ data }) {
return (
);
}
```
### Next Steps
- **[Custom Views](/docs/frontend)** — Frontend overview
- **[Hooks Reference](/docs/hooks)** — Available hooks
## Recipe: Webhook Integration
### Overview
Use `afterSave` and `afterDelete` callbacks to notify external services when data changes in Rebase.
### Slack Notification on New Order
```typescript
const ordersCollection: EntityCollection = {
slug: "orders",
name: "Orders",
dbPath: "orders",
callbacks: {
afterSave: async ({ values, entityId, status }) => {
if (status === "new") {
await fetch(process.env.SLACK_WEBHOOK_URL!, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `🛒 New order #${entityId}\nCustomer: ${values.customer_name}\nTotal: $${values.total}`
})
});
}
}
},
properties: { /* ... */ }
};
```
### Sync to External API
```typescript
callbacks: {
afterSave: async ({ values, entityId }) => {
// Sync product to Shopify
await fetch("https://your-shop.myshopify.com/admin/api/2024-01/products.json", {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-Shopify-Access-Token": process.env.SHOPIFY_TOKEN!
},
body: JSON.stringify({
product: {
id: values.shopify_id,
title: values.name,
body_html: values.description,
variants: [{ price: values.price }]
}
})
});
},
afterDelete: async ({ entityId, entity }) => {
// Remove from Shopify
if (entity.values.shopify_id) {
await fetch(
`https://your-shop.myshopify.com/admin/api/2024-01/products/${entity.values.shopify_id}.json`,
{
method: "DELETE",
headers: { "X-Shopify-Access-Token": process.env.SHOPIFY_TOKEN! }
}
);
}
}
}
```
### Error Handling
Use `afterSaveError` to handle failures gracefully:
```typescript
callbacks: {
afterSave: async ({ values, entityId }) => {
// This might fail
await syncToExternalService(values);
},
afterSaveError: async ({ entityId, error }) => {
// Log the error, send alert, or retry
console.error(`Webhook failed for entity ${entityId}:`, error);
await sendErrorAlert(entityId, error);
}
}
```
### Next Steps
- **[Entity Callbacks](/docs/collections/callbacks)** — Full callback reference
- **[Blog CMS Recipe](/docs/recipes/blog-cms)** — Complete blog example