Hybrid Data Loading
Use the Loader for base configuration from Console, but add local models or data at runtime. This pattern is useful for user-specific data, real-time updates, and A/B testing.
Prerequisites
- Completed Going to Production
- A project configured in Console
When to Use Hybrid Loading
- User-specific data – Preferences, cart items, session data
- Real-time updates – WebSocket data merged with static catalog
- A/B testing – Override specific items for test groups
- Local-first – Offline-capable apps with sync
Basic Pattern
Load from Console first, then add runtime data:
app.ts
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';
const dcupl = new Dcupl({
config: {
projectId: 'your-project-id',
apiKey: 'your-api-key',
},
});
const loader = new DcuplAppLoader();
dcupl.loaders.add(loader);
// 1. Load base data from Console
await loader.config.fetch();
await loader.process({ applicationKey: 'default' });
// 2. Add runtime-only model (not in Console)
dcupl.models.set({
key: 'UserPreferences',
properties: [
{ key: 'theme', type: 'string' },
{ key: 'language', type: 'string' },
],
});
dcupl.data.set(getUserPreferences(), { model: 'UserPreferences' });
// 3. Initialize
await dcupl.init();Updating Existing Data
Use upsert() to merge fresh data with Console-loaded data:
app.ts
// Console loaded 1000 products
// API returns 50 updated products
const freshProducts = await fetch('/api/products/updates').then((r) => r.json());
// Upsert merges: updates existing, adds new
dcupl.data.upsert(freshProducts, { model: 'Product' });
await dcupl.update();Real-Time Overlay
Add WebSocket updates on top of Console data:
app.ts
// Initial load from Console
await loader.config.fetch();
await loader.process({ applicationKey: 'default' });
await dcupl.init();
// Real-time updates via WebSocket
const ws = new WebSocket('wss://api.example.com/products');
ws.onmessage = async (event) => {
const update = JSON.parse(event.data);
if (update.type === 'price_change') {
dcupl.data.update(
[
{
key: update.productId,
price: update.newPrice,
},
],
{ model: 'Product' }
);
await dcupl.update();
}
};User Session Data
Keep user data separate from catalog data:
app.ts
// Define user session model (runtime only)
dcupl.models.set({
key: 'CartItem',
properties: [
{ key: 'productKey', type: 'string' },
{ key: 'quantity', type: 'int' },
],
references: [{ key: 'product', model: 'Product', property: 'productKey' }],
});
// Load from localStorage
const savedCart = JSON.parse(localStorage.getItem('cart') || '[]');
dcupl.data.set(savedCart, { model: 'CartItem' });
await dcupl.init();
// Query cart with product details via reference
const cartList = dcupl.lists.create({ modelKey: 'CartItem' });
const cartItems = cartList.catalog.query.items();
// Each item has .product reference resolvedPrecedence Rules
When the same item key exists in both Console and runtime data:
set()– Replaces all data (runtime wins completely)update()– Merges fields (runtime fields override Console fields)upsert()– Same as update, but also adds new items
// Console has: { key: 'p1', name: 'Laptop', price: 999 }
// This MERGES (keeps name from Console)
dcupl.data.update([{ key: 'p1', price: 899 }], { model: 'Product' });
// Result: { key: 'p1', name: 'Laptop', price: 899 }
// This REPLACES (loses name)
dcupl.data.set([{ key: 'p1', price: 899 }], { model: 'Product' });
// Result: { key: 'p1', price: 899 }Next Steps
- Data Management – All data operations
- Real-time Updates – WebSocket patterns
- Going to Production – Loader setup basics