Deco
Full-Code Guides

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:

  1. Call DESCRIBE to get SSE endpoint
  2. Connect EventSource to watch URL with URI pattern
  3. Receive events when resources change
  4. 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 name and description for 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:

  1. Create minimal signal resource with lastUpdated timestamp
  2. Update signal when external data changes
  3. 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 exist
  • ValidationError : Data doesn’t match schema
  • ConflictError : 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