Skip to main content

writing-data-transfer-config

Use when writing or editing a @webiny/data-transfer config file (ddb.transfer.config.ts / os.transfer.config.ts / custom.transfer.config.ts). Covers createDdbConfig / createOsConfig signatures, credential shapes (fromAwsProfile vs literal), fromEnv / numberFromEnv helpers, loadEnv, source/target collision + trimming rules, pointing at a preset, tuning knobs.

Source facts

Repository
webiny/data-transfer
Last source activity
May 11, 2026 at 20:47
Detected SKILL.md language
English
Stars
1
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
writing-data-transfer-config
description
Use when writing or editing a @webiny/data-transfer config file (ddb.transfer.config.ts / os.transfer.config.ts / custom.transfer.config.ts). Covers createDdbConfig / createOsConfig signatures, credential shapes (fromAwsProfile vs literal), fromEnv / numberFromEnv helpers, loadEnv, source/target collision + trimming rules, pointing at a preset, tuning knobs.
# Writing a `@webiny/data-transfer` config A config is a `.ts` file that `export default`s one of the two factory calls: - **`createDdbConfig(...)`** — DDB primary table (+ S3 files). Covers CMS + security + file manager + tenancy. - **`createOsConfig(...)`** — OpenSearch companion DDB table. CMS entries only, gzipped records. Both validate with Zod at import time — invalid configs fail fast with a useful message, before any AWS call. ## Minimal shape ```ts import { loadEnv, createDdbConfig, fromAwsProfile, fromEnv, numberFromEnv } from "@webiny/data-transfer"; loadEnv(import.meta.url); export default createDdbConfig({ source: { region: fromEnv("SOURCE_REGION", "us-east-1"), credentials: fromAwsProfile({ profile: fromEnv("SOURCE_PROFILE", "default") }), dynamodb: { tableName: fromEnv("SOURCE_DDB_TABLE") }, s3: { bucket: fromEnv("SOURCE_S3_BUCKET") } }, target: { region: fromEnv("TARGET_REGION", "us-east-1"), credentials: fromAwsProfile({ profile: fromEnv("TARGET_PROFILE", "default") }), dynamodb: { tableName: fromEnv("TARGET_DDB_TABLE") }, s3: { bucket: fromEnv("TARGET_S3_BUCKET") } }, pipeline: { preset: "v5-to-v6-ddb", // built-in, OR "./presets/my-preset.ts" segments: numberFromEnv("SEGMENTS", 4) } }); ``` ## Credentials — three accepted shapes The `credentials` field accepts any of the three. Pick what matches where the transfer runs; mixing is fine across source/target. ### A) Profile — explicit (`fromAwsProfile`) ```ts credentials: fromAwsProfile({ profile: fromEnv("SOURCE_PROFILE", "default") }) ``` Reads `~/.aws/credentials` using the named profile (re-export of `fromIni`; renamed because "ini" leaks the implementation detail). **Use locally** when you have multiple AWS accounts — explicit profile selection prevents a stray `AWS_ACCESS_KEY_ID` in your shell from silently overriding the wrong account. ### B) Default credential chain — flexible (`fromAwsCredentialChain`) ```ts credentials: fromAwsCredentialChain() ``` Runs the AWS SDK's default resolution: env vars → shared credentials file → SSO / web identity → EC2/ECS IAM role. **Use in CI / cloud** where the credentials source depends on the deploy target, or when you want one config that "just works" locally AND on a build agent AND on an IAM-instance-profile box. ### C) Literal credentials from env — explicit strings ```ts credentials: { accessKeyId: fromEnv("SOURCE_AWS_ACCESS_KEY_ID"), secretAccessKey: fromEnv("SOURCE_AWS_SECRET_ACCESS_KEY"), // sessionToken: fromEnv("SOURCE_AWS_SESSION_TOKEN") // only for temporary STS creds } ``` Use when your CI injects credentials as env vars directly, when you have temporary STS credentials to pass in, or when you prefer creds in a single `.env` file over a shared profile. ### Picking between them | Scenario | Pick | | --- | --- | | Local dev, multiple AWS accounts | **A** — `fromAwsProfile` | | CI with IAM role or env-based creds | **B** — `fromAwsCredentialChain` | | CI that injects `AWS_*` env vars explicitly | **B** or **C** | | One-config-works-everywhere | **B** — `fromAwsCredentialChain` | | Temporary STS creds (session token) | **C** — literal object | ## `fromEnv(name, default?)` Reads `process.env[name]` as a string. Throws if the variable is unset OR empty when no default is provided. Empty-string counts as missing because `KEY=` in a `.env` file is almost always a forgotten value. ```ts region: fromEnv("SOURCE_REGION", "us-east-1"), // has default tableName: fromEnv("SOURCE_DDB_TABLE"), // no default → throws if missing ``` ## `numberFromEnv(name, default?)` Same contract, but parses via `Number(...)`. Throws if the variable is set but not parseable (`SEGMENTS=four` → named error, not silent `NaN`). ```ts segments: numberFromEnv("SEGMENTS", 4), ``` ## `loadEnv(import.meta.url)` Loads the `.env` file **next to the config file** (not the one at the repo root). Using `import.meta.url` anchors the lookup to THIS file's directory — so running from the repo root with `--config=./projects/X/...` still loads `projects/X/.env`. Every project should have its own `.env`. ## Config validation rules Enforced by Zod at build time: - **All string fields are trimmed** (`region`, `tableName`, `bucket`, `endpoint`, creds, preset). A trailing-space paste error doesn't silently corrupt anything. - **Whitespace-only rejected** — empty-after-trim is treated as missing. - **Source/target collision guard**: - Same S3 bucket on both sides → rejected (would overwrite source files). - Same region + same DDB / OS-DDB table name → rejected (would read and write to the same table). Same table name across DIFFERENT regions is allowed — distinct physical tables. ## Pointing at a preset `pipeline.preset` takes one of: - **A built-in name**: `"v5-to-v6-ddb"` or `"v5-to-v6-os"` (filename in `src/presets/` without extension). The runner auto-discovers built-ins. - **A file path**: `"./presets/my-preset.ts"` or `"../shared/presets/foo.ts"`. Resolved relative to the CONFIG file's directory. ## `pipeline.modelsDir` — CMS model definitions Required by the OS preset (`v5-to-v6-os`) and by built-in transformers that inspect field types (`fixBrokenStorageKeys`, `transformRichText`, `addLiveField`). Point at a directory of exported model definitions. ```ts pipeline: { preset: "v5-to-v6-os", modelsDir: fromEnv("MODELS_DIR", "./models") } ``` Three JSON shapes are accepted and can be mixed freely in the same directory: | Shape | Example | |-------|---------| | Single model | `{ "modelId": "blog", "fields": [...], ... }` | | Array of models | `[{ "modelId": "blog", "fields": [...] }, ...]` | | Webiny admin export | `{ "groups": [...], "models": [...] }` | The Webiny admin panel's **Export** button produces the `{groups, models}` shape. JSON models override DB-loaded models (user-provided definition takes precedence over what was scanned from the source DDB table). ## Snapshot / debug (optional) ```ts debug: { snapshot: true // or: snapshot: { dir: "./my-snapshot", compress: false } } ``` Dumps every record the pipeline touches to local JSONL files. Useful for diffing source vs post-transform on a specific record without re-scanning AWS. Layout (one file per shard per pipeline per category): - `<dir>/<pipelineName>/segment-<n>.source.jsonl.gz` — post-filter, pre-transform records. - `<dir>/<pipelineName>/segment-<n>.post-transform.jsonl.gz` — after the transformer chain + onEnd. - `<dir>/<pipelineName>/segment-<n>.commands.jsonl.gz` — every emitted command (PutRecord, S3Copy, custom). - `<dir>/dropped/segment-<n>.jsonl.gz` — records that matched no pipeline filter. Default `dir`: `.transfer/<runId>/snapshot`. Default `compress`: `true`. Best-effort — write errors log `warn` but never fail the transfer. **Snapshot files may contain production data.** The default `.transfer/` location is gitignored in this repo. If you override `dir` to a path outside `.transfer/`, add your override path to `.gitignore` yourself — these files typically contain full source records + transformed records + emitted commands, which are usually not things you want committed. ### Persistent log file (`debug.logFile`) ```ts debug: { logFile: true // default: .transfer/<runId>/logs/<orchestrator|segment-N>.log // or: logFile: "./my-transfer.log" } ``` Writes raw pino JSONL to disk in addition to stdout. With `true`, each process gets its own file (orchestrator + one per worker), so concurrent appends can't interleave. With a string path, all processes append to the same file — fine for low-throughput orchestrator logs, risky when worker parallelism is high. Post-run inspection: `cat .transfer/<runId>/logs/*.log | pino-pretty`. Default path is under `.transfer/` (gitignored in this repo). Custom paths are the user's gitignore responsibility. ## Tuning (optional) ```ts tuning: { flushEvery: numberFromEnv("FLUSH_EVERY", 500), // records per shard flush — bounds peak memory ddb: { maxRetries: 3, initialBackoffMs: 100 }, s3: { concurrency: 10, maxRetries: 3, initialBackoffMs: 100 }, os: { maxRetries: 3, retryScheduleMs: [5000, 10000, 20000, 30000, 30000], gzipConcurrency: 16 } } ``` All optional; absent = built-in defaults. AWS SDK `retryMode: "adaptive"` is always on for DDB + S3 — it self-tunes backoff based on real throttle signals, so you usually don't need to tune these. **`flushEvery`** caps peak per-shard memory. The runner calls `processor.execute()` every N records and resets the pending-commands buffer. Default 500 (≈ 5 MB at a 10 KB average record). Lower to 100 for tables with very large records (approaching the 400 KB DDB max). ## Running it From the user project root: ```bash yarn transfer --config=./projects/<name>/ddb.transfer.config.ts ``` Or with a specific AWS profile pre-set in `.env`: ``` SOURCE_PROFILE=prod-reader TARGET_PROFILE=staging-writer ``` ## Common patterns - **DDB first, then OS** — run them as separate transfers with separate config files. They don't share state. DDB uses `v5-to-v6-ddb`; OS uses `v5-to-v6-os`. Both can share the same `.env`. - **Multiple target environments** — duplicate the project folder under `projects/` with different `.env`. Configs stay identical. - **Custom preset** — if the built-in doesn't match your needs, write one (see `writing-data-transfer-preset` skill) and point `pipeline.preset` at its file path. ## Anti-patterns - **`process.env.X!` bang-casting** — loses the friendly "variable not set" error, and silently passes `undefined` to the AWS SDK on typos. Use `fromEnv(name)` or `fromEnv(name, default)` instead. - **Hardcoded credentials in the config file** — `.env*` is gitignored; put secrets there. If `.env` ever grows a key that shouldn't be in source, make sure `.gitignore` covers it. - **Source and target point at the same table** — rejected at build time, but worth re-checking manually before a real run: different regions OR different table names.
View on GitHub