Use cases

Migrating from Neosync to PrivaCI

Neosync was archived on August 30, 2025 after the team was acquired by Grow Therapy. The hosted cloud is offline; the self-hosted version still runs but receives no updates and will diverge from PostgreSQL's release cadence over time.

Before going further: Neosync did two distinct things — it anonymized real production data for staging environments, and it generated synthetic data from scratch based on a schema description. PrivaCI covers the first workflow only. If your Neosync jobs connected to a production database, masked PII columns, and wrote a sanitized copy to staging or CI — that is what this guide addresses. If you were generating rows from scratch against an empty database without a production source, PrivaCI does not replace that workflow.

For the anonymization path — the common neosync alternative for teams leaving Neosync — copy production, mask PII, preserve FK integrity, ship to a lower environment — the migration is straightforward.

How Neosync jobs were configured

Neosync's anonymization jobs were not defined in a local file you committed to version control. They lived in Neosync's own storage, configured either through its web dashboard (the visual job builder with per-column transformer selectors) or through its API. Teams using Terraform could define jobs declaratively via the neosync_job provider resource; the CLI's neosync.yaml file was for connection parameters (API URL, API key) only, not for column-level transform rules.

Within a job, each column was assigned a named transformer type — things like transform_email, transform_full_name, generate_null — selected from Neosync's transformer library. For cases that needed custom logic, Neosync ran the Goja JavaScript engine inside its Go worker, so you could write a transform_javascript transformer with an arbitrary expression.

PrivaCI's equivalent is a mask-rules.yaml file checked into your repository alongside your application code. The column-level concept is the same; the delivery mechanism moves from a managed service's database to a YAML file you own.

Translating transformer types to mask-rules.yaml

The table below maps the Neosync transformer types teams most commonly used in anonymization jobs to their mask-rules.yaml equivalents. For the full provider list, see the configuration reference.

Neosync transformer type PrivaCI action
transform_email { action: fake, provider: email }
transform_full_name { action: fake, provider: full_name }
generate_first_name { action: fake, provider: first_name }
generate_last_name { action: fake, provider: last_name }
transform_phone_number { action: fake, provider: phone }
generate_ssn { action: fake, provider: ssn }
generate_street_address { action: fake, provider: street }
generate_null { action: null }
generate_string { action: fake, provider: company } — or { action: hash } for an opaque stable value
passthrough (omit the column — passthrough is the default)
transform_javascript (simple pattern redaction) { action: regex_mask, pattern: "..." }
transform_javascript (row conditionals) CEL when: on the column action (Standard / Compliance)
transform_javascript (cross-column / arbitrary JS) No direct equivalent — review in dry-run
generate_uuid (PK / serial column) Not needed — identity and serial columns are re-synced automatically
generate_uuid (non-PK UUID column to anonymize) { action: fake, provider: uuid }

A note on transform_javascript: regex_mask covers simple pattern redaction (masking characters at a fixed offset, for example). Row-conditional masking maps to a CEL when: guard on the column action (Standard and Compliance). Cross-column references or arbitrary hashing in JavaScript still have no direct YAML equivalent — flag those columns during dry-run and decide on a static rule or a hash.

Determinism works differently between the two tools. Neosync seeded its generators from row identity internally, with no explicit secret surface. PrivaCI uses a global_salt you supply at runtime, combined with the row's primary key values. The outcome is the same — the same source row always produces the same masked output — but the salt is yours to own and rotate.

A mask-rules.yaml for a typical SaaS user and payments schema:

# mask-rules.yaml
version: "1.0"
global_salt: env://ANONYMIZATION_SALT
strict_autodetect: true
on_existing_data: fail

tables:
  public.users:
    strategy: transform
    columns:
      email:         { action: fake, provider: email }
      full_name:     { action: fake, provider: full_name }
      phone:         { action: fake, provider: phone }
      date_of_birth: { action: null }
      national_id:   { action: regex_mask, pattern: "^.{3}", replace: "****" }

  public.addresses:
    strategy: transform
    columns:
      street: { action: fake, provider: street }
      # city and postcode omitted — passthrough is the default

  public.payments:
    strategy: transform
    columns:
      card_last4:    { action: static, value: "9999" }
      billing_name:  { action: fake, provider: full_name }
      billing_email: { action: fake, provider: email }

on_existing_data: fail prevents accidental re-runs against a target that already contains rows. strict_autodetect: true causes dry-run to exit non-zero if any column matches the PII pattern library but has no explicit YAML rule — enforcing masking policy as code in CI rather than relying on auto-detect alone.

What changes operationally

Neosync's self-hosted stack required four persistent services: the API server, a sync worker (which ran the Goja JS engine for custom transformers), a Neosync-managed Postgres instance, and a Temporal cluster for job orchestration. That is infrastructure to maintain, upgrade, monitor, and keep available between mask runs.

PrivaCI is a single container that boots, runs, and exits. There is no persistent service, no scheduler daemon, no internal database. Scheduling is handled by whatever you already use — a Kubernetes CronJob, an ECS scheduled task, a GitHub Actions schedule trigger, or a cron on a VM.

Crash recovery works differently too. Neosync relied on Temporal's durable execution model for retry and resume. PrivaCI writes a per-batch checkpoint to _privaci.table_checkpoints on the target database inside the same transaction as the data write. If a run is interrupted, resume continues from the last committed batch and refuses to proceed if the source schema, config, or salt changed since the interrupted run.

Running your first mask

With mask-rules.yaml in place, verify the column plan before writing any rows. The image's entrypoint is already privaci, so commands go directly after the image name:

export ANONYMIZATION_SALT="$(openssl rand -hex 32)"
export SOURCE_DB_URL="postgresql://user:pass@prod-replica:5432/app"
export TARGET_DB_URL="postgresql://user:pass@staging:5432/app"

docker run --rm \
  -e SOURCE_DB_URL -e TARGET_DB_URL -e ANONYMIZATION_SALT \
  -v "$PWD/mask-rules.yaml:/config/mask-rules.yaml:ro" \
  ghcr.io/boundarylogic/privaci:1.3.0 \
  dry-run --config /config/mask-rules.yaml

dry-run reads the source catalog, resolves the FK graph, runs PII auto-detection over uncovered columns, and exits without writing. With strict_autodetect: true set, it exits non-zero if any PII-pattern column lacks an explicit rule. Review the output, add rules for any flagged columns, then run:

docker run --rm \
  -e SOURCE_DB_URL -e TARGET_DB_URL -e ANONYMIZATION_SALT \
  -v "$PWD/mask-rules.yaml:/config/mask-rules.yaml:ro" \
  ghcr.io/boundarylogic/privaci:1.3.0 \
  run --config /config/mask-rules.yaml

Migrating Neosync subsetting rules

Subsetting requires the Compliance tier. If your Neosync jobs filtered rows before syncing, the equivalent is a commercial-extensions.yaml file passed alongside your masking config using the commercial image. On AWS Marketplace, attach an IAM task role that can verify your subscription at container start — see deployment options.

docker run --rm \
  -e SOURCE_DB_URL -e TARGET_DB_URL -e ANONYMIZATION_SALT \
  -v "$PWD/mask-rules.yaml:/config/mask-rules.yaml:ro" \
  -v "$PWD/commercial-extensions.yaml:/config/commercial-extensions.yaml:ro" \
  ghcr.io/boundarylogic/privaci-commercial:1.0.13 \
  run \
    --config /config/mask-rules.yaml \
    --commercial-extensions /config/commercial-extensions.yaml

The commercial-extensions.yaml file defines the root predicate:

# commercial-extensions.yaml
version: "1.0"
subset:
  - table: public.users
    predicate: "created_at >= now() - interval '90 days'"

PrivaCI resolves the full FK closure automatically from the predicate root: all rows in child tables that reference the matched users are included without additional predicate entries. For per-tenant isolation — equivalent to scoping a Neosync job to a specific workspace or account:

subset:
  - table: public.users
    predicate: "account_id = 451"

See the support repro guide for a worked example of tenant-scoped subsetting from a DR warm standby.

What PrivaCI adds beyond Neosync's anonymization scope

Bounded-memory streaming. PrivaCI streams rows through PostgreSQL's binary COPY protocol in fixed-size batches, keeping memory flat regardless of table size.

Signed compliance reports (Commercial). After a run, the run UUID is printed in the output and stored in _privaci.runs on the target. Pass it to the commercial image to generate a tamper-evident report. Set PRIVACI_REPORT_SIGNING_KEY_PEM to your Ed25519 private key; without it the output is unsigned JSON (fine for dev).

docker run --rm \
  -e TARGET_DB_URL \
  -e PRIVACI_REPORT_SIGNING_KEY_PEM \
  -v "/tmp:/tmp" \
  ghcr.io/boundarylogic/privaci-commercial:1.0.13 \
  report --run <run-uuid> --format json --output /tmp/report.json

Schema drift detection (Commercial). detect-drift compares the current source catalog against the snapshot from the last run and flags new PII columns before they reach staging. Combined with strict_autodetect: true, new columns cause the CI policy gate to fail until an explicit rule is added.

What PrivaCI doesn't cover

Synthetic generation from scratch. PrivaCI requires a live source database.

MySQL and SQL Server. PrivaCI is PostgreSQL-native today. Additional database engines are on the roadmap. S3 object export is on the roadmap. DynamoDB is not planned.

Web UI and API-driven job management. PrivaCI is CLI and YAML only. Neosync's dashboard, per-column transformer UI, and API-driven job orchestration have no equivalent.

Next steps

The OSS engine covers the core anonymization workflow and is free under ELv2. Pull the image and run dry-run against your source to see the column plan before committing to a full migration:

docker run --rm ghcr.io/boundarylogic/privaci:1.3.0 --help

For wiring into CI as a scheduled staging refresh, see the staging guide. For ephemeral per-PR databases with the masking policy gate, see the CI ephemeral guide. Commercial features — subsetting, signed reports, drift detection, JSONB path masking — are available from $149/month on AWS Marketplace (Standard and Compliance tiers).