---
title: Data anonymization
subtitle: Mask sensitive data in development branches using PostgreSQL Anonymizer
redirectFrom:
- /docs/concepts/anonymized-data
tag: new
enableTableOfContents: true
updatedOn: '2025-11-13T15:22:07.940Z'
---
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](/docs/extensions/postgresql-anonymizer) masking functions
- **Branch-specific rules**: You can define different masking rules for each anonymized Neon branch
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**

After creation, the Console loads the [Data Masking](#manage-masking-rules) page where you define and execute anonymization rules for your branch.
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
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](#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.
For complete API documentation with request/response examples, see the [API reference](#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](/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.
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
}'
```
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'
```
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'
```
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'
```
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()"
}
]
}'
```
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](/docs/introduction/branching)
- [Neon API reference](https://api-docs.neon.tech/reference/)
---
## Automate data anonymization with GitHub Actions
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`
The Neon GitHub integration can configure these secrets automatically. See [Neon GitHub integration](/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:
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 }}" <
## 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.