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).