Just launched: create branches with your PII already anonymized - test with production-like data, no manual setup
/Workflows & CI/CD/Data anonymization

Data anonymization

new

Mask sensitive data in development branches using PostgreSQL Anonymizer

Beta

This feature is in Beta. Please give us Feedback from the Neon Console or by connecting with us on Discord.

Need to test against production data without exposing sensitive information? Anonymized branches let you create development copies with masked personally identifiable information (PII) - such as emails, phone numbers, and other sensitive data.

Neon uses PostgreSQL Anonymizer for static data masking, and applies masking rules when you create or update the branch. This approach gives you realistic test data while protecting user privacy and supporting compliance requirements like GDPR.

Key characteristics:

  • Static masking: Data is masked once during branch creation or when you rerun anonymization
  • PostgreSQL Anonymizer integration: Uses the PostgreSQL Anonymizer extension's masking functions
  • Branch-specific rules: You can define different masking rules for each anonymized Neon branch

Static versus dynamic masking

This feature uses static masking, which permanently transforms data in the branch when anonymization runs. Unlike dynamic masking (which masks data during queries), static masking creates an actual masked copy of the data. To get fresh data from the parent, create a new anonymized branch.

Create a branch with anonymized data

Select Anonymized data as the data option when creating a new branch.

  1. Navigate to your project in the Neon Console
  2. Select Projects -> Branches from the sidebar
  3. Click New Branch
  4. In the Create new branch dialog:
    • Select your Parent branch (typically production or main)
    • (Optional) Enter a Branch name
    • (Optional) Automatically delete branch after is checked by default with 1 day selected. You can change it, uncheck it, or leave it as is to automatically delete the branch after the specified time.
    • Under data options, select Anonymized data
  5. Click Create

Neon Console 'Create new branch' dialog with 'Anonymized data' selected

After creation, the Console loads the Data Masking page where you define and execute anonymization rules for your branch.

Manage masking rules

From the Data Masking page:

  1. Select the schema, table, and column you want to mask.
  2. Choose a masking function from the dropdown list (e.g., "Dummy Free Email" to execute anon.dummy_free_email()). The Console provides a curated list of common functions. For the full set of PostgreSQL Anonymizer functions, you must use the API.
  3. Repeat for all sensitive columns.
  4. When you are ready, click Apply masking rules to start the anonymization job. You can monitor its progress on this page or via the API.

Neon Console 'data masking' dialog with example masking functions configured

Important: Rerunning the anonymization process on the anonymized branch applies rules to previously anonymized data, not fresh data from the parent branch. To start from the parent's original data, create a new anonymized branch.

Common workflow

  1. Create an anonymized branch from your production branch.
  2. Define masking rules for sensitive columns (emails, names, addresses, etc.).
  3. Apply the masking rules.
  4. Connect your development environment to the anonymized branch.
  5. When you need fresh data, create a new anonymized branch.

How anonymization works

When you create a branch with anonymized data:

  1. Neon creates a new branch with the schema and data from the parent branch.
  2. You define masking rules for tables and columns containing sensitive data:
    • Console: The Data Masking page opens automatically after branch creation.
    • API: Include masking rules in the creation request or add them later via the masking rules endpoint.
  3. You apply the masking rules (in Console, click Apply masking rules), and the PostgreSQL Anonymizer extension masks the branch data.
  4. You can update rules and rerun anonymization on the branch as needed.

The parent branch data remains unchanged. Rerunning anonymization applies rules to the branch's current (already masked) data, not fresh data from the parent.

note

The branch is unavailable for connections while anonymization is in progress.

Limitations

  • Currently cannot reset to parent, restore, or delete the read-write endpoint for anonymized branches.
  • Rerunning anonymization works on already-masked data. Create a new branch for fresh parent data.
  • Branch is unavailable during anonymization.
  • Masking does not enforce database constraints (e.g., primary keys can be masked as NULL).
  • The Console provides a curated subset of masking functions - use the API for all PostgreSQL Anonymizer masking functions.

API reference

The Neon API provides comprehensive control over anonymized branches, including access to all PostgreSQL Anonymizer masking functions and the ability to export/import masking rules for management outside of Neon.

Create anonymized branch

POST /projects/{project_id}/branch_anonymized

Creates a new branch with anonymized data using PostgreSQL Anonymizer for static masking.

Request body parameters:

  • masking_rules (optional): Array of masking rules to apply to the branch
  • start_anonymization (optional): Set to true to automatically start anonymization after creation

Example request:

curl -X POST \
  'https://console.neon.tech/api/v2/projects/{project_id}/branch_anonymized' \
  -H 'Authorization: Bearer $NEON_API_KEY' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "masking_rules": [
      {
        "database_name": "neondb",
        "schema_name": "public",
        "table_name": "users",
        "column_name": "email",
        "masking_function": "anon.dummy_free_email()"
      },
      {
        "database_name": "neondb",
        "schema_name": "public",
        "table_name": "users",
        "column_name": "age",
        "masking_function": "anon.random_int_between(25,65)"
      }
    ],
    "start_anonymization": true
  }'
Response body

Returns the created branch object with restricted_actions indicating operations not allowed on anonymized branches (restore and delete read-write endpoint).

{
  "branch": {
    "id": "br-divine-feather-a1b2c3d4",
    "project_id": "purple-moon-12345678",
    "parent_id": "br-plain-hill-e5f6g7h8",
    "parent_lsn": "0/1C3C998",
    "name": "br-divine-feather-a1b2c3d4",
    "current_state": "init",
    "pending_state": "ready",
    "state_changed_at": "2025-10-16T02:58:58Z",
    "creation_source": "console",
    "primary": false,
    "default": false,
    "protected": false,
    "cpu_used_sec": 0,
    "compute_time_seconds": 0,
    "active_time_seconds": 0,
    "written_data_bytes": 0,
    "data_transfer_bytes": 0,
    "created_at": "2025-10-16T02:58:58Z",
    "updated_at": "2025-10-16T02:58:58Z",
    "init_source": "parent-data",
    "restricted_actions": [
      {
        "name": "restore",
        "reason": "cannot restore anonymized branches"
      },
      {
        "name": "delete-rw-endpoint",
        "reason": "cannot delete read-write endpoints for anonymized branches"
      }
    ]
  },
  "endpoints": [
    {
      "host": "ep-fragrant-breeze-a1b2c3d4.us-east-1.aws.neon.tech",
      "id": "ep-fragrant-breeze-a1b2c3d4",
      "project_id": "purple-moon-12345678",
      "branch_id": "br-divine-feather-a1b2c3d4",
      "autoscaling_limit_min_cu": 1,
      "autoscaling_limit_max_cu": 4,
      "region_id": "aws-us-east-1",
      "type": "read_write",
      "current_state": "init",
      "pending_state": "active",
      "settings": {
        "preload_libraries": {
          "use_defaults": false,
          "enabled_libraries": ["anon"]
        }
      },
      "pooler_enabled": false,
      "pooler_mode": "transaction",
      "disabled": false,
      "passwordless_access": true,
      "creation_source": "console",
      "created_at": "2025-10-16T02:58:58Z",
      "updated_at": "2025-10-16T02:58:58Z",
      "proxy_host": "us-east-1.aws.neon.tech",
      "suspend_timeout_seconds": 0,
      "provisioner": "k8s-neonvm"
    }
  ],
  "operations": [
    {
      "id": "262dc2ba-4d78-4b7b-bb9a-e29532385f3a",
      "project_id": "purple-moon-12345678",
      "branch_id": "br-divine-feather-a1b2c3d4",
      "action": "create_branch",
      "status": "running",
      "failures_count": 0,
      "created_at": "2025-10-16T02:58:58Z",
      "updated_at": "2025-10-16T02:58:58Z",
      "total_duration_ms": 0
    },
    {
      "id": "f9f52b52-9828-47e4-9842-c08c2a9c14d3",
      "project_id": "purple-moon-12345678",
      "branch_id": "br-divine-feather-a1b2c3d4",
      "endpoint_id": "ep-fragrant-breeze-a1b2c3d4",
      "action": "start_compute",
      "status": "scheduling",
      "failures_count": 0,
      "created_at": "2025-10-16T02:58:58Z",
      "updated_at": "2025-10-16T02:58:58Z",
      "total_duration_ms": 0
    }
  ],
  "roles": [
    {
      "branch_id": "br-divine-feather-a1b2c3d4",
      "name": "neondb_owner",
      "protected": false,
      "created_at": "2025-09-12T13:47:59Z",
      "updated_at": "2025-09-12T13:47:59Z"
    }
  ],
  "databases": [
    {
      "id": 21560101,
      "branch_id": "br-divine-feather-a1b2c3d4",
      "name": "neondb",
      "owner_name": "neondb_owner",
      "created_at": "2025-09-12T13:47:59Z",
      "updated_at": "2025-09-12T13:47:59Z"
    }
  ],
  "connection_uris": [
    {
      "connection_uri": "postgresql://neondb_owner:[REDACTED]@ep-fragrant-breeze-a1b2c3d4.us-east-1.aws.neon.tech/neondb?sslmode=require",
      "connection_parameters": {
        "database": "neondb",
        "password": "[REDACTED]",
        "role": "neondb_owner",
        "host": "ep-fragrant-breeze-a1b2c3d4.us-east-1.aws.neon.tech",
        "pooler_host": "ep-fragrant-breeze-a1b2c3d4-pooler.us-east-1.aws.neon.tech"
      }
    }
  ]
}

Get anonymization status

GET /projects/{project_id}/branches/{branch_id}/anonymized_status

Retrieves the current status of an anonymized branch, including state and progress information.

Example request:

curl -X GET \
  'https://console.neon.tech/api/v2/projects/{project_id}/branches/{branch_id}/anonymized_status' \
  -H 'Authorization: Bearer $NEON_API_KEY' \
  -H 'Accept: application/json'
Response body

State values: created, initialized, initialization_error, anonymizing, anonymized, error. Response may include failed_at timestamp if operation failed.

{
  "branch_id": "br-aged-salad-637688",
  "project_id": "simple-truth-637688",
  "state": "anonymizing",
  "status_message": "Anonymizing table mydb.public.users (3/5)",
  "created_at": "2022-11-30T18:25:15Z",
  "updated_at": "2022-11-30T18:30:22Z"
}

Start anonymization

POST /projects/{project_id}/branches/{branch_id}/anonymize

Starts or restarts the anonymization process for branches in initialized, error, or anonymized state. Applies all defined masking rules.

Example request:

curl -X POST \
  'https://console.neon.tech/api/v2/projects/{project_id}/branches/{branch_id}/anonymize' \
  -H 'Authorization: Bearer $NEON_API_KEY' \
  -H 'Accept: application/json'
Response body
{
  "branch_id": "br-shiny-butterfly-w4393738",
  "project_id": "wild-sky-00366102",
  "state": "anonymized",
  "status_message": "Anonymization completed successfully (2 tables, 3 masking rules applied)",
  "created_at": "2025-11-01T14:01:39Z",
  "updated_at": "2025-11-01T14:01:41Z"
}

Get masking rules

GET /projects/{project_id}/branches/{branch_id}/masking_rules

Retrieves all masking rules defined for the specified anonymized branch.

Example request:

curl -X GET \
  'https://console.neon.tech/api/v2/projects/{project_id}/branches/{branch_id}/masking_rules' \
  -H 'Authorization: Bearer $NEON_API_KEY' \
  -H 'Accept: application/json'
Response body
{
  "masking_rules": [
    {
      "database_name": "neondb",
      "schema_name": "public",
      "table_name": "users",
      "column_name": "age",
      "masking_function": "anon.random_int_between(25,65)"
    },
    {
      "database_name": "neondb",
      "schema_name": "public",
      "table_name": "users",
      "column_name": "email",
      "masking_function": "anon.dummy_free_email()"
    }
  ]
}

You can also query masking rules directly from the database:

SELECT * FROM anon.pg_masking_rules;

Update masking rules

PATCH /projects/{project_id}/branches/{branch_id}/masking_rules

Updates masking rules for the specified anonymized branch. After updating, use the start anonymization endpoint to apply changes.

Example request:

curl -X PATCH \
  'https://console.neon.tech/api/v2/projects/{project_id}/branches/{branch_id}/masking_rules' \
  -H 'Authorization: Bearer $NEON_API_KEY' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "masking_rules": [
      {
        "database_name": "neondb",
        "schema_name": "public",
        "table_name": "users",
        "column_name": "email",
        "masking_function": "anon.dummy_free_email()"
      }
    ]
  }'
Response body

Returns the updated list of masking rules for the branch.

{
  "masking_rules": [
    {
      "database_name": "neondb",
      "schema_name": "public",
      "table_name": "users",
      "column_name": "email",
      "masking_function": "anon.dummy_free_email()"
    }
  ]
}

Automate data anonymization with GitHub Actions

important

The GitHub Actions workflow below uses manual SQL commands with PostgreSQL Anonymizer. For automation using the new Console/API approach documented above, wait for upcoming post-beta improvements to better support automated anonymization.

As an interim solution, you can automate anonymized branch creation using direct SQL commands as outlined below.

Creating anonymized database copies for development, testing, or preview environments can be automated with GitHub Actions. The following workflow creates anonymized Neon branches automatically whenever a pull request is opened or updated.

What you'll achieve for each pull request:

  • Automatic creation of a new Neon branch
  • Installation and initialization of the PostgreSQL Anonymizer extension
  • Application of predefined masking rules to sensitive fields
  • A ready-to-use anonymized dataset for use in CI, preview environments, or manual testing
  1. Requirements

    Before setting up the GitHub Action:

    • A Neon project with a populated parent branch
    • The following GitHub repository secrets:
      • NEON_PROJECT_ID
      • NEON_API_KEY

    tip

    The Neon GitHub integration can configure these secrets automatically. See Neon GitHub integration.

  2. Set up the GitHub Actions workflow

    Create a file at .github/workflows/create-anon-branch.yml (or similar) with the following content. It implements the same masking rules we used in the manual approach:

    note

    This simple workflow example covers the basics. For production use, consider enhancing it with error handling, retry logic, and additional security controls.

    name: PR Open - Create Branch, Run Static Anonymization
    
    on:
      pull_request:
        types: opened
    
    jobs:
      on-pr-open:
        runs-on: ubuntu-latest
        steps:
          - name: Create branch
            uses: neondatabase/create-branch-action@v6
            id: create-branch
            with:
              project_id: ${{ secrets.NEON_PROJECT_ID }}
              branch_name: anon-pr-${{ github.event.number }}
              role: neondb_owner
              api_key: ${{ secrets.NEON_API_KEY }}
    
          - name: Confirm branch created
            run: echo branch_id ${{ steps.create-branch.outputs.branch_id }}
    
          - name: Confirm connection possible
            run: |
              echo "Checking connection to the database..."
              psql "${{ steps.create-branch.outputs.db_url }}" -c "SELECT NOW();"
    
          - name: Enable anon extension
            run: |
              echo "Initializing the extension..."
              psql "${{ steps.create-branch.outputs.db_url }}" <<EOSQL
                SET neon.allow_unstable_extensions='true';
                CREATE EXTENSION IF NOT EXISTS anon CASCADE;
              EOSQL
              echo "Anon extension initialized."
    
          - name: Apply security labels
            run: |
              echo "Applying security labels..."
              psql "${{ steps.create-branch.outputs.db_url }}" <<EOSQL
                SECURITY LABEL FOR anon ON COLUMN users.first_name IS 'MASKED WITH FUNCTION anon.fake_first_name()';
                SECURITY LABEL FOR anon ON COLUMN users.last_name IS 'MASKED WITH FUNCTION anon.fake_last_name()';
                SECURITY LABEL FOR anon ON COLUMN users.iban IS 'MASKED WITH FUNCTION anon.fake_iban()';
                SECURITY LABEL FOR anon ON COLUMN users.email IS 'MASKED WITH FUNCTION anon.fake_email()';
              EOSQL
              echo "Security labels applied."
    
          - name: Run anonymization
            run: |
              echo "Running anonymization..."
              psql "${{ steps.create-branch.outputs.db_url }}" <<EOSQL
                SELECT anon.init();
                SELECT anon.anonymize_database();
              EOSQL
              echo "Database anonymization completed successfully."
  3. Testing the workflow

    To test this automation workflow:

    1. Customize the workflow for your environment by adjusting the branch naming convention and security labels
    2. Push the changes to your repository
    3. Open a new pull request
    4. Check the Actions tab in your GitHub repository to monitor the workflow execution
    5. Verify the anonymized branch creation and data anonymization by:
      • Viewing the GitHub Actions logs
      • Connecting to the new branch and confirm that the original values were replaced
      • Checking the data in the Neon Console's Tables view
  4. Cleaning up

    Remember to clean up anonymized branches when they're no longer needed. You can delete them manually or automate cleanup with the delete-branch-action GitHub Action when PRs are closed.

Conclusion

The PostgreSQL Anonymizer extension with Neon's branching functionality provides a solution for protecting sensitive data in development workflows. By using static masking with Neon branches, you can:

  • Create realistic test environments without exposing sensitive information
  • Obfuscate sensitive information such as names, addresses, emails, and other personally identifiable details (PII)
  • Automate anonymization processes as part of your CI/CD pipeline

While only static masking is currently supported in Neon, this approach offers a robust solution for most development and testing use cases.

Last updated on

Was this page helpful?