# Source: https://neon.com/llms/workflows-data-anonymization.txt

# Data anonymization

> The "Data Anonymization" document outlines the process and techniques for anonymizing sensitive data within Neon databases, ensuring compliance with privacy regulations while maintaining data utility for analysis.

## Source

- [Data anonymization HTML](https://neon.com/docs/workflows/data-anonymization): The original HTML version of this documentation

**Note** Beta: This feature is in Beta. Please give us [Feedback](https://console.neon.tech/app/projects?modal=feedback) from the Neon Console or by connecting with us on [Discord](https://discord.gg/92vNTzKDGp).

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](https://postgresql-anonymizer.readthedocs.io/) 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](https://neon.com/docs/extensions/postgresql-anonymizer) masking functions
- **Branch-specific rules**: You can define different masking rules for each anonymized Neon branch

   **Info** 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

Tab: Console

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**



After creation, the Console loads the [Data Masking](https://neon.com/docs/workflows/data-anonymization#manage-masking-rules) page where you define and execute anonymization rules for your branch.

Tab: API

Use the [Create anonymized branch](https://api-docs.neon.tech/reference/createprojectbranchanonymized) endpoint, for example:

```bash
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()"
      }
    ],
    "start_anonymization": true
  }'
```

**Request parameters:**

- `masking_rules` (optional): Array of masking rules to apply to the branch. Each rule specifies:
  - `database_name`: Target database
  - `schema_name`: Target schema (typically `public`)
  - `table_name`: Table containing sensitive data
  - `column_name`: Column to mask
  - `masking_function`: PostgreSQL Anonymizer function to apply
- `start_anonymization` (optional): Set to `true` to automatically start anonymization after branch creation

The API supports all PostgreSQL Anonymizer masking functions, providing more options than the Console UI. You can also export and import masking rules to manage them outside of Neon.

## Manage masking rules

Tab: Console

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](https://neon.com/docs/workflows/data-anonymization#get-anonymization-status).



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

Tab: API

For complete API documentation with request/response examples, see the [API reference](https://neon.com/docs/workflows/data-anonymization#api-reference) section below.

**Get masking rules**

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

Retrieves all masking rules defined for the branch.

**Update masking rules**

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

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

**Start anonymization**

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

Starts or restarts the anonymization process for branches in `initialized`, `error`, or `anonymized` state.

**Get anonymization status**

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

Returns the current state (`created`, `initialized`, `initialization_error`, `anonymizing`, `anonymized`, or `error`) and progress information.

## 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](https://neon.com/docs/connect/connect-from-any-app) 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](https://postgresql-anonymizer.readthedocs.io/en/latest/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:**

```bash
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
  }'
```

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

```json
{
  "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:**

```bash
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'
```

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

```json
{
  "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:**

```bash
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'
```

Details: Response body
```json
{
  "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:**

```bash
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'
```

Details: Response body
```json
{
  "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:

```sql
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:**

```bash
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()"
      }
    ]
  }'
```

Details: Response body
Returns the updated list of masking rules for the branch.

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

## Related resources

- [PostgreSQL Anonymizer documentation](https://postgresql-anonymizer.readthedocs.io/)
- [Neon branching overview](https://neon.com/docs/introduction/branching)
- [Neon API reference](https://api-docs.neon.tech/reference/)

---

## 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

## 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](https://neon.com/docs/guides/neon-github-integration).

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

```yaml
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."
```

## 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

## 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](https://github.com/neondatabase/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.