Stateful Collections (CRUD)

Full CRUD operations with persistent state management and server-side pagination

Overview

Stateful Collections provide a complete backend-like experience during frontend development. Unlike traditional endpoints that simulate responses, collections maintain state across requests with full CRUD operations, server-side pagination, and persistent storage.

Key Features

  • Full CRUD Operations
  • Server-Side Pagination
  • Persistent State
  • Branch Isolation
  • AI-Generated Seeds
  • Read-After-Write Consistency

Use Cases

  • • Admin dashboards
  • • E-commerce management
  • • User management systems
  • • Multi-tenant demos
  • • Data-heavy applications

How It Works

Collections store data in Supabase via edge functions, providing true server-side operations. When you create, update, or delete items, changes are persisted and immediately reflected in subsequent requests.

Quick Start

import { defineCollection, m, type Infer } from '@symulate/sdk';

// Define schema
const ProductSchema = m.object({
  id: m.uuid(),
  name: m.commerce.productName(),
  price: m.commerce.price(),
  inStock: m.boolean(),
  category: m.commerce.department(),
  createdAt: m.date(),
});

export type Product = Infer<typeof ProductSchema>;

// Define collection
export const products = defineCollection<Product>({
  name: 'products',
  basePath: '/api/products',
  schema: ProductSchema,
  seedCount: 20,  // Initial items to generate
  seedInstruction: 'Generate realistic e-commerce products',
});

That's it! The collection is now ready to use with full CRUD operations.

CRUD Operations

List Items

Retrieve paginated lists of items with optional filtering and sorting.

// Basic list
const response = await products.list();
console.log(response.data);        // Product[]
console.log(response.pagination);  // { page: 1, limit: 20, total: 20, totalPages: 1 }

// With pagination
const page2 = await products.list({ page: 2, limit: 10 });

// With sorting
const sorted = await products.list({
  sortBy: 'price',
  sortOrder: 'desc'
});

// With filtering
const filtered = await products.list({
  filter: { category: 'Electronics' }
});

Get Single Item

Retrieve a specific item by ID.

const product = await products.get('product-id-123');
console.log(product.name);  // Fully typed!

Create Item

Create new items with automatic ID and timestamp generation.

const newProduct = await products.create({
  name: 'Wireless Mouse',
  price: 29.99,
  inStock: true,
  category: 'Electronics'
  // id, createdAt auto-generated
});

console.log(newProduct.id);  // Auto-generated UUID

Auto-Generated Fields

Fields like id, createdAt, and updatedAt are automatically generated. You don't need to provide them when creating items.

Update Item (Partial)

Update specific fields while preserving others.

// Only update specified fields
const updated = await products.update('product-id-123', {
  price: 24.99,  // Only price changes
  inStock: false
});

// Other fields remain unchanged
console.log(updated.name);  // Original name preserved

Delete Item

Permanently remove items from the collection.

await products.delete('product-id-123');

// Item is permanently removed
const response = await products.list();
// product-id-123 no longer in the list

Configuration

PropertyTypeDescription
namestringCollection identifier (required)
basePathstringBase API path (required)
schemaBaseSchema<T>Data schema (required)
seedCountnumber?Initial items to generate (default: 20)
seedInstructionstring?AI instruction for seed data generation
operationsobject?Enable/disable specific CRUD operations

Selective Operations

You can disable specific CRUD operations if needed:

export const products = defineCollection<Product>({
  name: 'products',
  basePath: '/api/products',
  schema: ProductSchema,
  operations: {
    list: true,    // Enable list
    get: true,     // Enable get
    create: true,  // Enable create
    update: false, // Disable update
    delete: false, // Disable delete
  },
});

Branch Isolation

Collections support branch isolation for multi-tenant demos, testing environments, and customer-specific data separation.

Global Configuration

configureSymulate({
  collections: {
    branch: 'customer-acme'  // All operations use this branch
  }
});

// All collection operations now use 'customer-acme' branch
await products.list();   // Only returns products from 'customer-acme'
await products.create(); // Creates product in 'customer-acme'

Use Cases

Customer Demos

Create isolated datasets for each customer demo without interference.

// Demo for Customer A
configureSymulate({ collections: { branch: 'demo-customer-a' } });

// Demo for Customer B
configureSymulate({ collections: { branch: 'demo-customer-b' } });

Environment Separation

Separate data for development, staging, and production environments.

const branch = {
  development: 'dev',
  staging: 'staging',
  production: 'main'
}["production"];

configureSymulate({ collections: { branch } });

Testing Isolation

Create temporary datasets for testing without affecting main data.

// Set up test data
configureSymulate({ collections: { branch: 'test-suite-1' } });
await products.create({ name: 'Test Product' });

// Run tests...

// Clean up
npx symulate collections delete -b test-suite-1

Persistence Modes

Collections support three persistence modes for different use cases, from fast in-memory prototyping to full server-side state management.

Configuration

configureSymulate({
  collections: {
    persistence: {
      mode: 'local'  // 'memory', 'local', or 'cloud'
    }
  }
});

Mode Comparison

memory
Default

In-memory only - Fastest performance for quick prototyping

  • ✅ Fastest performance
  • ✅ Perfect for quick prototyping
  • ❌ Data lost on page refresh

Use for: Quick tests, demos where clean state is desired

local
Recommended for Development

localStorage (browser) or filesystem (Node.js) - Data survives refreshes

  • ✅ Data survives page refreshes
  • ✅ No backend required
  • ✅ Works in both browser and Node.js
  • ⚠️ ~5-10MB browser storage limit

Browser: Uses localStorage API

Node.js: Saves to .symulate-data.json file

Use for: Local development, offline testing, prototypes

cloud
Production-like

Server-side persistence via Supabase - True backend simulation

  • ✅ True server-side state
  • ✅ Multi-tenant with branch isolation
  • ✅ Shared across devices/sessions
  • ✅ Server-side pagination

Requires: Symulate API key and project ID

Use for: Realistic backend simulation, customer demos, team collaboration

Example Configurations

Local Development with Browser Persistence

configureSymulate({
  collections: {
    persistence: {
      mode: 'local'  // Survives page refresh
    }
  }
});

Production-like with Cloud Persistence

configureSymulate({
  symulateApiKey: 'sym_live_xxx',
  projectId: 'proj_xxx',
  collections: {
    persistence: {
      mode: 'cloud'  // Server-side with branch isolation
    },
    branch: 'customer-acme'
  }
});

Collections in Tenant Demos

Collections work seamlessly with Tenant Demos - isolated demo environments with pre-generated data that don't consume AI tokens during presentations. This is the recommended approach for customer demos, sales presentations, and trade shows.

Why Use Tenant Demos with Collections?

Traditional collections regenerate data on-demand using AI, which can be slow and inconsistent during live demos. Tenant demos pre-generate all collection data beforehand, ensuring instant responses and zero token consumption during presentations.

Traditional Collections vs. Tenant Demo Collections

AspectDevelopment ModeTenant Demo Mode
ConfigurationsymulateApiKey + projectIddemoApiKey only
Data GenerationOn-demand (first request)Pre-generated (before demo)
Token UsageConsumes tokens on generationZero tokens during demo
Response Time~2-10s first requestInstant (<100ms)
Data CustomizationseedInstruction in codeCustom instructions per demo
CRUD Operations✅ Full CRUD✅ Full CRUD (isolated)
Use CaseLocal developmentCustomer demos, sales

Using Collections with Demo API Keys

When you configure the SDK with a demoApiKey, all collection operations automatically route to pre-generated demo data:

import { configureSymulate } from '@symulate/sdk';
import { products, users } from './models';

// Configure with demo API key
configureSymulate({
  demoApiKey: 'sym_demo_abc123xyz789def456ghi',
  projectId: 'proj_xxx',  // Still required for routing
});

// All collection operations now use pre-generated demo data
const productList = await products.list();
// ✓ Returns pre-generated products (instant response)

const newProduct = await products.create({
  name: 'Demo Product',
  price: 99.99
});
// ✓ Creates in demo data (isolated from other demos)

const userList = await users.list();
// ✓ Returns pre-generated users with custom instructions

How It Works

When a demoApiKey is present, the SDK automatically routes all collection and endpoint calls to the tenant demo's pre-generated data instead of generating new data. This ensures consistent, fast responses perfect for live demonstrations.

Custom Instructions for Demo Collections

Tenant demos support a three-level custom instruction hierarchy, allowing you to generate customer-specific, localized, or industry-specific data for each demo.

1 General Instruction (Demo-Wide)

Applies to all collections and endpoints in the demo. Set when creating the demo in Platform UI.

Platform UI → Create Demo

General Instruction: "German automotive industry, luxury vehicles, Munich location"

Effect: All collections (products, users, orders) will use this context unless overridden.

2 Collection-Specific Instruction

Overrides general instruction for a specific collection. Set via Platform UI.

Collection: products

General: "German automotive industry, luxury vehicles"

Override: "Premium German car parts: BMWs, Mercedes, Audis, high-end accessories"

Effect: Products collection uses the specific instruction, while users and orders still use the general one.

3 Endpoint-Specific Instruction

Overrides both for a specific endpoint. Set via Platform UI.

Endpoint: GET /api/dashboard/stats

Collection: "Premium German car parts"

Override: "Show exceptional sales growth for Q4 2024, 45% increase YoY"

Effect: Only this endpoint uses the specific sales narrative, other endpoints use their respective instructions.

Instruction Priority

More specific instructions always override less specific ones. The hierarchy is: Endpoint-Specific > Collection-Specific > General. This allows maximum flexibility while maintaining consistency.

Creating Tenant Demos via Platform UI

The easiest way to create and manage tenant demos is through the Symulate Platform web interface:

  1. 1. Navigate to Dashboard → Demos
  2. 2. Click "Create Demo"
  3. 3. Fill in demo details:
    • • Demo name (e.g., "Customer Demo - ACME Corp")
    • • Description (optional)
    • • General custom instruction (optional)
  4. 4. Configure collections:
    • • Select which collections to include
    • • Set collection-specific custom instructions
    • • Adjust seed counts if needed
  5. 5. Configure endpoints:
    • • Select which endpoints to include
    • • Set endpoint-specific custom instructions
  6. 6. Generate demo data - The platform creates a generation job and pre-generates all data
  7. 7. Copy demo API key and use in your SDK configuration

Example SDK Configuration:

// After creating demo via Platform UI, use the generated API key:
configureSymulate({
  demoApiKey: 'sym_demo_abc123xyz789def456ghi',  // From Platform UI
  projectId: 'proj_xxx',
});

// All calls now use pre-generated demo data
await products.list();  // Instant response with custom data

Demo Workflow Example

1
Before Customer Demo

Create tenant demo via Platform UI with customer-specific instructions:

  • • Name: "Demo - Toyota Berlin"
  • • Instruction: "Japanese automotive company, Toyota vehicles, Berlin Germany location"
  • • Collections: products (50), users (20), orders (100)
2
Configure Application
configureSymulate({
  demoApiKey: 'sym_demo_toyota_berlin_xxx',
  projectId: 'proj_xxx',
});
3
During Demo

All operations are instant and use customer-specific data:

  • ✓ Products show Toyota-specific automotive parts
  • ✓ Users have German names and Berlin addresses
  • ✓ CRUD operations work in isolated demo environment
  • ✓ Zero latency, zero token consumption
4
After Demo (Optional)

Regenerate with updated instructions via Platform UI:

  • • Go to Dashboard → Demos
  • • Click refresh icon on your demo
  • • Update general instruction if needed
  • • Trigger re-generation

Switching Between Development and Demo Mode

You can easily switch between development mode (with on-demand generation) and demo mode (with pre-generated data) by changing the SDK configuration:

// Development mode - generate data on-demand
configureSymulate({
  symulateApiKey: 'sym_live_xxx',
  projectId: 'proj_xxx',
  collections: {
    persistence: { mode: 'cloud' }
  }
});

// Demo mode - use pre-generated data
configureSymulate({
  demoApiKey: 'sym_demo_abc123xyz',
  projectId: 'proj_xxx',
  // collections config ignored when demoApiKey is present
});

// You can use environment variables for dynamic switching
const isDemoMode = process.env.DEMO_MODE === 'true';

configureSymulate(
  isDemoMode
    ? {
        demoApiKey: process.env.DEMO_API_KEY,
        projectId: process.env.PROJECT_ID,
      }
    : {
        symulateApiKey: process.env.SYMULATE_API_KEY,
        projectId: process.env.PROJECT_ID,
        collections: { persistence: { mode: 'cloud' } }
      }
);

Best Practices for Tenant Demo Collections

  • Pre-generate before presentations - Don't rely on on-demand generation during live demos
  • Use custom instructions - Tailor data to customer's industry, location, and use case
  • Test CRUD operations - Ensure create/update/delete work as expected in demo environment
  • Monitor generation jobs - Check status before demo to ensure all data is ready
  • Create separate demos per customer - Avoid mixing data from different customers
  • Regenerate periodically - Keep demo data fresh and relevant

Learn More

For more details on creating and managing tenant demos, see the Tenant Demos documentation.

Query Parameter Customization

Customize query parameter names and routing for pagination, sorting, and filtering to match your backend API conventions.

Default Behavior

By default, collections automatically add these query parameters to list operations:

  • Pagination: page, limit
  • Sorting: sortBy, sortOrder
  • Filtering: filter
// These work out of the box
await products.list({ page: 1, limit: 20, sortBy: 'price', sortOrder: 'desc' });
// GET /api/products?page=1&limit=20&sortBy=price&sortOrder=desc

Role-Based Parameter Customization

Use the params array with semantic roles to customize parameter names and routing:

defineCollection({
  name: 'products',
  basePath: '/api/products',
  schema: ProductSchema,
  operations: {
    list: {
      params: [
        // Custom pagination parameter names
        { name: 'pageNumber', location: 'query', role: 'pagination.page', schema: m.number() },
        { name: 'pageSize', location: 'query', role: 'pagination.limit', schema: m.number() },

        // Custom sorting parameter names
        { name: 'orderBy', location: 'query', role: 'sort.field', schema: m.string() },
        { name: 'direction', location: 'query', role: 'sort.order', schema: m.string() },

        // Custom filter parameter name
        { name: 'search', location: 'query', role: 'filter', schema: m.object({ category: m.string() }) },
      ]
    }
  }
});

// Internal API stays the same
await products.list({ page: 1, limit: 20, sortBy: 'price', sortOrder: 'desc' });

// But generates URL with custom parameter names:
// GET /api/products?pageNumber=1&pageSize=20&orderBy=price&direction=desc

Available Parameter Roles

RolePurposeDefault Name
pagination.pageCurrent page numberpage
pagination.limitItems per pagelimit
sort.fieldField to sort bysortBy
sort.orderSort direction (asc/desc)sortOrder
filterFilter criteriafilter

Parameter Locations

Control WHERE parameters are sent using the location property:

defineCollection({
  name: 'products',
  operations: {
    list: {
      params: [
        // Send in query string (default)
        { name: 'page', location: 'query', role: 'pagination.page', schema: m.number() },

        // Send in request body (switches to POST)
        { name: 'filter', location: 'body', role: 'filter', schema: m.object({ category: m.string() }) },

        // Send as HTTP header
        { name: 'X-Sort-By', location: 'header', role: 'sort.field', schema: m.string() },
      ]
    }
  }
});

// Usage
await products.list({ page: 1, filter: { category: 'Electronics' }, sortBy: 'price' });
// POST /api/products?page=1
// Headers: X-Sort-By: price
// Body: { "filter": { "category": "Electronics" } }

Available Locations

query(default)

Send as URL query string parameter (e.g., ?page=1&limit=20)

bodyPOST

Send in request body. Automatically switches method from GET to POST.

header

Send as HTTP header (useful for custom headers like X-Page, X-Sort-By)

Disabling Query Parameters

You can disable automatic query parameters globally or per-operation:

Global Disable

configureSymulate({
  collections: {
    disableQueryParams: true  // Disables for ALL collections
  }
});

// No query parameters added
await products.list({ page: 1, limit: 20 });
// GET /api/products (no query params)

Per-Operation Disable

defineCollection({
  name: 'products',
  operations: {
    list: {
      disableQueryParams: true  // Disables only for this operation
    }
  }
});

Common API Conventions

Laravel / Spring Boot Style

params: [
  { name: 'page', location: 'query', role: 'pagination.page', schema: m.number() },
  { name: 'per_page', location: 'query', role: 'pagination.limit', schema: m.number() },
  { name: 'sort', location: 'query', role: 'sort.field', schema: m.string() },
  { name: 'order', location: 'query', role: 'sort.order', schema: m.string() },
]

ASP.NET Core Style

params: [
  { name: 'pageNumber', location: 'query', role: 'pagination.page', schema: m.number() },
  { name: 'pageSize', location: 'query', role: 'pagination.limit', schema: m.number() },
  { name: 'orderBy', location: 'query', role: 'sort.field', schema: m.string() },
  { name: 'sortOrder', location: 'query', role: 'sort.order', schema: m.string() },
]

Django Style

params: [
  { name: 'page', location: 'query', role: 'pagination.page', schema: m.number() },
  { name: 'page_size', location: 'query', role: 'pagination.limit', schema: m.number() },
  { name: 'ordering', location: 'query', role: 'sort.field', schema: m.string() },
  { name: 'search', location: 'query', role: 'filter', schema: m.string() },
]

Automatic POST Conversion

When any parameter uses location: 'body', the request method automatically changes from GET to POST. This is because GET requests with request bodies are non-standard and not supported by most APIs.

Production Mode Only

Query parameter customization currently applies only to production mode (environment: 'production') when calling your real backend API.

CLI Management

Manage collections via CLI commands for inspection, deletion, and pre-generation.

List Collections

# List all collections
npx symulate collections list

# List collections for specific branch
npx symulate collections list -b customer-acme

Example Output:

   Found 2 collection(s):

   • products
     └─ main (20 items)
     └─ customer-acme (15 items)

   • users
     └─ main (10 items)

Delete Collections

# Delete specific collection
npx symulate collections delete -n products

# Delete from specific branch
npx symulate collections delete -n products -b dev

# Delete all collections (requires confirmation)
npx symulate collections delete --all

Safety Confirmation

When using --all, you must type "DELETE ALL" to confirm deletion across all branches.

Pre-generate Collections

Generate all defined collections before starting your application.

# Pre-generate on main branch
npx symulate collections pregenerate

# Pre-generate for specific branch
npx symulate collections pregenerate -b demo

Example Output:

   Found 3 collection(s) to generate

     Generating "products"...
       ✅ Generated 20 items
     Generating "users"...
       ✅ Generated 10 items
     Generating "orders"...
       ✅ Generated 15 items

Custom Instructions for Multi-Tenant Demos

Custom instructions for data generation are ONLY available in Tenant Demos (created via Platform UI). Development pre-generation uses the seedInstruction defined in your collection schemas.

For customer-specific, localized, or industry-specific demo data, create a Tenant Demo with custom instructions via the Platform UI.

Pagination & Filtering

Pagination

Server-side pagination returns only the requested page, not all data.

const response = await products.list({
  page: 2,
  limit: 10
});

console.log(response.pagination);
// {
//   page: 2,
//   limit: 10,
//   total: 50,      // Total items across all pages
//   totalPages: 5   // Total number of pages
// }

Sorting

// Sort by price (descending)
const expensive = await products.list({
  sortBy: 'price',
  sortOrder: 'desc'
});

// Sort by name (ascending)
const alphabetical = await products.list({
  sortBy: 'name',
  sortOrder: 'asc'
});

Filtering

// Filter by category
const electronics = await products.list({
  filter: { category: 'Electronics' }
});

// Multiple filters
const filtered = await products.list({
  filter: {
    category: 'Electronics',
    inStock: true
  }
});

Combined Query

const result = await products.list({
  page: 1,
  limit: 20,
  sortBy: 'price',
  sortOrder: 'desc',
  filter: {
    category: 'Electronics',
    inStock: true
  }
});

Collection Metadata & Aggregates

Use m.collectionsMeta to include pagination metadata and aggregate functions in your response schemas. These fields are automatically calculated and populated by the server.

Pagination Metadata

Include pagination information in your list operation responses:

const ListResponseSchema = m.object({
  data: m.array(ProductSchema),
  pagination: m.object({
    page: m.collectionsMeta.page(),
    limit: m.collectionsMeta.limit(),
    total: m.collectionsMeta.total(),
    totalPages: m.collectionsMeta.totalPages(),
  }),
});

export const products = defineCollection({
  name: 'products',
  schema: ProductSchema,
  operations: {
    list: {
      responseSchema: ListResponseSchema
    }
  }
});

// Response automatically includes pagination metadata
const response = await products.list({ page: 2, limit: 20 });
console.log(response.pagination);
// {
//   page: 2,
//   limit: 20,
//   total: 150,
//   totalPages: 8
// }

Aggregate Functions

Calculate statistics across all items in the collection (not just the current page):

Available Aggregates

  • m.collectionsMeta.avg(fieldName) - Average value
  • m.collectionsMeta.sum(fieldName) - Sum of all values
  • m.collectionsMeta.min(fieldName) - Minimum value
  • m.collectionsMeta.max(fieldName) - Maximum value
  • m.collectionsMeta.count(fieldName, value) - Count items matching value
const ProductSchema = m.object({
  id: m.uuid(),
  name: m.string(),
  price: m.number(),
  category: m.string(),
  inStock: m.boolean(),
});

const ProductStatsSchema = m.object({
  data: m.array(ProductSchema),
  pagination: m.object({
    page: m.collectionsMeta.page(),
    limit: m.collectionsMeta.limit(),
    total: m.collectionsMeta.total(),
    totalPages: m.collectionsMeta.totalPages(),
  }),
  stats: m.object({
    averagePrice: m.collectionsMeta.avg("price"),
    totalValue: m.collectionsMeta.sum("price"),
    cheapest: m.collectionsMeta.min("price"),
    mostExpensive: m.collectionsMeta.max("price"),
    inStockCount: m.collectionsMeta.count("inStock", true),
  }),
});

export const products = defineCollection({
  name: 'products',
  schema: ProductSchema,
  seedCount: 100,
  operations: {
    list: {
      responseSchema: ProductStatsSchema
    }
  }
});

// Aggregates calculated across ALL 100 products, not just current page
const response = await products.list({ page: 1, limit: 10 });
console.log(response.stats);
// {
//   averagePrice: 89.99,
//   totalValue: 8999.00,
//   cheapest: 9.99,
//   mostExpensive: 499.99,
//   inStockCount: 67
// }

Advanced Example: Analytics Dashboard

Combine metadata and aggregates for powerful analytics:

const OrderSchema = m.object({
  id: m.uuid(),
  customerId: m.string(),
  total: m.number(),
  status: m.string("pending, processing, completed, cancelled"),
  createdAt: m.date(),
});

const OrderAnalyticsSchema = m.object({
  orders: m.array(OrderSchema),
  pagination: m.object({
    page: m.collectionsMeta.page(),
    limit: m.collectionsMeta.limit(),
    total: m.collectionsMeta.total(),
    totalPages: m.collectionsMeta.totalPages(),
  }),
  analytics: m.object({
    // Revenue metrics
    totalRevenue: m.collectionsMeta.sum("total"),
    averageOrderValue: m.collectionsMeta.avg("total"),
    largestOrder: m.collectionsMeta.max("total"),

    // Order status counts
    completedOrders: m.collectionsMeta.count("status", "completed"),
    pendingOrders: m.collectionsMeta.count("status", "pending"),
    cancelledOrders: m.collectionsMeta.count("status", "cancelled"),
  }),
});

export const orders = defineCollection({
  name: 'orders',
  schema: OrderSchema,
  seedCount: 500,
  operations: {
    list: {
      responseSchema: OrderAnalyticsSchema
    }
  }
});

// Get page 1 with complete analytics
const dashboard = await orders.list({ page: 1, limit: 25 });

console.log(`Showing ${dashboard.orders.length} of ${dashboard.pagination.total} orders`);
console.log(`Total Revenue: $${dashboard.analytics.totalRevenue.toFixed(2)}`);
console.log(`Average Order: $${dashboard.analytics.averageOrderValue.toFixed(2)}`);
console.log(`Completed: ${dashboard.analytics.completedOrders}`);
console.log(`Pending: ${dashboard.analytics.pendingOrders}`);

Key Features

  • ✅ Aggregates across entire collection
  • ✅ Auto-updated on create/update/delete
  • ✅ Works with all persistence modes
  • ✅ Zero configuration required
  • ✅ Full TypeScript type inference

Common Use Cases

  • • Dashboard statistics
  • • E-commerce analytics
  • • Reporting endpoints
  • • Admin panels
  • • Real-time metrics

Collections vs. Traditional Endpoints

FeatureTraditional EndpointsStateful Collections
PurposeSimulated responsesFull backend simulation
State❌ No persistence✅ Server-side persistence
CRUD❌ Static mock data✅ Full CRUD operations
Pagination❌ Client-side (all data loaded)✅ Server-side (only requested page)
Consistency❌ Data resets on reload✅ Data persists across requests
Branch Support❌ No✅ Multi-tenant isolation
Best ForSimple GET requestsAdmin dashboards, data management

When to Use Each

  • Use Endpoints: Simple read-only data fetching, static content, landing pages
  • Use Collections: Admin panels, CRUD operations, data management, multi-tenant demos

Complete Examples

E-Commerce Product Management

import { defineCollection, m, type Infer } from '@symulate/sdk';

const ProductSchema = m.object({
  id: m.uuid(),
  name: m.commerce.productName(),
  description: m.lorem.paragraph(),
  price: m.commerce.price(),
  category: m.commerce.department(),
  inStock: m.boolean(),
  imageUrl: m.internet.avatar(),
  createdAt: m.date(),
  updatedAt: m.date(),
});

export type Product = Infer<typeof ProductSchema>;

export const products = defineCollection<Product>({
  name: 'products',
  basePath: '/api/products',
  schema: ProductSchema,
  seedCount: 50,
  seedInstruction: 'Generate diverse tech products and gadgets',
});

// Usage in your app
async function productDashboard() {
  // List with pagination
  const page1 = await products.list({ page: 1, limit: 20 });

  // Create new product
  const newProduct = await products.create({
    name: 'Wireless Headphones',
    description: 'High-quality wireless headphones',
    price: 79.99,
    category: 'Audio',
    inStock: true,
    imageUrl: 'https://picsum.photos/200/300'
  });

  // Update stock status
  await products.update(newProduct.id, {
    inStock: false
  });

  // Delete product
  await products.delete(newProduct.id);
}

User Management System

const UserSchema = m.object({
  id: m.uuid(),
  name: m.person.fullName(),
  email: m.email(),
  role: m.string("Role: admin, user, or guest"),
  status: m.string("Status: active, inactive, or suspended"),
  lastLogin: m.date(),
  createdAt: m.date(),
});

export type User = Infer<typeof UserSchema>;

export const users = defineCollection<User>({
  name: 'users',
  basePath: '/api/users',
  schema: UserSchema,
  seedCount: 100,
  seedInstruction: 'Generate diverse company employees',
});

// Usage
async function userManagement() {
  // Get active admins
  const admins = await users.list({
    filter: {
      role: 'admin',
      status: 'active'
    },
    sortBy: 'name'
  });

  // Suspend user
  await users.update('user-id', {
    status: 'suspended'
  });

  // Search by email (pagination)
  const results = await users.list({
    page: 1,
    limit: 10,
    filter: { email: 'john@example.com' }
  });
}

Multi-Tenant Customer Demos

// Configure for Customer A
configureSymulate({
  symulateApiKey: 'sym_live_xxx',
  projectId: 'proj_xxx',
  collections: {
    branch: 'demo-customer-a'
  }
});

// All operations are isolated to Customer A's demo
await products.create({ name: 'Customer A Product' });
const customerAProducts = await products.list();

// Switch to Customer B
configureSymulate({
  collections: {
    branch: 'demo-customer-b'
  }
});

// Completely separate data
await products.create({ name: 'Customer B Product' });
const customerBProducts = await products.list();

// Customer A and B data never mix!

Ready to Build with Collections?

Check out our CLI reference or API documentation for more details.