# Cloud sync & configuration

Connect your local workspace to a dcupl Console project, then keep the two in sync. The CLI tracks what changed on each side and transfers only the differences — pushes and pulls stay fast, predictable, and never silently clobber edits made on the other side.

> **Note:** **Do this first.** Every `files` command needs a project ID and API key. Create a project in the [dcupl Console](https://console.dcupl.com) and copy its **CLI** key (type CLI read/write, see [API keys](/console/api-keys)): `status`, `push` and `pull` reject every other key type. Then run `dcupl config set` once — see **Configure credentials** below. Without credentials, a command exits 1 with ``Missing projectId. Set it via `dcupl config set`, the DCUPL_PROJECT_ID env var, or a .env file.`` (or the same message for `apiKey`); a key the server refuses gives ``API key rejected (…). Check your key with `dcupl config set --api-key <key>` …``.

## How sync works

Sync is a **3-way diff**: the CLI compares your local files, the server, and the last-synced *baseline* to decide what to push, pull, or flag as a conflict. Toggle the mode to see how deletions are handled.

Additive: only new and changed files transfer — nothing is ever deleted.

| File | Local | Server | Action |
| --- | --- | --- | --- |
| `models/Product.dcupl.json` | modified | unchanged | Push |
| `workflows/sync.workflow-v3.json` | added | absent | Push (new) |
| `models/Order.dcupl.json` | unchanged | modified | Pull |
| `data/products.csv` | modified | modified | Conflict — stops |
| `data/legacy.csv` | deleted | present | Kept on server |

## The status → review → push loop

`files status` is read-only — it transfers nothing. Review the diff, then push.

```console title="zsh — cloud sync"
# see what differs between local and server — read-only
$ dcupl files status --json
{
  "success": true,
  "versionId": "draft",
  "hasBaseline": true,
  "uploads": ["models/Product.dcupl.json", "workflows/sync.workflow-v3.json"],
  "downloads": ["models/Order.dcupl.json"],
  "conflicts": [{ "path": "data/products.csv", "reason": "both changed" }],
  "wouldDeleteOnServerInStrictMode": ["data/legacy.csv"],
  "wouldDeleteLocallyInStrictMode": [],
  "unchanged": 0,
  "needsRepair": []
}

# a conflict stops the push — resolve it by choosing a winner
$ dcupl files push --force --path data/products.csv
[1/1] ✓ upload data/products.csv
Pushed to version: draft
Scope: data/products.csv

Uploaded (1)
  ✓ data/products.csv
Force-resolved conflicts (1)
  ! data/products.csv — both changed

Unchanged: 0

# now push the rest
$ dcupl files push --json
{
  "success": true,
  "versionId": "draft",
  "uploaded": ["models/Product.dcupl.json", "workflows/sync.workflow-v3.json"],
  "deleted": [],
  "failed": [],
  "conflicts": [],
  "resolvedConflicts": [],
  "unchanged": 2,
  "unverified": []
}
```

## Configure credentials

Run `dcupl config set` once per workspace. Interactive by default; it writes the project ID to `dcupl.config.json` (workspace config) and your API key to `dcupl.secrets.json` when that file exists (a `dcupl init` workspace has one), otherwise to `.env` as `DCUPL_API_KEY`, adding `.env` to `.gitignore`. Pass flags to skip the prompts for scripts and CI.

```bash
# interactive — prompts for project id + API key
dcupl config set

# non-interactive, for scripts and CI
dcupl config set --project-id my-project --api-key <api-key> --yes

# inspect resolved config (key redacted as ***last4)
dcupl config get
dcupl config get --show-secrets
```

> **Note:** **Credential precedence.** The CLI resolves each value in this order: **command flag**, where the command has one (`--api-key` / `--project-id` for a remote source on `dcupl validate`, `dcupl app create --load` and `dcupl app loaders add`; the `files` commands have none) → environment variable (`DCUPL_API_KEY`, `DCUPL_PROJECT_ID`) → a workspace `.env` (real environment variables win over it) → the stored `dcupl.secrets.json` / `dcupl.config.json`. CI should inject the key via the env var or `dcupl config set --api-key`, never a committed file. `consoleApiUrl` has no environment variable — set it with `dcupl config set --console-api-url <url>`.

> **Watch out:** Gitignore `dcupl.secrets.json` before your first commit — it holds your API key (the `.gitignore` of a new `dcupl init` folder already lists it and `.env`). `dcupl config set` adds `.env` to `.gitignore` itself when it writes the key there, and the CLI writes `.dcupl/.gitignore` (ignoring everything in `.dcupl/`) together with the sync state, so that state is never committed.

### config set flags

| Flag | Purpose |
| --- | --- |
| `--project-id <str>` | Console project identifier |
| `--api-key <str>` | API key for the project |
| `--console-api-url <str>` | Console API base URL (self-hosted only) |
| `--yes` | Non-interactive; accept defaults and skip prompts |

### Config files

| File | Contents | Commit? |
| --- | --- | --- |
| `dcupl.config.json` | Workspace config (project id, baseFolder, paths, filesUpload include/exclude) | Yes |
| `dcupl.secrets.json` | API key | No — gitignore |
| `.env` | DCUPL\_API\_KEY (written by config set when there is no secrets file; loaded at startup) | No — config set adds it to .gitignore |
| `dcupl/dcupl.lc.json` | Loader config (models, sources) | Yes |
| `.dcupl/sync-state/` | Local sync ledger | No — ignored by .dcupl/.gitignore, which the CLI writes |

Single-file operations (`read`, `write`, `delete`, `move`, `copy`, `create-folder`) and version management (`files versions list`, `copy`, `delete`) are listed with every flag in the [Cloud sync commands](/cli/commands#files).

## Resolving conflicts

When both sides changed the same file, sync reports the conflict and stops — there is no auto-merge. Choose a winner with `--force`: `push --force` keeps local and overwrites the server; `pull --force` takes the server and overwrites local.

```bash
# both sides edited data/products.csv
dcupl files status
# => Conflicts (1)
#      ! data/products.csv — both changed
# (a push now aborts with PUSH_CONFLICTS and exits 1)

# keep your local copy (overwrites the server)
dcupl files push --force --path data/products.csv

# OR take the server copy (overwrites local)
dcupl files pull --force --path data/products.csv
```

## Version resolution

Most commands accept `--version` (or `-v`). The CLI resolves the target version in this order:

1. 1 The explicit `--version` flag, if passed
2. 2 The current HEAD recorded in your sync-state
3. 3 A fallback of `draft`

Only `files pull` sets HEAD: on the first pull, and when `pull --version` switches to another version (`--dest` leaves it alone). Push never moves it, so a workspace that has never pulled pushes to `draft`. A switch needs a clean workspace or `--force`; otherwise the pull aborts with `PULL_DIRTY_SWITCH`.

> **Watch out:** Pushing to `production` is rejected. Push to another version, then promote it by copying it: `dcupl files versions copy --from draft --to production` (this replaces the contents of `production`).

## What can be synced

`status`, `push` and `pull` only see files under the workspace's `baseFolder` (default `dcupl/`; paths are relative to it) that match `filesUpload.include` and not `filesUpload.exclude` in `dcupl.config.json`. `dcupl init`, and `dcupl config set` when it creates the file, set the include to `**/*.json`, `**/*.csv` and `**/*.js`; add patterns there to sync other allowed files. Run the commands from the workspace root (the folder that holds `dcupl.config.json`). In a folder without one, they sync `dcupl/` with the include patterns `**/*.json`, `**/*.csv` and `**/*.js` and print a notice on stderr, so `--json` output stays one JSON value.

The dcupl CDN stores **text files only**. Sync accepts `.csv`, `.json`, `.txt`, `.md` and `.js` (scripts, operators, and transformers ship as `.js`). Any other extension is rejected before a request is sent, and a file whose bytes are not valid UTF-8 is rejected rather than uploaded with mangled content — that applies to `files push` and to `files write --file` alike. A rejected file does not stop the rest of the batch; the run exits non-zero.

Large files are handled for you. `push` uploads files of up to 500 MB each. A larger file fails with `File '<path>' is … MB — larger than the 500 MB per-file upload limit.` To fix it, split the file. Or leave it out of the push: add it to `filesUpload.exclude` in `dcupl.config.json`, or push only the paths you name with `--path`. On slow links, use `--concurrency` to lower the default of 4 parallel transfers.

## Sync commands

| Command | What it does |
| --- | --- |
| `dcupl files status [--version <id>] [--path <p>...]` | Show local vs. server differences. Read-only. |
| `dcupl files push [--version <id>] [--path <p>...] [--strict] [--yes] [--force] [--concurrency <n>]` | Upload new and changed files |
| `dcupl files pull [--version <id>] [--path <p>...] [--strict] [--yes] [--force] [--dest <path>] [--concurrency <n>]` | Download new and changed files (--dest: sandbox copy, no sync state change) |
| `dcupl files list [--search <str>] [--tree]` | List server files, filtered or as a tree |

Single-file operations (`read`, `write`, `delete`, `move`, `copy`, `create-folder`) and version management (`files versions list`, `copy`, `delete`) are listed with every flag in the [Cloud sync commands](/cli/commands#files).

## Next steps

- [Workflows](/cli/workflows) — Author, test, and deploy templates
- [CI/CD integration](/cli/ci-cd) — Automate sync in your pipeline
- [Validating a project](/cli/validate) — Health-check before you push
- [Commands](/cli/commands) — The complete CLI command reference
