# 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 and real-time updates.

## Prerequisites

- Completed [Setup options](/sdk/setup-options)
- A project configured in [Console](/console)

## When to use hybrid loading

- **User-specific data** – Preferences, cart items, session data
- **Real-time updates** – WebSocket data merged with static catalog

dcupl keeps all data in memory. Persisting runtime data (for example in `localStorage`) and syncing it back to a server is up to your application.

## Basic pattern

Load from Console first, then add runtime data:

```typescript filename="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 (process() without options loads every resource)
await loader.config.fetch();
await loader.process();

// 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([{ key: 'prefs', theme: 'dark', language: 'en' }], { model: 'UserPreferences' });

// 3. Initialize (init() doesn't fetch anything itself, so it runs after loader.process())
await dcupl.init();
```

The sections below build on this instance. After the first `init()`, new models and data only need `await dcupl.update()`; don't call `init()` again.

## Updating existing data

Use `upsert()` to merge fresh data with Console-loaded data:

```typescript filename="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:

```typescript filename="app.ts"
// After the initial load and init() from the Basic Pattern

// 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:

```typescript filename="app.ts"
// Define user session model (runtime only)
dcupl.models.set({
  key: 'CartItem',
  properties: [
    { key: 'productKey', type: 'string' },
    { key: 'quantity', type: 'int' },
  ],
  // origin: the reference reads the product key from the productKey field
  references: [{ key: 'product', type: 'singleValued', model: 'Product', origin: 'productKey' }],
});

// Load from localStorage
const savedCart = JSON.parse(localStorage.getItem('cart') || '[]');
dcupl.data.set(savedCart, { model: 'CartItem' });

// The instance is already initialized: apply the new model and its data
await dcupl.update();

// Query cart with product details via reference
const cartList = dcupl.lists.create({ modelKey: 'CartItem' });

// A reference comes back as { key } by default; project it to get the product's fields
const cartItems = cartList.catalog.query.items({
  projection: { $: true, product: { $: true } },
});
// [{ key: 'c1', productKey: 'p1', quantity: 2, product: { key: 'p1', name: 'Laptop', price: 999 } }, ...]
```

## Precedence rules

When the same item key exists in both Console and runtime data:

1. **`set()`** – Replaces all data of the model (runtime wins completely; items you don't pass are removed)
2. **`update()`** – Merges fields into existing items (fields you send replace the stored values; unknown keys are ignored)
3. **`upsert()`** – Same as update, but also adds new items

All three are staged until the next `await dcupl.update()`.

To refresh the Console data later, process only the data resources, for example by tag, and apply them:

```typescript
await loader.process({ resourceTags: ['data'] }); // only resources tagged 'data'
await dcupl.update();
```

A plain `loader.process()` works too. It reloads the model files as well and rebuilds each model in place from them (properties are not duplicated, a changed type applies, a removed attribute disappears), and it drops the resources that left the loader configuration. The lists your app already holds, plain or scoped, serve the new data after `await dcupl.update()`, so you need no new lists or instance. Processing only the data resources by tag skips downloading the model files again. Models you defined in code, such as `UserPreferences`, keep their definition and rows.

Loader data resources are applied as `set` by default, so the refresh replaces every runtime change to those models, such as WebSocket prices. With `options: { updateType: 'upsert' }` on the resource, runtime items and attributes the resource doesn't contain stay, but the values it contains still overwrite yours. Either way, re-apply your overlay after a refresh (see [Processing](/sdk/api/loader#processing)).

```typescript
// Console has: { key: 'p1', name: 'Laptop', price: 999 }, plus p2 and p3

// This MERGES (keeps name from Console)
dcupl.data.update([{ key: 'p1', price: 899 }], { model: 'Product' });
await dcupl.update();
// p1: { key: 'p1', name: 'Laptop', price: 899 }

// This REPLACES all Product data (p1 loses name, p2 and p3 are removed)
dcupl.data.set([{ key: 'p1', price: 899 }], { model: 'Product' });
await dcupl.update();
// Products: [{ key: 'p1', price: 899 }]
```

## Next steps

- [Data management](/sdk/core-concepts/data-management) — All data operations
- [Real-time updates](/sdk/advanced/realtime-updates) — WebSocket patterns
- [Setup options](/sdk/setup-options) — Loader setup basics
