# Production deployment

Deploy your dcupl application to production with this checklist and the dcupl-specific parts of a deployment: the production setup, API keys, data loading and refresh, long-running servers, and the Content Security Policy. For error tracking and monitoring, see [Production Debugging](/sdk/guides/production-debugging).

## Pre-deployment checklist

### Code quality

- [ ] All quality validation errors resolved
- [ ] TypeScript compilation passes without errors
- [ ] Tests passing, including the data quality checks ([Testing](/sdk/guides/testing))
- [ ] The results of `init()` and `update()` are checked (they resolve `false` instead of throwing)

### Configuration

- [ ] `config.version` names the version you published to (the default is `draft`)
- [ ] Quality validation disabled, or kept on on purpose to report data errors
- [ ] CDN URLs verified

### Performance

- [ ] `@dcupl/core` is bundled once, in one version (each entry bundles its dependencies, so tree shaking can't trim the SDK)
- [ ] Data loading strategy defined
- [ ] Caching strategy implemented
- [ ] Large dataset handling tested

### Security

- [ ] The browser bundle carries only a scoped read-only key
- [ ] The CLI key and other keys that can write are only in server environments and CI secrets
- [ ] The project is private: a request for one of its files without one of the project's keys answers `403`. New projects are private by default; for an older project, private file serving is part of a paid plan that dcupl turns on for your project; ask your dcupl contact ([What the API Key Protects](#what-the-api-key-protects))
- [ ] Content Security Policy allows the dcupl hosts

## Production configuration

### Optimized dcupl setup

```typescript filename="src/config/production.ts"
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';

export function createProductionDcupl() {
  const dcupl = new Dcupl({
    config: {
      projectId: process.env.DCUPL_PROJECT_ID!,
      apiKey: process.env.DCUPL_API_KEY!,
      version: 'production', // The version you published to; the default is 'draft'
    },
    quality: {
      enabled: false, // Disable for performance
    },
  });

  const loader = new DcuplAppLoader();
  dcupl.loaders.add(loader);

  return { dcupl, loader };
}

export async function initializeProduction() {
  const { dcupl, loader } = createProductionDcupl();

  try {
    // Fetch published configuration
    await loader.config.fetch();
    await loader.process();

    // init() doesn't throw: it logs the error and resolves false
    if (!(await dcupl.init())) throw new Error('dcupl.init() failed');

    return dcupl;
  } catch (error) {
    console.error('Failed to initialize dcupl:', error);
    throw error;
  }
}
```

`loader.config.fetch()` throws when the loader configuration can't be fetched. A model or data file that fails to download does **not** make `loader.process()` or `init()` return `false`: the failure is logged and recorded as a loader error (`type: 'loader'`, e.g. `InvalidResource`) in `dcupl.quality.errors.summary()`, even with quality disabled. Check for those (or for `failedItems` in the loader progress, see [Capture Loader Errors](/sdk/guides/production-debugging#capture-loader-errors)) before you treat the instance as ready. [Load from the console](/sdk/guides/load-from-console) covers the project ID, the key, the version and what each fetch error means.

### Environment variables

```bash filename=".env.production"
# Production Configuration
DCUPL_PROJECT_ID=prod-project-id
DCUPL_API_KEY=prod-api-key-xxx
NODE_ENV=production
```

In a browser app, every variable the bundle reads ends up in the bundle: see [What the API Key Protects](#what-the-api-key-protects) for which key goes there.

## Performance optimization

### 1. Disable quality validation

Quality validation adds overhead. Disable in production:

```typescript
const dcupl = new Dcupl({
  quality: {
    enabled: false, // Production: disabled
  },
});
```

**Trade-off:** with quality disabled, `dcupl.quality.errors.summary()` returns only loader errors (resources that failed to load) and the `$DcuplErrorTrackingErrors` model does not exist. Keep it enabled if you report data-quality errors from production (see [Production Debugging](/sdk/guides/production-debugging)), and measure the difference on your own data.

### 2. Use partial updates

Avoid full data reloads when possible:

```typescript
// Bad: Full reload
dcupl.data.set(allData, { model: 'Product' });
await dcupl.init();

// Good: Partial update — only the changed attributes and what depends on them are reprocessed
dcupl.data.update([{ key: 'p1', price: 999 }], { model: 'Product' });
await dcupl.update();
```

`data.update()` only changes items that already exist (unknown keys are ignored); use `data.upsert()` to add new items in the same call.

`update()` falls back to a full rebuild when a `data.set()` or `data.reset()` is part of the batch, when a model definition changed after `init()` (`models.set()`, `models.update()` or `models.remove()`), when the changes can alter the result of a [query reference](/sdk/api/models#reference), or when more than half of the models **and** more than half of their items changed. An `update()` with nothing staged also re-runs the full build. Both thresholds are configurable, see [Partial Updates](/sdk/advanced/performance#partial-updates).

### 3. Implement list pooling

Reuse lists instead of creating new ones:

```typescript
class ListPool {
  private pool: Map<string, DcuplList[]> = new Map();

  get(dcupl: Dcupl, modelKey: string): DcuplList {
    const pool = this.pool.get(modelKey) || [];
    if (pool.length > 0) {
      return pool.pop()!;
    }
    return dcupl.lists.create({ modelKey });
  }

  release(list: DcuplList) {
    list.catalog.query.clear();
    const modelKey = list.modelKey;
    const pool = this.pool.get(modelKey) || [];
    pool.push(list);
    this.pool.set(modelKey, pool);
  }
}
```

### 4. Use pagination

For large result sets:

```typescript
const productList = dcupl.lists.create({ modelKey: 'Product' });

// Page 1, 50 items (reasonable page size)
productList.catalog.query.applyOptions({ start: 0, count: 50 });
const page = productList.catalog.query.items();

// Total number of matches (count() returns the page length)
const total = productList.catalog.fn.metadata().currentSize;
```

### 5. Lazy load non-critical data

Load the resources the first screen needs, render, then load the rest in the background. Tag the resources in your loader configuration and process them by tag. A tag filter skips untagged resources, so tag the model resources too:

```typescript
// Resources tagged 'critical' (the models and the first data), then the app can render
await loader.process({ resourceTags: ['critical'] });
if (!(await dcupl.init())) throw new Error('dcupl.init() failed');
renderApp(dcupl);

// The rest in the background, applied with update()
void loader.process({ resourceTags: ['optional'] }).then(() => dcupl.update());
```

A data resource replaces all rows of its model by default (`options.updateType` is `'set'`). When an optional resource feeds the same model as a critical one, give it `options: { updateType: 'upsert' }`. See [Partial Data Loading](/sdk/guides/error-handling#partial-data-loading) for a version that reports failed resources.

## Error handling

### Graceful initialization

```typescript
export async function initWithFallback() {
  const { dcupl, loader } = createProductionDcupl();

  try {
    await loader.config.fetch();
    await loader.process();
    if (!(await dcupl.init())) throw new Error('dcupl.init() failed');
    return { dcupl, status: 'ready' };
  } catch (error) {
    console.error('dcupl initialization failed:', error);

    // Option 1: Return degraded state
    return { dcupl: null, status: 'error', error };

    // Option 2: Retry with exponential backoff
    // return retryInit();
  }
}
```

### Retry logic

```typescript
async function retryInit(maxRetries = 3): Promise<Dcupl> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    // A fresh instance per attempt: the failed attempt's loader errors would
    // otherwise stay in dcupl.quality.errors.summary()
    const { dcupl, loader } = createProductionDcupl();
    try {
      await loader.config.fetch();
      await loader.process();
      if (!(await dcupl.init())) throw new Error('dcupl.init() failed');
      return dcupl;
    } catch (error) {
      console.error(`Init attempt ${attempt} failed:`, error);
      dcupl.destroy();

      if (attempt === maxRetries) {
        throw new Error(`Failed after ${maxRetries} attempts`);
      }

      // Exponential backoff
      await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt - 1)));
    }
  }
  throw new Error('Unreachable');
}
```

### Query error handling

```typescript
function safeQuery(list: DcuplList) {
  try {
    return list.catalog.query.items();
  } catch (error) {
    console.error('Query failed:', error);
    return []; // Return empty array as fallback
  }
}
```

## Monitoring

[Production Debugging](/sdk/guides/production-debugging) covers error tracking with Sentry, startup and query timings, loader failures and data-quality alerts. For a server, add a health check:

### Health check endpoint

```typescript
// Express/Node.js example

// Liveness: the process is up
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// Readiness: dcupl is initialized and has data
app.get('/health/dcupl', (req, res) => {
  try {
    const list = dcupl.lists.create({ modelKey: 'Product' }); // throws when the model is missing
    const count = list.catalog.query.count();
    list.destroy();
    if (count === 0) throw new Error('No products loaded');

    res.json({
      status: 'healthy',
      productCount: count,
      timestamp: new Date().toISOString(),
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: (error as Error).message,
      timestamp: new Date().toISOString(),
    });
  }
});
```

`/health/dcupl` answers `503` when the model is missing or has no items, for example after a failed load. Use it to take the instance out of rotation (a readiness check), not to restart it: a restart doesn't fix a data problem.

## Deployment strategies

### Static site (Vercel, Netlify)

```typescript filename="src/lib/dcupl.ts"
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';

let dcuplInstance: Dcupl | null = null;

export async function getDcupl(): Promise<Dcupl> {
  if (dcuplInstance) {
    return dcuplInstance;
  }

  const dcupl = new Dcupl({
    config: {
      projectId: process.env.NEXT_PUBLIC_DCUPL_PROJECT_ID!,
      // NEXT_PUBLIC_ variables are inlined into the browser bundle: a scoped read-only key only
      apiKey: process.env.NEXT_PUBLIC_DCUPL_API_KEY!,
      version: 'production',
    },
    quality: { enabled: false },
  });

  const loader = new DcuplAppLoader();
  dcupl.loaders.add(loader);

  await loader.config.fetch();
  await loader.process();
  // Don't cache a failed instance: init() resolves false instead of throwing
  if (!(await dcupl.init())) throw new Error('dcupl.init() failed');

  dcuplInstance = dcupl;
  return dcupl;
}
```

### Node.js server and SSR

On a server, build one instance per process and share it between requests. `init()` parses every model and data file, so an instance per request is slow:

```typescript filename="src/server/dcupl.ts"
import { Dcupl } from '@dcupl/core';
import { DcuplAppLoader } from '@dcupl/loader';

let instance: Promise<{ dcupl: Dcupl; loader: DcuplAppLoader }> | null = null;

// One instance per process, shared by every request
export function getDcupl() {
  instance ??= (async () => {
    const dcupl = new Dcupl({
      config: {
        projectId: process.env.DCUPL_PROJECT_ID!,
        apiKey: process.env.DCUPL_API_KEY!, // stays on the server
        version: 'production',
      },
    });
    const loader = new DcuplAppLoader();
    dcupl.loaders.add(loader);
    await loader.config.fetch();
    await loader.process();
    if (!(await dcupl.init())) throw new Error('dcupl.init() failed');
    return { dcupl, loader };
  })().catch((error) => {
    instance = null; // let the next request try again
    throw error;
  });
  return instance;
}

// Call it after you publish new data (from a webhook or a timer).
// Reloading only the resources tagged 'data' skips the model files;
// a plain process() also reloads the model files; update() applies their changes.
export async function refreshData(): Promise<boolean> {
  const { dcupl, loader } = await getDcupl();
  await loader.config.fetch(); // also renews the ?v= version parameter of CDN urls
  await loader.process({ resourceTags: ['data'] });
  return dcupl.update();
}
```

```typescript title="Per request"
app.get('/products', async (req, res) => {
  const { dcupl } = await getDcupl();
  const list = dcupl.lists.create({ modelKey: 'Product' });
  try {
    list.catalog.query.addCondition({ attribute: 'category', operator: 'eq', value: String(req.query.category) });
    res.json(list.catalog.query.items({ count: 50 }));
  } finally {
    list.destroy(); // lists stay registered in dcupl.lists until destroyed
  }
});
```

- **New data:** a running instance never picks up data you publish later. Reload the data resources as above ([Problem: Stale Data](/sdk/guides/production-debugging#problem-stale-data)). If the model files changed too, call `loader.process()` without `resourceTags` in `refreshData()`: it reloads the model files as well, and the `dcupl.update()` after it applies the changed models to the running instance, so you need no new instance.
- **Lists:** a list created per request stays in `dcupl.lists` until `list.destroy()`. For a one-off query, `dcupl.query.execute({ modelKey, queries })` needs no list.
- **Shared results:** results are deep clones by default (`performance.clone`), so a handler that changes a result doesn't change the shared instance. Keep cloning on for a shared instance.
- **Runtime:** the loader uses the global `fetch` (see [Compatibility](/sdk/compatibility#nodejs) for the Node.js versions). The packages work with both `import` and `require()`.

### Docker container

```dockerfile filename="Dockerfile"
FROM node:24-alpine

WORKDIR /app

# Copy package files
COPY package*.json ./
RUN npm ci

# Copy app files
COPY . .

# Build, then drop the dev dependencies the build needed
RUN npm run build && npm prune --omit=dev

# Set environment
ENV NODE_ENV=production

# Run
CMD ["npm", "start"]
```

Pass `DCUPL_PROJECT_ID` and `DCUPL_API_KEY` as environment variables when you start the container, not as `ENV` lines in the image.

### Kubernetes

The dcupl-specific parts of a deployment are the key from a secret and the two health checks:

```yaml filename="k8s/deployment.yaml"
# spec.template.spec of the Deployment
containers:
  - name: app
    image: your-registry/dcupl-app:latest
    env:
      - name: DCUPL_PROJECT_ID
        valueFrom:
          secretKeyRef:
            name: dcupl-secrets
            key: project-id
      - name: DCUPL_API_KEY
        valueFrom:
          secretKeyRef:
            name: dcupl-secrets
            key: api-key
    livenessProbe:
      httpGet:
        path: /health
        port: 3000
    readinessProbe:
      httpGet:
        path: /health/dcupl
        port: 3000
      initialDelaySeconds: 10
      periodSeconds: 10
```

## Security considerations

### What the API key protects

The loader sends the API key with every request to the dcupl CDN, as an `api-key` query parameter next to the `?v=` version parameter. In a browser app, anyone who opens the network tab can read it, and environment variables don't change that: the build inlines them into the bundle. So treat a key that ships to the browser as public. It identifies the project and, on a private project, decides which files can be read. It doesn't hide the data the app loads from the people who use the app.

- **Browser apps** use a scoped read-only key whose file read patterns (`files.read`) cover only what the app loads. The key new projects get for browser apps reads `dcupl.lc.json`, `models/**`, `data/**`, `transformers/**` and `operators/**`. It leaves out `scripts/**` on purpose: the scripts of your REST APIs and workflows live there. Don't add `scripts/**` to a key that ships in a browser: an app that loads `script` resources needs its own scoped key, with read patterns for just the scripts it loads. The SDK readonly key works too, but it reads every file of the project. See [API keys](/console/api-keys).
- **Keys that can write**, the CLI key and scoped keys with file write patterns (`files.write`), stay on the server and in CI secrets. Never put them in a browser bundle or a `NEXT_PUBLIC_` / `VITE_` variable.
- **A private project** serves its files only to requests that carry one of the project's keys. On a public project, anyone who knows the URLs can read the files, with or without a key.

New projects are private by default and get a scoped read-only key for browser apps. A project created before projects became private by default stays public: private file serving for it is part of a paid plan that dcupl turns on for your project; ask your dcupl contact. See [API keys](/console/api-keys).

On a server (Node.js, SSR, Docker), the key stays in the server's environment:

```typescript
// Never hardcode API keys
const dcupl = new Dcupl({
  config: {
    projectId: process.env.DCUPL_PROJECT_ID!, // From environment
    apiKey: process.env.DCUPL_API_KEY!, // From environment
  },
});
```

### Secrets storage

The CLI key belongs in your CI provider's secret store. The CLI reads it from `DCUPL_API_KEY`:

```yaml filename=".github/workflows/deploy.yml"
# GitHub Actions example
- name: Deploy
  env:
    DCUPL_PROJECT_ID: ${{ secrets.DCUPL_PROJECT_ID }}
    DCUPL_API_KEY: ${{ secrets.DCUPL_API_KEY }}
  run: npm run deploy
```

See [CI/CD integration](/cli/ci-cd) for pushing files from CI.

### Content Security Policy

The dcupl part of a Content Security Policy:

```typescript
// Next.js example
const cspHeader = `
  default-src 'self';
  script-src 'self' 'unsafe-eval';
  connect-src 'self' https://cdn.dcupl.com https://api.dcupl.com;
`;
```

- `connect-src`: `cdn.dcupl.com` serves your files. `api.dcupl.com` answers the version check the loader makes before loading from the dcupl CDN; it sets the `?v=` parameter that keeps browsers and caches from serving old files. Add the hosts of any other resource URLs in your loader configuration.
- `'unsafe-eval'` is only needed when the app runs scripts, custom operators or loader transformers: dcupl compiles them with `new Function`. Leave it out when the app has none, when you turn scripting off with `scripting: { enabled: false }` (the loader then skips those resources with a `ScriptingDisabled` error), or when you pass your own compiler as `scripting.compile` ([Dcupl API](/sdk/api/dcupl)).

## Post-deployment checklist

### Immediate checks

- [ ] Application loads without errors
- [ ] dcupl initialization succeeds
- [ ] `dcupl.quality.errors.summary()` has no loader errors (`type: 'loader'`)
- [ ] Queries return expected results
- [ ] Health check endpoint responds

### Ongoing monitoring

- [ ] Error tracking configured, including console errors ([Production Debugging](/sdk/guides/production-debugging))
- [ ] Alerting for initialization failures
- [ ] A data refresh after each publish, for long-running servers

### Rollback plan

- [ ] Previous version tagged and deployable
- [ ] Rollback procedure documented

## Troubleshooting production issues

[Common Production Problems](/sdk/guides/production-debugging#common-production-problems) covers the debug steps and messages for data that doesn't load, slow initialization, memory and stale data. The dcupl-specific checks first:

- **Data doesn't load:** check `failedItems` in the loader progress and the `InvalidResource` errors in `summary()`. A `403` on a private project means the key is missing, isn't a key of the project, or doesn't cover that path.
- **Old data after a publish:** check that `config.version` names the version you published to (the default is `draft`), and reload the data in a long-running process ([Problem: Stale Data](/sdk/guides/production-debugging#problem-stale-data)).
- **Memory grows:** check the number of lists (`dcupl.lists.getAll().length`) and destroy lists you no longer need.

## Next steps

- [Production Debugging](/sdk/guides/production-debugging) — Error tracking and monitoring
- [Performance Guide](/sdk/advanced/performance) — Optimization strategies
- [API keys](/console/api-keys) — Key types and scoped read patterns
- [Common mistakes](/sdk/guides/common-mistakes) — Avoid common pitfalls
