Resources
Create typed, versioned resources with real-time sync
deconfig is deco CMS’s git-like versioned filesystem for storing resources. This guide shows you how to define custom resource types with full CRUD operations and real-time synchronization.
Use cases:
- Theme settings
- User preferences
- Configuration data
- Feature flags
- Any typed data that needs versioning and real-time updates
How It Works
Define a resource schema → Auto-generated CRUD tools → Type-safe frontend client → Real-time SSE updates
Resources are:
- Typed with Zod schemas
- Versioned in deconfig (git-like storage)
- Referenceable via
rsc://URIs - Real-time with Server-Sent Events
Define a Resource
Create a schema in server/tools/ :
import { DeconfigResource } from "@decocms/runtime/deconfig";
import { z } from "zod";
const ThemeSettings = DeconfigResource.define({
resourceName: "theme_settings",
dataSchema: z.object({
name: z.string().min(1),
primaryColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
secondaryColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
textColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/),
}),
});
// Register in your tools array
export const tools = [
(env: Env) => ThemeSettings.create(env),
// ... other tools
];
export { ThemeSettings };
This generates 6 CRUD tools automatically:
| Tool | Purpose |
|---|---|
CREATE | Create new resource with data |
READ | Get resource by URI |
UPDATE | Update resource (partial merge) |
DELETE | Delete resource by URI |
SEARCH | List/filter resources with pagination |
DESCRIBE | Get metadata and SSE endpoint |
Resource URIs
Resources are identified by URIs in the format: rsc://app-id/theme_settings/resource-id
The resource-id can be auto-generated or you can specify it in CREATE:
await client.tools.DECO_RESOURCE_THEME_SETTINGS_CREATE({
resourceId: "dark-theme", // Optional: specify ID
data: { /* ... */ }
});
Use in Frontend
After running npm run gen:self , use the generated client:
Basic Operations
import { client } from "./lib/rpc";
// Create
const theme = await client.tools.DECO_RESOURCE_THEME_SETTINGS_CREATE({
data: {
name: "Dark Theme",
primaryColor: "#1a1a1a",
secondaryColor: "#2d2d2d",
textColor: "#ffffff",
}
});
// Read
const theme = await client.tools.DECO_RESOURCE_THEME_SETTINGS_READ({
uri: "rsc://my-app/theme_settings/dark-theme"
});
// Update (partial update - merges with existing)
await client.tools.DECO_RESOURCE_THEME_SETTINGS_UPDATE({
uri: "rsc://my-app/theme_settings/dark-theme",
data: { primaryColor: "#000000" }
});
// Delete
await client.tools.DECO_RESOURCE_THEME_SETTINGS_DELETE({
uri: "rsc://my-app/theme_settings/dark-theme"
});
// Search with filtering and pagination
const results = await client.tools.DECO_RESOURCE_THEME_SETTINGS_SEARCH({
term: "dark", // Searches in name, description, resourceId
page: 1, // Page number (starts at 1)
pageSize: 10, // Items per page (use Infinity for all)
sortBy: "name", // Sort by: "name", "resourceId", "mtime"
sortOrder: "asc", // "asc" or "desc"
filters: { // Optional filters
created_by: "user-id",
updated_by: ["user-1", "user-2"],
},
});
// Result structure
// {
// items: Resource[],
// total: number,
// page: number,
// pageSize: number
// }
Metadata tracking: Resources automatically track created_by , updated_by , created_at , and updated_at .
Real-time Updates with SSE
Subscribe to resource changes via Server-Sent Events:
import { useEffect } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { client } from "./lib/rpc";
function useThemeSync() {
const queryClient = useQueryClient();
useEffect(() => {
let eventSource: EventSource;
const setup = async () => {
// Get SSE endpoint
const desc = await client.tools.DECO_RESOURCE_THEME_SETTINGS_DESCRIBE({});
const watchUrl = desc.features?.watch?.pathname;
if (!watchUrl) return;
// Connect to SSE stream (watch all theme_settings resources)
const uri = desc.uriTemplate; // rsc://app/theme_settings/*
eventSource = new EventSource(`${watchUrl}?uri=${encodeURIComponent(uri)}`);
eventSource.onmessage = (event) => {
const update = JSON.parse(event.data);
// Invalidate queries to trigger refetch
queryClient.invalidateQueries({ queryKey: ["themes"] });
};
eventSource.onerror = (error) => {
console.error("SSE connection error:", error);
eventSource.close();
// Implement reconnection logic if needed
};
};
setup();
return () => {
eventSource?.close();
};
}, [queryClient]);
}
How it works:
- Call
DESCRIBEto get SSE endpoint - Connect EventSource to watch URL with URI pattern
- Receive events when resources change
- Update UI by invalidating React Query cache
Changes propagate instantly to all connected clients.
Complete Example
Combine CRUD + real-time sync:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import { client } from './your-client';
export function useThemeSettings() {
const queryClient = useQueryClient();
// Fetch all themes
const { data: themes, isLoading } = useQuery({
queryKey: ["themes"],
queryFn: async () => {
const result = await client.DECO_RESOURCE_THEME_SETTINGS_SEARCH({
pageSize: Infinity,
});
return result.items;
},
});
// Create mutation
const createTheme = useMutation({
mutationFn: async (data: ThemeData) => {
return client.DECO_RESOURCE_THEME_SETTINGS_CREATE({ data });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["themes"] });
},
});
// Update mutation
const updateTheme = useMutation({
mutationFn: async ({ uri, data }: { uri: string; data: Partial<ThemeData> }) => {
return client.DECO_RESOURCE_THEME_SETTINGS_UPDATE({ uri, data });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["themes"] });
},
});
// Real-time SSE subscription
useEffect(() => {
let eventSource: EventSource | null = null;
const setupSSE = async () => {
try {
const description = await client.DECO_RESOURCE_THEME_SETTINGS_DESCRIBE({});
const watchPathname = description.features?.watch?.pathname;
const uriTemplate = description.uriTemplate;
if (watchPathname) {
const watchUrl = `${watchPathname}?uri=${encodeURIComponent(uriTemplate)}`;
eventSource = new EventSource(watchUrl);
eventSource.onmessage = (event) => {
const update = JSON.parse(event.data);
if (update.uri) {
queryClient.invalidateQueries({ queryKey: ["themes"] });
}
};
}
} catch (error) {
console.error("Failed to setup SSE:", error);
}
};
setupSSE();
return () => {
eventSource?.close();
};
}, [queryClient]);
return {
themes,
isLoading,
createTheme: createTheme.mutate,
updateTheme: updateTheme.mutate,
};
}
Schema Best Practices
Design resources with searchability in mind:
const MyResource = DeconfigResource.define({
resourceName: "my_resource",
dataSchema: z.object({
name: z.string().min(1).describe("Display name"),
description: z.string().describe("Resource description"),
// ^ These fields are searchable by default
// Your custom fields
apiKey: z.string().describe("API key"),
environment: z.enum(["dev", "prod"]).describe("Environment"),
isActive: z.boolean().default(true),
}),
});
Tips:
- Include
nameanddescriptionfor better search - Use
.describe()for documentation - Set sensible defaults with
.default() - Validate formats strictly (hex colors, emails, URLs)
Advanced Options
Custom Validation
Add business logic validation beyond schema checks:
const UserSettings = DeconfigResource.define({
resourceName: "user_settings",
dataSchema: z.object({
username: z.string(),
email: z.string().email(),
}),
validate: async (data) => {
const exists = await checkUsernameExists(data.username);
if (exists) {
throw new Error("Username already exists");
}
},
});
Custom Storage Path
const PrivateSettings = DeconfigResource.define({
resourceName: "private_settings",
dataSchema: /* ... */,
directory: "/private/user_settings",
});
Watch Specific Resource
// Watch one specific resource instead of all
const uri = "rsc://my-app/theme_settings/dark-theme";
const watchUrl = `${watchPathname}?uri=${encodeURIComponent(uri)}`;
Tool Description Enhancements
Customize generated tool descriptions:
const MyResource = DeconfigResource.define({
resourceName: "my_resource",
dataSchema: /* ... */,
enhancements: {
DECO_RESOURCE_MY_RESOURCE_SEARCH: {
description: "Search my resources with custom filters",
},
},
});
External Data Signaling
Use deconfig resources as signals to trigger refetches for external data (Airtable, Google Sheets, APIs):
Pattern:
- Create minimal signal resource with
lastUpdatedtimestamp - Update signal when external data changes
- Frontend watches signal via SSE, refetches external data on changes
Example:
// Backend: Signal resource
const ExternalSync = DeconfigResource.define({
resourceName: "external_sync",
dataSchema: z.object({
source: z.string(),
lastUpdated: z.string().datetime(),
}),
});
// Backend: Update signal after external change
await client.tools.DECO_RESOURCE_EXTERNAL_SYNC_UPDATE({
uri: "rsc://app/external_sync/airtable-data",
data: { source: "airtable", lastUpdated: new Date().toISOString() }
});
// Frontend: Watch signal and refetch
useEffect(() => {
// Watch external_sync resource
// On change → refetch from Airtable/external API
}, []);
Error Handling
Handle resource operation errors:
try {
const theme = await client.tools.DECO_RESOURCE_THEME_SETTINGS_READ({
uri: "rsc://app/theme_settings/missing"
});
} catch (error) {
if (error.message.includes("not found")) {
// Resource doesn't exist
} else if (error.message.includes("validation")) {
// Schema validation failed
} else {
// Other errors
}
}
Common errors:
NotFoundError: Resource URI doesn’t existValidationError: Data doesn’t match schemaConflictError: Resource with ID already exists (on CREATE)
Performance Tips
Pagination:
// Bad: Load everything
pageSize: Infinity
// Good: Paginate large datasets
pageSize: 20, page: currentPage
React Query stale time:
useQuery({
queryKey: ["themes"],
queryFn: fetchThemes,
staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
Debounce updates:
const debouncedUpdate = debounce(
(data) => client.tools.DECO_RESOURCE_THEME_SETTINGS_UPDATE({ uri, data }),
500
);
Configuration Reference
All options available in DeconfigResource.define() :
DeconfigResource.define({
// Required
resourceName: string, // Resource type (snake_case)
dataSchema: ZodSchema, // Zod schema for validation
// Optional
directory?: string, // Custom storage path
validate?: async (data) => void, // Custom validation logic
enhancements?: { // Custom tool descriptions
DECO_RESOURCE_NAME_OPERATION: {
description: string,
},
},
})
Returns:
create(env): Function to register resource tools- Generated CRUD tools with typed interfaces
Found an error or want to improve this page?
Edit this page