Deployment

Remote Sync

Sync data from a remote UDL server for local development

Overview

Remote sync allows a local UDL server to fetch and mirror data from a production UDL server. This is useful for local development when you want to work with real production data without configuring CMS credentials locally.

Architecture

┌─────────────────────────────────────────────────────────────────────────┐
│                         PRODUCTION SERVER                               │
│  ┌─────────────┐     ┌─────────────┐     ┌─────────────────────────┐   │
│  │ Contentful  │────▶│             │────▶│                         │   │
│  │ Shopify     │     │   Plugins   │     │       Node Store        │   │
│  │ Custom CMS  │     │             │     │                         │   │
│  └─────────────┘     └─────────────┘     └───────────┬─────────────┘   │
│                                                      │                  │
│                                          ┌───────────┴───────────┐      │
│                                          │                       │      │
│                                     /_sync              /ws (optional)  │
│                                          │                       │      │
└──────────────────────────────────────────┼───────────────────────┼──────┘
                                           │                       │
                                           │    Initial Fetch      │  Real-time
                                           │    (all nodes)        │  Updates
                                           │                       │
                                           ▼                       ▼
┌──────────────────────────────────────────────────────────────────────────┐
│                          LOCAL DEV SERVER                                │
│                                                                          │
│  ┌─────────────────────────────────────────────────────────────────┐    │
│  │                         Node Store                               │    │
│  │                    (mirror of production)                        │    │
│  └─────────────────────────────────────────────────────────────────┘    │
│                                    │                                     │
│                                    ▼                                     │
│                              /graphql                                    │
│                                    │                                     │
│                                    ▼                                     │
│                           Your Application                               │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

How It Works

Step 1: Initial Sync

When the local server starts with remote.url configured:

Local Server                              Remote Server
     │                                          │
     │  GET /_sync?since=1970-01-01T00:00:00Z   │
     │─────────────────────────────────────────▶│
     │                                          │
     │         { updated: [...], deleted: [] }  │
     │◀─────────────────────────────────────────│
     │                                          │
     │  Populate local node store               │
     │                                          │

The local server requests all nodes by using the epoch timestamp. The remote server returns every node in its store.

Step 2: WebSocket Connection (Optional)

If the remote server has WebSockets enabled, the local server connects for real-time updates:

Local Server                              Remote Server
     │                                          │
     │  WebSocket: ws://remote/ws               │
     │─────────────────────────────────────────▶│
     │                                          │
     │  { type: "subscribe", data: "*" }        │
     │─────────────────────────────────────────▶│
     │                                          │
     │  { type: "subscribed", types: [...] }    │
     │◀─────────────────────────────────────────│
     │                                          │

Step 3: Real-time Updates

When content changes on the remote server, updates flow automatically:

CMS Webhook                Remote Server                Local Server
     │                           │                            │
     │  Content Updated          │                            │
     │──────────────────────────▶│                            │
     │                           │                            │
     │                           │  { type: "node:updated",   │
     │                           │    nodeId: "...",          │
     │                           │    data: {...} }           │
     │                           │───────────────────────────▶│
     │                           │                            │
     │                           │          Update local store│
     │                           │                            │

Configuration

Basic Setup

udl.config.ts
import { defineConfig } from 'universal-data-layer';

export const config = defineConfig({
  port: 4000,
  remote: {
    url: 'https://production-udl.example.com',
  },
});

What Gets Skipped

When remote.url is set, the local server:

  • Skips plugin loading (no sourceNodes calls)
  • Skips webhook handlers (remote handles those)
  • Keeps local GraphQL endpoint active
  • Keeps local codegen working (if configured)

Shared Config Files

You can use the same config file for both production and local development. UDL automatically detects which mode to use by checking if the remote server is reachable:

udl.config.ts
export const config = defineConfig({
  port: 4000,
  plugins: ['./plugins/my-source'],
  remote: {
    url: 'http://localhost:4000',
  },
});

When production starts first:

Production                           Local Dev (not started yet)
     │
     │  Check: Is http://localhost:4000 reachable?
     │  ❌ No (nothing running yet)
     │
     │  Load plugins, source data
     │  Start server on :4000
     │

When local dev starts after:

Production (running on :4000)        Local Dev
                                          │
                                          │  Check: Is http://localhost:4000 reachable?
                                          │  ✅ Yes (production is running)
                                          │
                                          │  Sync from remote instead of loading plugins
                                          │

This works for any setup:

  • Same machine, same port (production starts first)
  • Same machine, different ports
  • Different machines (local dev connects to remote production)
  • Multi-tenant setups on localhost

Use Cases

Local Development with Production Data

Work with real content without CMS credentials:

udl.config.ts
export const config = defineConfig({
  remote: {
    url: process.env.PRODUCTION_UDL_URL,
  },
  codegen: {
    output: './generated',
  },
});

CI/CD Preview Builds

Preview deployments can sync from production:

udl.config.ts
export const config = defineConfig({
  remote: {
    // Production UDL for preview builds
    url: process.env.CI ? process.env.PRODUCTION_UDL_URL : undefined,
  },
  plugins: process.env.CI ? [] : [
    // Local dev uses plugins directly
    { name: '@universal-data-layer/plugin-source-contentful', options: {...} },
  ],
});

Staging Environments

Staging can mirror production data:

Production UDL ──────▶ Staging UDL (remote sync)
       │
       └─────────────▶ Local Dev (remote sync)

WebSocket Behavior

The WebSocket connection is optional and graceful:

Remote Server StateLocal Server Behavior
WebSocket enabledConnects and receives real-time updates
WebSocket disabledWorks fine, just no real-time updates
WebSocket unavailableLogs message, continues without it

To enable WebSocket on the production server:

udl.config.ts (production)
export const config = defineConfig({
  plugins: [...],
  remote: {
    websockets: {
      enabled: true,
    },
  },
});

Comparison: Remote Sync vs Local Plugins

AspectRemote SyncLocal Plugins
CMS credentialsNot needed locallyRequired locally
Data freshnessMirrors productionDirect from CMS
Webhook handlingRemote serverLocal server
Network dependencyRequires remote UDLRequires CMS APIs
Setup complexityMinimalRequires plugin config

Troubleshooting

Connection Refused

Error: Failed to fetch from remote UDL: ECONNREFUSED
  • Verify the remote URL is correct
  • Check if the remote server is running
  • Ensure network/firewall allows the connection

Empty Node Store

If no nodes are loaded:

  1. Check remote server has data: curl https://remote/_sync?since=1970-01-01T00:00:00Z
  2. Verify the response contains updated array with nodes
  3. Check for errors in local server logs

WebSocket Not Connecting

This is normal if the remote server doesn't have WebSocket enabled. The local server will log:

📡 Remote WebSocket not available (this is fine if remote has websockets disabled)

Next Steps