> ## Documentation Index
> Fetch the complete documentation index at: https://docs.superblocks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Set up Superblocks Database

> Provision Superblocks-managed AWS Aurora PostgreSQL inside your own AWS account using Terraform and your data plane

<Note>
  **Coming soon**: Superblocks Database is in limited beta. [Contact us](mailto:support@superblockshq.com) to request access.

  The Terraform submodules described below are not yet published to the Terraform Registry. This page documents the planned setup experience so you can review the required AWS permissions ahead of time.
</Note>

Superblocks Database gives each app its own isolated PostgreSQL database on AWS Aurora PostgreSQL in your AWS account. It lives in your VPC, is encrypted at rest, and authenticates with IAM tokens so there are no passwords to rotate or store. Superblocks manages the database lifecycle; you retain ownership of every AWS resource.

Superblocks runs a **shared pool of Aurora PostgreSQL clusters**. Each app that uses Superblocks Database gets its own isolated logical database inside that pool. You can tune isolation and capacity; the defaults keep costs predictable without sacrificing performance.

This page is written for the team that owns your AWS account. For the builder-facing view of the same feature, see [Superblocks Database](/admin/superblocks-on-aws/database).

## Prerequisites

Superblocks Database runs on Aurora PostgreSQL in your own AWS account, so it requires a [Hybrid](/enterprise/deployment-overview) or [Cloud-Prem](/enterprise/cloud-prem/aws) deployment with a Superblocks [data plane](/enterprise/hybrid-architecture/overview) you operate. It is not available on Superblocks Cloud: every resource lives in your VPC, so there is no multi-tenant equivalent. AWS is the only supported cloud today.

Before you start, confirm each of the following:

* **A data plane on [ECS Fargate](/enterprise/hybrid-architecture/deployment/aws_ecs_fargate) or [EKS](/enterprise/hybrid-architecture/deployment/kubernetes).** Each data plane covers one AWS account and one AWS region. To run databases in more than one region, deploy one data plane per region.
* **Terraform or OpenTofu 1.6 or later.** You need this even if you deploy the data plane with Helm and do not otherwise use Terraform, because the IAM roles and the state bucket are only distributed as a Terraform module. OpenTofu is also part of the runtime: your data plane runs `tofu apply` to create and manage databases after setup.
* **Permission to create IAM roles, an S3 bucket, and Aurora clusters.** This is a one-time setup operation. See [Raw IAM policy JSON](#raw-iam-policy-json) if your organization requires policies to be pre-approved.
* **A VPC with private subnets in at least two Availability Zones.**
* **Enough AWS quota headroom** for Aurora clusters and instances, DB subnet groups, security groups, ENIs, and free IP addresses in your database subnets. The pool grows over time, so plan for more than the first cluster.

## How databases go from edit to deployed

A database is created when a builder with database creation permission asks for one in the Superblocks editor. If an existing Aurora PostgreSQL cluster has capacity under your scaling settings, Superblocks queues a task to create an isolated logical database on that cluster. Your data plane picks tasks up from the queue and applies them.

Clark generates the database schema and stores it as `.sql` files in the application code. Once the database is ready, Clark queues a task for the data plane to run the migrations.

Databases follow your data plane layout. The common setup uses separate data planes for staging and production, so each app gets an isolated database per environment. The production database is created when the application is deployed.

The diagram below traces one app's database end to end. While a builder is still editing, Clark asks for a database, your data plane creates it, and Clark's generated schema is applied. On deploy, the same sequence runs again against a separate production database before the deploy can finish. Superblocks only queues work; your data plane claims it and calls AWS.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Dev as Builder
    participant Clark as Clark
    participant SB as Superblocks control plane
    participant DP as Data plane lifecycle worker
    participant AWS as Your AWS account

    Dev->>Clark: Ask for a feature that needs persistent data
    Clark->>SB: Request a database for this app
    SB->>SB: Queue a lifecycle task
    DP->>SB: Claim the pending task
    DP->>AWS: tofu apply: create isolated logical database
    AWS-->>DP: Database ready
    DP->>SB: Ready callback
    Clark->>Clark: Generate schema as .sql files in the app code
    Clark->>SB: Queue a migration task
    DP->>AWS: Apply migrations to the development database
    Dev->>SB: Deploy the app
    SB->>DP: Create the production database, then apply migrations
    DP->>AWS: tofu apply plus migrations
    SB-->>Dev: Deploy proceeds once the production database is ready
```

Per-app databases are pooled into Aurora PostgreSQL clusters, which is usually the cheapest option that still performs well. When clusters approach the capacity you configured, Superblocks queues a task to provision a new cluster. You control those limits, and therefore the spend.

## Setup decisions

A few choices are made once, in Terraform or Helm, and then apply to every Aurora cluster and every app database that data plane creates. Superblocks does not expose them in the admin panel (the panel only shows what your data plane registered), so decide them before you run the module. Anything not listed here has a safe default.

### Which data planes get databases, and what they are called

You declare one entry per data plane in the `agents` map, and each entry names the [data tags](/development-lifecycle/build/data-tags) it serves and the VPC it provisions into. A data plane covers one AWS account and one AWS region, so a second region means a second data plane. The common split is one data plane for nonprod and one for production, which is also what gives each app a separate database per environment.

Three parts of this are fixed once databases exist. Changing them afterwards breaks access to existing databases and currently requires manual recovery, so settle them now:

* **The data tag names.** Database and user names are derived from a hash of the tag: `sbndb_<profile-token>_<application-token>` for the database and `sbndb_<profile-token>_<application-token>_runtime` for its user. `<profile-token>` is the first 16 hex characters of the SHA-256 of the lowercased data tag, and `<application-token>` is a 24-hex hash of the application ID. The connector role's permissions are scoped to that pattern, so renaming a tag orphans every existing database and user under the old name. Tag names must satisfy the module validation: 1–15 lowercase letters, digits, or hyphens (`^[a-z0-9-]{1,15}$`).
* **The VPC.** Every IAM policy is scoped to it.
* **The map key that names each data plane.** Changing it makes Terraform destroy and recreate the IAM roles, and the data plane is non-functional until it is redeployed with the new role ARNs. No data is lost, but this is a coordinated change, not a rename.

### How much capacity each cluster gets

**Aurora Serverless v2 is the default and recommended capacity shape.** You set an ACU range instead of an instance size, and Aurora scales within it as load changes. That fits a pooled cluster whose load is the sum of many apps. Aurora provisioned instances and standalone RDS for PostgreSQL are also supported (the IAM policies cover both engines), but Serverless v2 is what the examples use and what we recommend starting from.

Two values inside `deployment.serverless_v2` matter most. The ACU range (`min_acu` to `max_acu`) sets the floor and ceiling of what one cluster can consume, and `instance_count` decides whether a second warm instance stands by for immediate failover. Production is typically two instances with a nonzero floor; nonprod can run a single instance that pauses when idle.

<Note>
  Setting `min_acu = 0` lets a cluster pause after an idle period. Use that for nonprod, not production. Scale-to-zero requires Aurora PostgreSQL 13.15+, 14.12+, 15.7+, 16.3+, or a later major version.
</Note>

### How many apps share a cluster

`pool.max_databases` caps how many app databases land on one Aurora cluster before Superblocks provisions another. The default is 100. Lowering it spreads apps across more clusters, which shrinks the blast radius of a failover or an engine patch and raises your baseline cost; raising it does the reverse. Along with the ACU range, this is the main control over what the pool costs as it grows.

### Whether Superblocks creates the IAM roles or reuses yours

By default the module creates a lifecycle worker role and a connector role for each data plane. If your data plane's ECS task role or IRSA role already carries permissions you need to preserve, set `existing_role_name` on that `agents` entry instead: the module attaches its policies to your existing role rather than creating a new one.

### Which keys encrypt state and credentials

Both encryption choices default to AWS-managed keys, which incur no KMS charges and require nothing from you:

* **The OpenTofu state bucket** uses SSE-S3. Supply `kms_key_arn` to use SSE-KMS with your own key instead; the lifecycle worker's policy is then scoped to that one key ARN and no other key in your account.
* **The RDS-managed master secret** uses the AWS-managed Secrets Manager key for your account. Supply `rds_secret_kms_key_arn` on an `agents` entry to use your own key.

## One-time setup

Setup runs once per data plane: create the AWS prerequisites with Terraform, provision the first Aurora cluster from the admin panel, and confirm builders can use it. After that, Superblocks grows and manages the pool without further setup.

### Step 1: Run the Terraform module

Superblocks publishes two submodules as part of the `superblocksteam/superblocks/aws` Terraform registry module:

* `modules/native-db-prereqs`: run by all customers. Creates the lifecycle worker IAM roles, connector IAM roles, and the shared S3 state bucket.
* `modules/native-db`: ECS Fargate only. Takes the prereq outputs plus your database config and renders the data plane runtime configuration as `ecs_env_vars`, ready to inject into the ECS task definition.

Use a single Terraform state bucket for all data planes in a given account and region so subsequent runs are idempotent. For the full inventory of what this run creates, see [What Superblocks creates in your AWS account](#what-superblocks-creates-in-your-aws-account).

<Tabs>
  <Tab title="ECS Fargate">
    For Fargate deployments, both modules are run together. The `modules/native-db` output (`ecs_env_vars`) is passed directly into the root `terraform_aws_superblocks` module, which wires it into the ECS task definition. You do not need to inspect the contents of `ecs_env_vars`; it is generated in the expected format.

    ```mermaid theme={null}
    sequenceDiagram
        autonumber
        participant Admin as Your admin
        participant TF as Terraform
        participant AWS as Your AWS account
        participant ECS as ECS
        participant DP as Data plane
        participant SB as Superblocks control plane

        Admin->>TF: terraform apply: native-db-prereqs plus native-db
        TF->>AWS: Create lifecycle worker IAM role
        TF->>AWS: Create connector IAM role
        TF->>AWS: Create S3 state bucket
        TF->>ECS: Update task definition: database env vars plus lifecycle worker task role
        ECS->>DP: Redeploy the data plane automatically
        DP->>SB: Register capability, engine, and capacity
        SB-->>Admin: Admin panel shows the registered config as read-only
    ```

    **Step 1a: Add both modules to your Terraform config.**

    <Warning>
      **`modules/native-db` is not published yet, and its input names are provisional.** The block below reflects the interface as currently designed, not a released module. Treat the input names as subject to change until the module is on the Terraform Registry, and check the module's own README when it ships rather than copying these names into production Terraform today. The `native-db-prereqs` inputs, the IAM policies, and the root `terraform_aws_superblocks` wiring are stable.
    </Warning>

    ```hcl theme={null}
    # Step 1 of 2: IAM roles and S3 state bucket.
    # Run once per region. All data planes in this region share one S3 state bucket.
    module "native_db_prereqs" {
      source  = "superblocksteam/superblocks/aws//modules/native-db-prereqs"
      version = "~> 1.0"

      deployment_type = "fargate"
      region          = "us-east-1"

      # One entry per data plane in this region.
      # The map key names the IAM roles and must be unique per AWS account
      # (max 15 characters, lowercase alphanumeric and hyphens).
      agents = {
        prod = {
          # Data tags namespace the DB users Superblocks provisions for this data plane.
          # A tag "nonprod" creates databases named sbndb_6fdc0c6b96ee8a74_<application-token>
          # with runtime users named sbndb_6fdc0c6b96ee8a74_<application-token>_runtime
          # (<profile-token> = first 16 hex of SHA-256 of the lowercased tag).
          agent_tags = ["nonprod", "production"]

          # VPC the lifecycle worker is allowed to provision Aurora into.
          # The IAM policy is scoped to this VPC. The lifecycle worker cannot create
          # databases in other VPCs in your account.
          vpc_id = "vpc-055aa171d205ef5e7"

          # Optional: attach lifecycle worker policies to an existing ECS task role
          # instead of creating a new one. Use this for brownfield deployments where
          # your data plane's ECS task already has a role with permissions to keep.
          # existing_role_name = "my-existing-data-plane-task-role"

          # Optional: ARN of a customer-managed KMS key used to encrypt the RDS-managed
          # master secret in Secrets Manager. When omitted, the secret uses the
          # AWS-managed Secrets Manager key for your account.
          # rds_secret_kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/mrk-..."
        }
      }

      # Optional: customer-managed KMS key for the OpenTofu state bucket.
      # When omitted, the bucket uses AWS-managed encryption (SSE-S3, no extra charge).
      # When set, the lifecycle worker IAM policy scopes KMS access to this key only.
      # kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/mrk-..."

      # Optional: override the default resource name prefix ("sb-native-db").
      # IAM roles are named <name_prefix>-<agent_name>-*. Max 16 characters.
      # name_prefix = "acme-native-db"

      tags = {
        Environment = "production"
      }
    }

    # Step 2 of 2: data plane database config. One module call per data plane.
    # Generates ecs_env_vars in the expected format.
    module "native_db_prod" {
      source  = "superblocksteam/superblocks/aws//modules/native-db"
      version = "~> 1.0"

      # Must match the agents map key above. This is the data plane's stable
      # identity for the database pool. See the setup decisions above.
      agent_name = "prod"

      # Must match agent_tags on the agents["prod"] entry above. native-db-prereqs
      # does not re-export agent_tags, so repeat the list here.
      agent_tags         = ["nonprod", "production"]
      connector_role_arn = module.native_db_prereqs.agents["prod"].connector_role_arn

      # All data planes in this region share one bucket.
      state_bucket_name = module.native_db_prereqs.state_bucket_name

      region = "us-east-1"

      # Namespaces this data plane's OpenTofu state within the shared S3 bucket.
      # Must be unique per data plane. Using the agents map key as a suffix is recommended.
      key_prefix = "native-db/prod"

      # Maximum number of logical databases a single Aurora cluster can hold
      # before a new cluster is automatically provisioned into the pool.
      pool = {
        max_databases = 100
      }

      # Physical database configuration for every Aurora cluster
      # this data plane provisions.
      physical_module_inputs = {
        # Days to retain automated backups (1 to 35).
        backup_retention_period = 7

        # Delete automated backups when the cluster is destroyed.
        delete_automated_backups = false

        # Prevent accidental deletion of the cluster.
        # Recommended true for production.
        deletion_protection = true

        # Aurora capacity. Serverless v2 is the default and recommended shape:
        # capacity scales between min_acu and max_acu with no instance sizing to pick.
        # instance_count = 2 keeps a second warm instance for immediate failover.
        deployment = {
          serverless_v2 = {
            instance_count = 2
            max_acu        = 32
            min_acu        = 2
          }
        }

        # Skip the final snapshot when the cluster is destroyed.
        # Leave false in production to retain a snapshot before deletion.
        skip_final_snapshot = false

        # Security groups allowed to reach the database on port 5432, typically
        # your data plane's security group. Prefer this over allowed_cidr_blocks,
        # which is intended for tightly scoped sandbox testing.
        source_security_group_ids = ["sg-0a1b2c3d4e5f67890"]

        # Private subnets for the Aurora subnet group.
        # Must span at least two Availability Zones.
        subnet_ids = [
          "subnet-0d7439ecc4e9ec21b",
          "subnet-0edfe9083e033f7aa",
          "subnet-0fb328c771fda45b2",
        ]

        # Tag Aurora clusters with the same values applied to the IAM and S3
        # resources created above. native-db-prereqs has no tags output, so repeat
        # the map you passed to that module.
        tags = {
          Environment = "production"
        }

        # VPC where the Aurora cluster will be created.
        # Must match the vpc_id declared in the agents map in native_db_prereqs above.
        vpc_id = "vpc-055aa171d205ef5e7"

        # Note: public accessibility is not an input. The module always creates
        # private clusters, and the lifecycle worker IAM policy rejects a create
        # that asks for a public endpoint regardless of module configuration.
      }
    }

    module "terraform_aws_superblocks" {
      source  = "superblocksteam/superblocks/aws"
      version = "~> 1.0"

      vpc_id         = "vpc-055aa171d205ef5e7"
      lb_subnet_ids  = ["subnet-lb-aaa", "subnet-lb-bbb"]
      ecs_subnet_ids = ["subnet-ecs-aaa", "subnet-ecs-bbb"]
      domain         = "your-domain.com"
      subdomain      = "superblocks"

      superblocks_agent_key = "[YOUR_AGENT_KEY]"

      # Comma-separated string, not a list. Must cover the same data tags as the
      # agent_tags declared in native_db_prereqs above. See the warning below.
      superblocks_agent_tags = "profile:nonprod,profile:production"

      # Run the ECS task as the lifecycle worker role. This is required and separate
      # from the env vars below: environment variables cannot change the task role,
      # so without this the data plane keeps its previous role and every AWS call the
      # lifecycle worker makes fails with AccessDenied.
      superblocks_agent_role_arn = module.native_db_prereqs.agents["prod"].lifecycle_worker_role_arn

      # Wire the generated database env vars into the ECS task definition.
      superblocks_agent_environment_variables = module.native_db_prod.ecs_env_vars
    }
    ```

    **Step 1b: Run `terraform apply`.** The module applies three changes: it creates the prereq resources (IAM roles and S3), renders the data plane runtime config as env vars, and wires them into the ECS task definition along with the lifecycle worker task role.

    <Warning>
      The `agent_tags` declared in `native_db_prereqs` must refer to the same [data tags](/development-lifecycle/build/data-tags) as the `superblocks_agent_tags` you configure for this data plane in `terraform_aws_superblocks`. The module uses each tag's profile token (the first 16 hex characters of `SHA-256(lowercase(tag))`) to scope the connector role's `rds-db:connect` permissions to `sbndb_<profile-token>_*_runtime`. If the two diverge, the data plane's IAM token will not match any permitted DB user and queries will fail. There is no automatic validation between them.

      Note the two inputs use different types as well as different formats. `agent_tags` is a list of bare data tag keys (`["nonprod", "production"]`), while `superblocks_agent_tags` is a single comma-separated string using the `profile:<DATA_TAG_KEY>` form (`"profile:nonprod,profile:production"`). Wildcards are allowed in neither: `agent_tags` rejects `*`, and the data plane refuses to start with `profile:*` when Superblocks Database is enabled, because the control plane routes database work by exact data tag.
    </Warning>

    **Step 1c: ECS redeploys automatically** when the task definition is updated. Your data plane starts with Superblocks Database enabled and registers its database lifecycle capability with the Superblocks control plane.

    #### Multiple data planes in the same region

    To manage multiple data planes in the same region, add one entry per data plane to the `agents` map and one `native_db_*` module call per data plane. All data planes automatically share the same S3 state bucket. Each gets its own isolated IAM roles and its own physical database config.

    If you need different capacity per data tag set (for example, scale-to-zero for nonprod and two warm instances for production), define separate data planes, one per tag set, and give each its own `physical_module_inputs`.

    Each data plane is also a separate ECS service, so each `native_db_*` module's `ecs_env_vars` output is wired into its own `terraform_aws_superblocks` invocation.

    ```hcl theme={null}
    module "native_db_prereqs" {
      # ...same source, version, deployment_type, and region as above...
      agents = {
        prod = {
          agent_tags = ["production"]
          vpc_id     = "vpc-055aa171d205ef5e7"
        }
        dev = {
          agent_tags = ["nonprod"]
          vpc_id     = "vpc-0def456"
        }
      }
    }

    # One native_db module call per data plane.
    # Production: two warm instances, never paused.
    module "native_db_prod" {
      # ...
      agent_name         = "prod"
      agent_tags         = ["production"]
      connector_role_arn = module.native_db_prereqs.agents["prod"].connector_role_arn
      state_bucket_name  = module.native_db_prereqs.state_bucket_name
      key_prefix         = "native-db/prod"
      pool               = { max_databases = 100 }

      physical_module_inputs = {
        deployment = {
          serverless_v2 = {
            instance_count = 2
            max_acu        = 32
            min_acu        = 2
          }
        }

        subnet_ids = ["subnet-0d7439ecc4e9ec21b", "subnet-0edfe9083e033f7aa"]
        vpc_id     = "vpc-055aa171d205ef5e7"
      }
    }

    # Nonprod: one instance that scales to zero after five idle minutes.
    module "native_db_dev" {
      # ...
      agent_name         = "dev"
      agent_tags         = ["nonprod"]
      connector_role_arn = module.native_db_prereqs.agents["dev"].connector_role_arn
      state_bucket_name  = module.native_db_prereqs.state_bucket_name
      key_prefix         = "native-db/dev"
      pool               = { max_databases = 100 }

      physical_module_inputs = {
        deployment = {
          serverless_v2 = {
            auto_pause_seconds = 300
            instance_count     = 1
            max_acu            = 4
            min_acu            = 0
          }
        }

        subnet_ids = ["subnet-0a1b2c3d4e5f60001", "subnet-0a1b2c3d4e5f60002"]
        vpc_id     = "vpc-0def456"
      }
    }

    # Each data plane is a separate ECS service. Wire each one's task role and env
    # vars into its own invocation. Both are per-data-plane.
    module "terraform_aws_superblocks_prod" {
      # ...
      superblocks_agent_tags                  = "profile:production"
      superblocks_agent_role_arn              = module.native_db_prereqs.agents["prod"].lifecycle_worker_role_arn
      superblocks_agent_environment_variables = module.native_db_prod.ecs_env_vars
    }

    module "terraform_aws_superblocks_dev" {
      # ...
      superblocks_agent_tags                  = "profile:nonprod"
      superblocks_agent_role_arn              = module.native_db_prereqs.agents["dev"].lifecycle_worker_role_arn
      superblocks_agent_environment_variables = module.native_db_dev.ecs_env_vars
    }
    ```

    For data planes in a different AWS region, use a separate `native_db_prereqs` invocation with an AWS provider alias configured for that region. Each region gets its own S3 bucket.
  </Tab>

  <Tab title="EKS">
    For EKS deployments, the Terraform module handles IAM and S3 only. The database configuration is set separately in your Helm chart under the `databaseLifecycle` block.

    ```mermaid theme={null}
    sequenceDiagram
        autonumber
        participant Admin as Your admin
        participant TF as Terraform
        participant AWS as Your AWS account
        participant Helm as Helm
        participant DP as Data plane
        participant SB as Superblocks control plane

        Admin->>TF: terraform apply: native-db-prereqs
        TF->>AWS: Create lifecycle worker IAM role
        TF->>AWS: Create connector IAM role
        TF->>AWS: Create S3 state bucket
        TF-->>Admin: Output role ARNs and bucket name
        Admin->>Helm: Add role ARNs, bucket, and db config to values.yaml
        Admin->>Helm: helm upgrade
        Helm->>DP: Restart the data plane with Superblocks Database enabled
        DP->>SB: Register capability, engine, and capacity
        SB-->>Admin: Admin panel shows the registered config as read-only
    ```

    **Step 1a: Run the prereqs module and apply.**

    ```hcl theme={null}
    module "native_db_prereqs" {
      source  = "superblocksteam/superblocks/aws//modules/native-db-prereqs"
      version = "~> 1.0"

      deployment_type = "eks"
      region          = "us-east-1"

      # One entry per data plane in this region.
      # The map key names the IAM roles and must be unique per AWS account
      # (max 15 characters, lowercase alphanumeric and hyphens).
      agents = {
        prod = {
          # Data tags namespace the DB users Superblocks provisions for this data plane.
          # A tag "nonprod" creates databases named sbndb_6fdc0c6b96ee8a74_<application-token>
          # with runtime users named sbndb_6fdc0c6b96ee8a74_<application-token>_runtime
          # (<profile-token> = first 16 hex of SHA-256 of the lowercased tag).
          agent_tags = ["nonprod", "production"]

          # VPC the lifecycle worker is allowed to provision Aurora into.
          # The IAM policy is scoped to this VPC. The lifecycle worker cannot create
          # databases in other VPCs in your account.
          vpc_id = "vpc-055aa171d205ef5e7"

          # ARN of the EKS cluster's OIDC provider. Required for EKS (IRSA).
          # Found in the EKS console under "Configuration > Authentication", or via:
          #   aws eks describe-cluster --name <cluster-name> \
          #     --query "cluster.identity.oidc.issuer" --output text
          oidc_provider_arn = "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE"

          # Kubernetes namespace and service account name of the data plane pod.
          # Used to scope the OIDC trust policy condition to the specific service account
          # (system:serviceaccount:<namespace>:<service_account_name>).
          # Defaults match the standard Superblocks Helm chart values. Only override if you deviate.
          # namespace            = "superblocks"
          # service_account_name = "superblocks-agent"

          # Optional: attach lifecycle worker policies to an existing IRSA role
          # instead of creating a new one. Use this for brownfield deployments where
          # your data plane pod already has an IRSA-annotated role from a prior Helm install.
          # existing_role_name = "my-existing-data-plane-irsa-role"

          # Optional: ARN of a customer-managed KMS key used to encrypt the RDS-managed
          # master secret in Secrets Manager. When omitted, the secret uses the
          # AWS-managed Secrets Manager key for your account.
          # rds_secret_kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/mrk-..."
        }
      }

      # Optional: customer-managed KMS key for the OpenTofu state bucket.
      # When omitted, the bucket uses AWS-managed encryption (SSE-S3, no extra charge).
      # When set, the lifecycle worker IAM policy scopes KMS access to this key only.
      # kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/mrk-..."

      # Optional: override the default resource name prefix ("sb-native-db").
      # IAM roles are named <name_prefix>-<agent_name>-*. Max 16 characters.
      # name_prefix = "acme-native-db"

      tags = {
        Environment = "production"
      }
    }
    ```

    **Step 1b: Wire the outputs into your Helm values.** After `terraform apply`, add the module outputs and your database config to your Helm values. The settings span four top-level sections, not just `databaseLifecycle`. The two IAM roles in particular are wired through the sections that already own identity in the chart:

    | What                  | Where it goes                       | Why there                                                                                                             |
    | --------------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
    | Lifecycle worker role | `serviceAccount.annotations`        | EKS grants pod identity through the IRSA annotation, so the role reaches the pod the same way any other AWS role does |
    | Connector role        | `postgres.nativeDbConnectorRoleArn` | It is a query-time database credential, and `postgres` owns the data plane's PostgreSQL connection settings           |
    | Data tags             | `superblocks.agentTags`             | Source of truth for which data tags accept database work                                                              |
    | Everything else       | `databaseLifecycle`                 | Feature-specific configuration                                                                                        |

    ```yaml theme={null}
    superblocks:
      # Stable name for this data plane. Must match the agents map key you declared
      # in native_db_prereqs. Required when Superblocks Database is enabled.
      agentName: prod

      # Comma-separated string, each data tag prefixed with profile:.
      # Must cover the same data tags as agent_tags above. See the warning below.
      agentTags: profile:nonprod,profile:production

    serviceAccount:
      annotations:
        # Lifecycle worker role, assumed by the data plane pod through IRSA.
        # From: module.native_db_prereqs.agents["prod"].lifecycle_worker_role_arn
        eks.amazonaws.com/role-arn: "<lifecycle_worker_role_arn>"

    postgres:
      # Connector role, used at query time to authenticate to app databases with
      # RDS IAM tokens.
      # From: module.native_db_prereqs.agents["prod"].connector_role_arn
      nativeDbConnectorRoleArn: "<connector_role_arn>"

      # The data plane refuses to authenticate as any IAM role outside this list.
      # It must cover nativeDbConnectorRoleArn above, or the chart rejects the upgrade.
      iamAllowedRoleArnPrefixes:
        - "<connector_role_arn>"

    databaseLifecycle:
      enabled: true

      # OpenTofu state. The bucket is shared across every data plane in this account
      # and region; keyPrefix namespaces this data plane's state inside it and must
      # be unique per data plane.
      backend:
        stateBackend: s3
        # From: module.native_db_prereqs.state_bucket_name
        bucket: "<state_bucket_name>"
        region: us-east-1
        keyPrefix: native-db/prod
        useLockfile: true

      # Secrets Manager ARNs the data plane may read, used to fetch the AWS-managed
      # Aurora master credential during provisioning. Scope this to RDS-managed
      # secrets in your account and region. An empty list denies resolution, which
      # makes every provision fail. Use the rds!db- prefix instead for standalone RDS.
      allowedRefPrefixes:
        - arn:aws:secretsmanager:us-east-1:123456789012:secret:rds!cluster-

      # Aurora is not the chart default. The default is standalone RDS for
      # PostgreSQL, so select the Aurora module explicitly. Pin the ref to the
      # version your data plane release ships with.
      modules:
        physical:
          source: git::https://github.com/superblocksteam/terraform-superblocks-databases.git//modules/aws-aurora-managed-cluster?ref=v0.4.1

      # Maximum number of logical databases a single Aurora cluster can hold
      # before a new cluster is automatically provisioned into the pool.
      pool:
        maxDatabases: 100

      # Physical database configuration for every Aurora cluster
      # this data plane provisions.
      physicalModuleInputs:
        # Days to retain automated backups (1 to 35).
        backup_retention_period: 7

        # Delete automated backups when the cluster is destroyed.
        delete_automated_backups: false

        # Prevent accidental deletion of the cluster.
        # Recommended true for production.
        deletion_protection: true

        # Aurora capacity. Serverless v2 is the module default and the recommended
        # shape: capacity scales between min_acu and max_acu with no instance sizing
        # to pick. instance_count 2 keeps a second warm instance for immediate
        # failover. Spelled out here because these values determine what the pool
        # costs as it grows, so choose them deliberately.
        deployment:
          serverless_v2:
            instance_count: 2
            max_acu: 32
            min_acu: 2

        # Skip the final snapshot when the cluster is destroyed.
        # Leave false in production to retain a snapshot before deletion.
        skip_final_snapshot: false

        # Security groups allowed to reach the database on port 5432, typically
        # your data plane's security group. Prefer this over allowed_cidr_blocks,
        # which is intended for tightly scoped sandbox testing.
        source_security_group_ids:
          - sg-0a1b2c3d4e5f67890

        # Private subnets for the Aurora subnet group.
        # Must span at least two Availability Zones.
        subnet_ids:
          - subnet-0d7439ecc4e9ec21b
          - subnet-0edfe9083e033f7aa
          - subnet-0fb328c771fda45b2

        # VPC where the Aurora cluster will be created.
        # Must match the vpc_id declared in the agents map in native_db_prereqs above.
        vpc_id: vpc-055aa171d205ef5e7

        # Note: public accessibility is not configurable here. The module always
        # creates private clusters, and the lifecycle worker IAM policy rejects a
        # create that asks for a public endpoint regardless of module configuration.
    ```

    Values not shown keep their chart defaults, which are the recommended settings:

    * **TLS** uses `verify-full` against the AWS trust bundle packaged in the data plane image. The chart refuses to enable Superblocks Database with any weaker mode.
    * **The OpenTofu working directory and binary path** are fixed properties of that image. The chart rejects attempts to override either.
    * **The resource types the lifecycle worker may create** default to exactly what the database modules it ships with declare. Set `allowedResourceTypes` only to run a fork or a custom module. A non-empty list replaces the default outright rather than adding to it.

    <Warning>
      The `agent_tags` declared in `native_db_prereqs` must refer to the same [data tags](/development-lifecycle/build/data-tags) as the `agentTags` you configure for this data plane in your Helm chart. The module uses each tag's profile token (the first 16 hex characters of `SHA-256(lowercase(tag))`) to scope the connector role's `rds-db:connect` permissions to `sbndb_<profile-token>_*_runtime`. If the two diverge, the data plane's IAM token will not match any permitted DB user and queries will fail. There is no automatic validation between them.

      Note the two inputs use different types as well as different formats. `agent_tags` is a list of bare data tag keys (`["nonprod", "production"]`), while `agentTags` is a single comma-separated string using the `profile:<DATA_TAG_KEY>` form (`"profile:nonprod,profile:production"`). Wildcards are allowed in neither: `agent_tags` rejects `*`, and the chart refuses to render `profile:*` when Superblocks Database is enabled, because the control plane routes database work by exact data tag.
    </Warning>

    **Step 1c: Upgrade your Helm chart.**

    ```bash theme={null}
    helm upgrade superblocks-agent superblocksteam/superblocks-agent \
      -f values.yaml \
      --namespace superblocks
    ```

    Your data plane restarts with Superblocks Database enabled and registers its database lifecycle capability with the Superblocks control plane.

    #### Multiple data planes in the same region

    For multiple data planes in the same region (whether in the same EKS cluster or separate clusters), add one entry per data plane to the `agents` map. Each gets its own lifecycle worker role and connector role, scoped to its own OIDC provider and VPC. They all share the S3 state bucket automatically.

    ```hcl theme={null}
    module "native_db_prereqs" {
      # ...same source, version, deployment_type, and region as above...
      agents = {
        prod = {
          agent_tags        = ["production"]
          vpc_id            = "vpc-055aa171d205ef5e7"
          oidc_provider_arn = "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/PROD_CLUSTER"
        }
        dev = {
          agent_tags        = ["nonprod"]
          vpc_id            = "vpc-0def456"
          oidc_provider_arn = "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/DEV_CLUSTER"
        }
      }
    }
    ```

    Each data plane then gets its own Helm values file referencing its own entry in the `agents` output. Use `module.native_db_prereqs.agents["prod"]` for the prod data plane and `module.native_db_prereqs.agents["dev"]` for the dev data plane. The `state_bucket_name` output is shared and used by both.

    For data planes in a different AWS region, use a separate `native_db_prereqs` invocation with an AWS provider alias configured for that region. Each region gets its own S3 bucket.
  </Tab>
</Tabs>

### Step 2: Provision your database from the admin panel

Once the data plane is running with Superblocks Database enabled, return to the data plane setup wizard, or open the Superblocks admin panel and navigate to **Databases**. You will see the database configuration registered by your data plane, including the engine, capacity settings, and data tags pulled from its running configuration. These are read-only: they reflect what you configured in Terraform or Helm.

Click **Provision**. Superblocks dispatches the request to your data plane, which runs `tofu apply` in your AWS account. You do not re-enter any configuration; it is already in the data plane.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Admin as Admin panel
    participant SB as Superblocks control plane
    participant DP as Data plane lifecycle worker
    participant AWS as Your AWS account

    Admin->>SB: Click Provision
    SB->>SB: Deduplicate: one provisioning request per deployment
    DP->>SB: Claim the provisioning request
    SB->>DP: Dispatch the provisioning request
    DP->>AWS: tofu init plus tofu apply
    Note over AWS: Aurora cluster<br/>DB subnet group<br/>Security group<br/>tagged ManagedBy and Vpc
    AWS-->>DP: Cluster ready
    DP->>SB: Terminal callback: success or failure with error detail
    SB-->>Admin: Admin panel reflects the result
```

Provisioning an Aurora cluster typically takes 5 to 15 minutes. The admin panel shows live status and updates automatically, so you do not need to keep the browser open.

**If two admins click Provision at the same time**, only one request proceeds. The second admin sees who triggered provisioning and when, and can follow the same live status.

**When provisioning completes**, the data plane sends a confirmation to the Superblocks control plane and the panel shows a success state. If provisioning fails, you see the error detail and a retry option. Failures are surfaced here, not to end users.

### Step 3: Ready

Once the panel shows a success state, your builders can create apps backed by Superblocks Database immediately. There is no cold-start wait, because the database pool is already running.

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Dev as Builder
    participant SB as Superblocks control plane
    participant DP as Data plane lifecycle worker
    participant Pool as Shared Aurora cluster

    Dev->>SB: Create an app backed by Superblocks Database
    SB->>Pool: Check capacity in the shared pool
    alt Capacity available
        SB->>DP: Dispatch: create logical database
        DP->>Pool: tofu apply: create PostgreSQL database and role
        DP->>SB: Ready callback
        SB-->>Dev: App integration ready in seconds
    else Pool full
        SB->>DP: Dispatch: provision a new Aurora cluster first
        DP->>SB: New cluster registered in the pool
        SB->>DP: Dispatch: create logical database
        DP->>Pool: tofu apply
        DP->>SB: Ready callback
        SB-->>Dev: App integration ready in 5 to 15 minutes
    end
```

From this point:

* **Builders** can create apps backed by Superblocks Database without any infrastructure wait. Superblocks provisions an isolated logical database inside the shared pool on demand.
* **Deploys are gated.** Superblocks blocks a production deploy if the production database or its required SQL migrations are not ready.

## What Superblocks creates in your AWS account

Resources arrive in two waves with very different lifetimes. Setup creates a small, fixed set of control resources that live as long as the feature is enabled. Everything else is created on demand as the pool grows, and those are the resources you manage day to day.

### At setup: control resources

The Terraform module creates these once. They are inexpensive to keep, and removing them disables provisioning.

**Lifecycle worker IAM role**: your data plane assumes this role to manage database infrastructure. Its permissions are narrowly scoped: every mutation (create, modify, delete) is tag-gated, so the role can only act on resources that Superblocks itself tagged at creation time. It cannot touch resources in other VPCs or resources it did not create. See [IAM permissions](#iam-permissions) for the full breakdown.

**Connector IAM role**: one role per data plane. This is the identity your data plane uses to reach app databases at query time. To run a query, the data plane assumes this role and uses the resulting credentials to sign a short-lived RDS IAM auth token, which PostgreSQL accepts in place of a password. Provisioning creates the PostgreSQL role the token authenticates as and grants it `rds_iam`; the PostgreSQL role name follows `sbndb_<profile-token>_<application-token>_runtime` (see [Connector role](#connector-role)), and is unrelated to the connector role's ARN. The trust policy limits who can assume this role to the lifecycle worker role for that data plane. See [Connector role](#connector-role) for how the permissions are narrowed on each connection.

**S3 bucket**: stores OpenTofu state so Superblocks can manage your database resources over time (modify, delete, track drift). Key properties:

* One bucket per account and region
* Versioning enabled, so state can be recovered if corrupted
* Public access blocked
* Encrypted at rest with **SSE-S3** by default (AWS-managed keys, no KMS charges). Optionally bring your own KMS key via `kms_key_arn` to use SSE-KMS. When provided, the lifecycle worker IAM policy scopes KMS access to that specific key ARN only
* Superblocks reads and writes to this bucket; you own and retain it
* State keys follow the pattern `{key_prefix}/{tag}/{resource_key}.tfstate`

### As the pool grows: one set per Aurora cluster

Your data plane creates the following each time it adds a cluster to the pool (first when you click **Provision**, and again whenever existing clusters reach `max_databases`):

* **Aurora PostgreSQL cluster and its instances**: named `sb-{hash}`, IAM authentication enabled, public accessibility disabled, storage encrypted at rest, and the master password managed by AWS Secrets Manager so there are no plaintext passwords
* **DB subnet group**: scoped to the subnets you specified in `physical_module_inputs`
* **Security group**: controls which sources (security groups or CIDRs) can reach the database on port 5432

Individual app databases are logical databases inside these clusters, each with its own PostgreSQL role. They are not separate AWS resources, so they do not appear in the RDS console as their own entries.

### How everything is tagged

Every resource above carries both `ManagedBy=superblocks-native-database-lifecycle` and `Vpc={your-vpc-id}`. These tags are what enforce the lifecycle worker role's scope: the IAM policies only allow mutations on resources carrying both, and the scoping tags cannot be stripped.

The same tags let you find everything the feature owns, either through a Resource Group or from the CLI:

```bash theme={null}
aws resourcegroupstaggingapi get-resources \
  --tag-filters Key=ManagedBy,Values=superblocks-native-database-lifecycle
```

### S3 state bucket properties

The bucket is created unconditionally: one per account and region, shared across all data planes in the same module invocation. Default naming is `sb-native-db-<REGION>-<ACCOUNT_ID>`.

When creating it manually, configure the following properties:

| Property                           | Required value              | Notes                                                                                                                                                                                                                                                    |
| ---------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Versioning                         | Enabled                     | Required for state recovery                                                                                                                                                                                                                              |
| Block public access                | All four settings enabled   | `block_public_acls`, `block_public_policy`, `ignore_public_acls`, `restrict_public_buckets`                                                                                                                                                              |
| Server-side encryption             | SSE-S3 (default) or SSE-KMS | SSE-KMS only when `kms_key_arn` is provided; use the same key ARN supplied to the module                                                                                                                                                                 |
| S3 bucket key                      | Disabled                    | Bucket keys cache a per-bucket KMS data key to reduce KMS API call volume. The module disables this (`bucket_key_enabled = false`). This is separate from `kms_key_arn`; you can use SSE-KMS with a customer-managed key and still have bucket keys off. |
| Non-current version expiration     | 90 days                     | Keeps state recoverable without unbounded storage growth                                                                                                                                                                                                 |
| Abort incomplete multipart uploads | 7 days                      |                                                                                                                                                                                                                                                          |
| Bucket policy                      | None                        | Access is controlled entirely via the IAM policies above; no explicit bucket policy is required                                                                                                                                                          |

## You own the AWS resources

Every resource lives in your account, and AWS bills you directly for it. The Superblocks defaults balance cost and performance for most customers.

**Pool growth is automatic.** As each cluster reaches its `max_databases` limit, another is added to the pool, so your spend grows with usage without another `terraform apply`. The [setup decisions](#setup-decisions) (ACU range, `instance_count`, and `max_databases`) determine what that growth costs. The admin **Databases** page shows current pool state and capacity.

### Protect the OpenTofu state bucket

The state bucket is how Superblocks tracks what it created so it can later modify, resize, or retire those resources. If the state is lost, Superblocks can no longer manage the existing databases and recovery is a manual exercise.

* Leave versioning enabled and leave the non-current version expiration alone.
* Do not rename, move, or re-encrypt the bucket, and do not manage it with another tool.
* Do not add lifecycle rules that expire current object versions.

### Do not manage the tagged resources by hand

Editing or deleting the tagged `sb-*` clusters, subnet groups, or security groups directly in the AWS console creates drift that the lifecycle worker will attempt to reconcile on its next run. Route changes through your Terraform or Helm configuration instead.

### Keep the data plane current

The data plane image contains the lifecycle worker and the database modules it applies. Fixes, new engine options, and configuration changes ship with data plane releases, so a data plane that is not upgraded falls behind on both. See [Upgrading the data plane](/enterprise/hybrid-architecture/manage/upgrade).

### What Superblocks does, and what stays yours

Superblocks creates, configures, scales, and retires the Aurora clusters and the per-app logical databases, and runs your applications' SQL migrations.

Superblocks does **not**:

* **Patch or upgrade the database engine.** AWS applies engine patches during the maintenance window you configure. Because applications share a cluster, a patch or failover affects every application on that cluster. Running a second instance (`instance_count = 2`) reduces the impact but does not remove it.
* **Restore your data.** Automated backups are configured from your `backup_retention_period`, but performing a point-in-time restore or a snapshot restore is an operation you run in AWS.
* **Tune PostgreSQL for your workload**, or manage database grants beyond the roles it creates for each application.
* **Monitor the database on your behalf.** Instance-level metrics and logs stay in your CloudWatch.

## Security

| What                | Details                                                                                                                 |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| Authentication      | IAM token via `rds-db:connect`: no passwords stored or rotated                                                          |
| Network             | The database lives in your private subnets; `publicly_accessible = false` is enforced by IAM policy                     |
| Encryption (S3)     | SSE-S3 by default (AWS-managed keys); optionally SSE-KMS with your own key via `kms_key_arn`                            |
| Encryption (Aurora) | Storage encrypted at rest; TLS in transit                                                                               |
| IAM scope           | All lifecycle worker mutations are tag-gated to resources Superblocks created in your VPC                               |
| State storage       | OpenTofu state lives in your S3 bucket; Superblocks does not retain a copy                                              |
| Master credentials  | Managed by AWS Secrets Manager; the lifecycle worker reads them by ref, so there is no plaintext in Superblocks systems |

### IAM permissions

#### Lifecycle worker role

Your data plane runs as this role to manage database infrastructure. The role carries three inline policies.

**State backend policy.** Scoped exclusively to the S3 bucket the Terraform module creates, with no wildcards:

* **S3**: `ListBucket`, `GetObject`, `PutObject`, `DeleteObject` on the state bucket only
* **KMS** (only when `kms_key_arn` is provided): `Encrypt`, `Decrypt`, `GenerateDataKey` on the specific key ARN. Superblocks cannot use any other KMS key in your account.

**RDS provisioning policy.** This is the largest policy. **Every mutation is tag-gated**: the lifecycle worker can only create, modify, or delete resources carrying both `ManagedBy=superblocks-native-database-lifecycle` and `Vpc={your-vpc-id}`. Create actions must supply both tags as request tags. Delete and modify actions require both tags to already exist on the target resource. The IAM policy only permits removing tags other than `ManagedBy` and `Vpc`, so the scoping tags cannot be stripped.

Additional constraints enforced in the policy:

* RDS instance ARNs are restricted to the `sb-*` namespace, so Superblocks cannot modify your existing RDS instances
* `rds:PubliclyAccessible` must be `false` on all create actions: the policy rejects any create that asks for a public endpoint
* `rds:ManageMasterUserPassword` must be `true` on standalone RDS creates, so master credentials are always managed by Secrets Manager and never set as a static password. This condition also discriminates standalone RDS creates from Aurora cluster member creates: Aurora member creates do not pass this key (the master password is inherited from the cluster), so `rds:ManageMasterUserPassword` evaluates to false and only the Aurora member statement matches.
* `rds:DatabaseEngine` is pinned to `postgres` (standalone RDS) and `aurora-postgresql` (Aurora cluster) at create time, so the lifecycle worker cannot provision any other engine type
* `rds:StorageEncrypted` must be `true` on standalone RDS and Aurora cluster creates. Storage encryption is enforced by IAM at create time, not only by module configuration.
* Security group mutations are scoped to the same `ManagedBy` and `Vpc` tag pair
* EC2 security group creation is additionally scoped to the specific VPC ID via the `ec2:VpcID` condition, so Superblocks cannot create resources in other VPCs in your account
* KMS access for RDS storage encryption is gated on `aws:CalledVia: rds.amazonaws.com`, so Superblocks cannot use your KMS keys directly

Allowed describe and list actions (`DescribeDBInstances`, `DescribeDBSubnetGroups`, `DescribeVpcs`, `DescribeSubnets`, and similar) are read-only and apply to `"Resource": "*"`. This is required by AWS, because describe APIs do not support resource-level restrictions, but no data is retained by Superblocks.

**Master secrets policy.** Scoped to RDS-managed master secrets (`rds!db-*` and `rds!cluster-*` ARN prefixes):

* **Read**: `GetSecretValue` and `DescribeSecret` on RDS-managed secrets tagged with `ManagedBy` and `Vpc`: the master credentials for databases Superblocks created
* **Write** (create and tag): only allowed via `aws:CalledVia: rds.amazonaws.com`. Superblocks cannot directly create Secrets Manager secrets; only RDS can, on behalf of Superblocks.
* **IAM**: `CreateServiceLinkedRole` scoped to `rds.amazonaws.com`. This is a one-time action required to create the RDS service-linked role if it does not already exist in your account.

#### Connector role

One connector role is created per data plane. The role grants `rds-db:connect` scoped to the DB users Superblocks provisions for that data plane: one statement per data tag, matching the pattern `sbndb_<profile-token>_*_runtime` in your region. `<profile-token>` is the first 16 hexadecimal characters of the SHA-256 digest of the lowercased data tag (for example, `nonprod` → `6fdc0c6b96ee8a74` and `production` → `ab8e18ef4ebebedd`). The connector role can only authenticate as DB users that Superblocks created for the declared data tags. It cannot connect as any other DB user, or to databases in your account that Superblocks did not create.

The trust policy limits who can assume the connector role to the specific lifecycle worker role ARN for that data plane. No other principal can assume it.

#### How a connection is authenticated

Neither the connector role's ARN nor the lifecycle worker's identity appears inside PostgreSQL. The database only ever sees a PostgreSQL role name and an auth token, and IAM decides whether that token is valid. The sequence for a single query is:

1. **Provisioning** creates the app's PostgreSQL role (`sbndb_<profile-token>_<application-token>_runtime`, where `<profile-token>` comes from the data tag as described above and `<application-token>` is a 24-hex hash of the application ID) and grants it `rds_iam`. That grant makes PostgreSQL delegate authentication for this role to IAM instead of checking a stored password. No password is ever generated for it.
2. **The data plane checks the target** against the connector role ARN it was configured with, and refuses the connection if they disagree. A compromised control plane cannot redirect the data plane at an IAM role its operator never approved.
3. **The data plane assumes the connector role**, attaching an inline session policy that allows exactly one action on exactly one resource: `rds-db:connect` on this cluster, as this one PostgreSQL role. The credentials it gets back are narrower than the connector role itself.
4. **It signs an RDS IAM auth token** for that PostgreSQL role with those session credentials, and connects using the token as the password, over TLS with full certificate and hostname verification.

The effective permission on any given connection is the intersection of the two policies: the connector role's own policy bounds it to `sbndb_<profile-token>_*_runtime` users for the declared data tags, and the per-connection session policy narrows that to the single user for the single app being served. One app's credentials cannot reach another app's database, even though both use the same connector role.

### Raw IAM policy JSON

The Terraform module creates the following IAM resources per data plane. If your organization requires policies to be pre-approved or manually provisioned before Terraform can attach them, use the JSON below.

Placeholders to substitute before use:

| Placeholder                   | Example value                                                                         | Description                                                                       |
| ----------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| `<REGION>`                    | `us-east-1`                                                                           | AWS region                                                                        |
| `<ACCOUNT_ID>`                | `123456789012`                                                                        | 12-digit AWS account ID                                                           |
| `<VPC_ID>`                    | `vpc-0123456789abcdef0`                                                               | VPC ID declared for this data plane                                               |
| `<BUCKET_NAME>`               | `sb-native-db-us-east-1-123456789012`                                                 | S3 state bucket name (default naming)                                             |
| `<LIFECYCLE_WORKER_ROLE_ARN>` | `arn:aws:iam::123456789012:role/sb-native-db-prod-lifecycle-worker-us-east-1`         | ARN of the lifecycle worker role                                                  |
| `<CONNECTOR_ROLE_ARN>`        | `arn:aws:iam::123456789012:role/superblocks-native-db-connector-prod`                 | ARN of the connector role                                                         |
| `<OIDC_PROVIDER_ARN>`         | `arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE` | EKS OIDC provider ARN (EKS only)                                                  |
| `<OIDC_PROVIDER_URL>`         | `oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE`                                         | OIDC URL (the ARN above with `arn:aws:iam::<ACCOUNT_ID>:oidc-provider/` stripped) |

When creating these manually, create the lifecycle worker role first (it has no dependencies), then the connector role (its trust policy references the lifecycle worker role ARN), then create and attach all policies.

<AccordionGroup>
  <Accordion title="Lifecycle worker role: trust policy (ECS Fargate)">
    Allows the ECS task service principal to assume the role.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "sts:AssumeRole",
          "Principal": {
            "Service": "ecs-tasks.amazonaws.com"
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Lifecycle worker role: trust policy (EKS with IRSA)">
    Allows the data plane pod's service account to assume the role via OIDC web identity. `<OIDC_PROVIDER_URL>` is the OIDC provider URL without the ARN prefix (everything after `oidc-provider/`). The `sub` condition scopes trust to the specific Kubernetes service account. Adjust `namespace` and `service_account_name` if you deviate from the Superblocks defaults.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": "sts:AssumeRoleWithWebIdentity",
          "Principal": {
            "Federated": "<OIDC_PROVIDER_ARN>"
          },
          "Condition": {
            "StringEquals": {
              "<OIDC_PROVIDER_URL>:aud": "sts.amazonaws.com"
            },
            "StringLike": {
              "<OIDC_PROVIDER_URL>:sub": "system:serviceaccount:superblocks:superblocks-agent"
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 1: Assume connector (sb-native-db-<AGENT_NAME>-assume-connector)">
    Allows the lifecycle worker to assume its connector role for RDS IAM authentication. Attached to the lifecycle worker role.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AssumeNativeDatabaseConnector",
          "Effect": "Allow",
          "Action": "sts:AssumeRole",
          "Resource": ["<CONNECTOR_ROLE_ARN>"]
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 2: State bucket (sb-native-db-<REGION>-<ACCOUNT_ID>)">
    Allows the lifecycle worker to read and write OpenTofu state in the shared S3 bucket.

    The last statement (`StateBucketKms`) handles bucket encryption. **Without a customer KMS key** (the default, SSE-S3), the resource is `"*"` and access is constrained to calls originating from S3, as shown below. **With a customer KMS key** (`kms_key_arn` set), replace `"Resource": "*"` with the specific key ARN and remove the `Condition` block entirely.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "StateBucketList",
          "Effect": "Allow",
          "Action": [
            "s3:GetBucketLocation",
            "s3:GetBucketVersioning",
            "s3:ListBucket"
          ],
          "Resource": "arn:aws:s3:::<BUCKET_NAME>"
        },
        {
          "Sid": "StateBucketObjectReadWrite",
          "Effect": "Allow",
          "Action": [
            "s3:AbortMultipartUpload",
            "s3:DeleteObject",
            "s3:GetObject",
            "s3:GetObjectVersion",
            "s3:PutObject"
          ],
          "Resource": "arn:aws:s3:::<BUCKET_NAME>/*"
        },
        {
          "Sid": "StateBucketKms",
          "Effect": "Allow",
          "Action": [
            "kms:Decrypt",
            "kms:DescribeKey",
            "kms:Encrypt",
            "kms:GenerateDataKey",
            "kms:ReEncryptFrom",
            "kms:ReEncryptTo"
          ],
          "Resource": "*",
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "s3.amazonaws.com"
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 3: RDS provisioning (sb-native-db-<AGENT_NAME>-rds-provisioning-<REGION>)">
    Allows the lifecycle worker to describe RDS and Aurora resources globally (required by AWS, because describe APIs do not support resource-level restrictions) and to create new RDS and Aurora resources scoped to the `sb-*` namespace and the declared VPC. All create actions enforce tagging at request time, `publicly_accessible = false`, and `manage_master_user_password = true`. The `RdsCreateDbInstance` and `RdsCreateAuroraClusterInstance` statements both cover `rds:CreateDBInstance` but are cleanly discriminated: standalone RDS creates pass `rds:ManageMasterUserPassword = true` and Aurora member creates do not, so each request matches exactly one statement.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "RdsDescribe",
          "Effect": "Allow",
          "Action": [
            "rds:DescribeDBClusterParameterGroups",
            "rds:DescribeDBClusterParameters",
            "rds:DescribeDBClusterSnapshots",
            "rds:DescribeDBClusters",
            "rds:DescribeDBEngineVersions",
            "rds:DescribeDBInstances",
            "rds:DescribeDBParameterGroups",
            "rds:DescribeDBParameters",
            "rds:DescribeDBSnapshots",
            "rds:DescribeDBSubnetGroups",
            "rds:DescribeGlobalClusters",
            "rds:DescribePendingMaintenanceActions",
            "rds:ListTagsForResource"
          ],
          "Resource": "*"
        },
        {
          "Sid": "CreateRdsServiceLinkedRole",
          "Effect": "Allow",
          "Action": "iam:CreateServiceLinkedRole",
          "Resource": "*",
          "Condition": {
            "StringEquals": {
              "iam:AWSServiceName": "rds.amazonaws.com"
            }
          }
        },
        {
          "Sid": "RdsCreateDbInstance",
          "Effect": "Allow",
          "Action": "rds:CreateDBInstance",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*"
          ],
          "Condition": {
            "Bool": {
              "rds:ManageMasterUserPassword": "true",
              "rds:PubliclyAccessible": "false",
              "rds:StorageEncrypted": "true"
            },
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "rds:DatabaseEngine": "postgres"
            }
          }
        },
        {
          "Sid": "RdsCreateAuroraCluster",
          "Effect": "Allow",
          "Action": "rds:CreateDBCluster",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "Bool": {
              "rds:ManageMasterUserPassword": "true",
              "rds:StorageEncrypted": "true"
            },
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "rds:DatabaseEngine": "aurora-postgresql"
            }
          }
        },
        {
          "Sid": "RdsCreateAuroraClusterInstance",
          "Effect": "Allow",
          "Action": "rds:CreateDBInstance",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "Bool": {
              "rds:PubliclyAccessible": "false"
            },
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "rds:DatabaseEngine": "aurora-postgresql"
            }
          }
        },
        {
          "Sid": "RdsCreateParameterGroups",
          "Effect": "Allow",
          "Action": [
            "rds:CreateDBClusterParameterGroup",
            "rds:CreateDBParameterGroup"
          ],
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateDbSubnetGroup",
          "Effect": "Allow",
          "Action": "rds:CreateDBSubnetGroup",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*",
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsTagOnCreate",
          "Effect": "Allow",
          "Action": "rds:AddTagsToResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsTagNativeSnapshotOnCreate",
          "Effect": "Allow",
          "Action": "rds:AddTagsToResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*"
          ],
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsEncryptedStorageKmsViaRds",
          "Effect": "Allow",
          "Action": [
            "kms:CreateGrant",
            "kms:Decrypt",
            "kms:DescribeKey",
            "kms:GenerateDataKey"
          ],
          "Resource": "*",
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "rds.amazonaws.com"
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 4: RDS mutation (sb-native-db-<AGENT_NAME>-rds-mutation-<REGION>)">
    Allows the lifecycle worker to modify, delete, snapshot, and retag RDS and Aurora resources it created. All mutations are gated on the `ManagedBy` and `Vpc` resource tags, so the lifecycle worker cannot modify any RDS resource it did not create. The `ForAllValues:StringNotEquals` condition in `RdsRemoveTagsExceptScopingTags` prevents the scoping tags from being stripped.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "RdsMutate",
          "Effect": "Allow",
          "Action": [
            "rds:DeleteDBCluster",
            "rds:DeleteDBClusterParameterGroup",
            "rds:DeleteDBInstance",
            "rds:DeleteDBParameterGroup",
            "rds:DeleteDBSubnetGroup",
            "rds:ModifyDBCluster",
            "rds:ModifyDBClusterParameterGroup",
            "rds:ModifyDBInstance",
            "rds:ModifyDBParameterGroup",
            "rds:ModifyDBSubnetGroup",
            "rds:RebootDBCluster",
            "rds:RebootDBInstance",
            "rds:ResetDBClusterParameterGroup",
            "rds:ResetDBParameterGroup"
          ],
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "BoolIfExists": {
              "rds:ManageMasterUserPassword": "true"
            },
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsDeleteAuroraClusterFinalSnapshot",
          "Effect": "Allow",
          "Action": "rds:DeleteDBCluster",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateSnapshotFromManagedCluster",
          "Effect": "Allow",
          "Action": "rds:CreateDBClusterSnapshot",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateNativeClusterSnapshot",
          "Effect": "Allow",
          "Action": "rds:CreateDBClusterSnapshot",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateSnapshotFromManagedInstance",
          "Effect": "Allow",
          "Action": "rds:CreateDBSnapshot",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsCreateNativeSnapshot",
          "Effect": "Allow",
          "Action": "rds:CreateDBSnapshot",
          "Resource": "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*",
          "Condition": {
            "StringEqualsIfExists": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsAddTagsToManagedResources",
          "Effect": "Allow",
          "Action": "rds:AddTagsToResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "StringEqualsIfExists": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "RdsRemoveTagsExceptScopingTags",
          "Effect": "Allow",
          "Action": "rds:RemoveTagsFromResource",
          "Resource": [
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:cluster-snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:db:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:pg:*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:snapshot:sb-*",
            "arn:aws:rds:<REGION>:<ACCOUNT_ID>:subgrp:sb-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "ForAllValues:StringNotEquals": {
              "aws:TagKeys": ["ManagedBy", "Vpc"]
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 5: EC2 networking (sb-native-db-<AGENT_NAME>-ec2-provisioning-<REGION>)">
    Allows the lifecycle worker to describe VPC and subnet topology (globally, as required by AWS) and to create and manage security groups scoped to the declared VPC. The `Ec2CreateSecurityGroupVpcScope` statement additionally restricts `CreateSecurityGroup` to the specific VPC resource via `ec2:VpcID`, so the lifecycle worker cannot create security groups in any other VPC.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "Ec2VpcDescribe",
          "Effect": "Allow",
          "Action": [
            "ec2:DescribeAccountAttributes",
            "ec2:DescribeAvailabilityZones",
            "ec2:DescribeNetworkInterfaces",
            "ec2:DescribeSecurityGroupRules",
            "ec2:DescribeSecurityGroups",
            "ec2:DescribeSubnets",
            "ec2:DescribeTags",
            "ec2:DescribeVpcAttribute",
            "ec2:DescribeVpcs"
          ],
          "Resource": "*"
        },
        {
          "Sid": "Ec2CreateSecurityGroupResource",
          "Effect": "Allow",
          "Action": ["ec2:CreateSecurityGroup"],
          "Resource": "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "Ec2CreateSecurityGroupVpcScope",
          "Effect": "Allow",
          "Action": ["ec2:CreateSecurityGroup"],
          "Resource": "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:vpc/<VPC_ID>",
          "Condition": {
            "StringEquals": {
              "ec2:VpcID": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "Ec2SecurityGroupMutate",
          "Effect": "Allow",
          "Action": [
            "ec2:AuthorizeSecurityGroupEgress",
            "ec2:AuthorizeSecurityGroupIngress",
            "ec2:DeleteSecurityGroup",
            "ec2:ModifySecurityGroupRules",
            "ec2:RevokeSecurityGroupEgress",
            "ec2:RevokeSecurityGroupIngress"
          ],
          "Resource": "*",
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "Ec2CreateTagsOnCreateSecurityGroup",
          "Effect": "Allow",
          "Action": ["ec2:CreateTags"],
          "Resource": [
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group-rule/*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>",
              "ec2:CreateAction": "CreateSecurityGroup"
            }
          }
        },
        {
          "Sid": "Ec2CreateTagsOnManagedResources",
          "Effect": "Allow",
          "Action": ["ec2:CreateTags"],
          "Resource": [
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group-rule/*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "StringEqualsIfExists": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "Ec2DeleteTagsExceptScopingTags",
          "Effect": "Allow",
          "Action": ["ec2:DeleteTags"],
          "Resource": [
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group/*",
            "arn:aws:ec2:<REGION>:<ACCOUNT_ID>:security-group-rule/*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            },
            "ForAllValues:StringNotEquals": {
              "aws:TagKeys": ["ManagedBy", "Vpc"]
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 6: Secrets Manager (sb-native-db-<AGENT_NAME>-secrets-<REGION>)">
    Allows the lifecycle worker to read RDS-managed master secrets for databases it owns. Secret creation is only permitted via `aws:CalledVia: rds.amazonaws.com`, so the lifecycle worker cannot directly create Secrets Manager secrets; only RDS can, on its behalf.

    The last statement (`DecryptRdsManagedSecretKmsKey`) handles KMS decryption for the RDS-managed master secret. **Without a customer KMS key** (`rds_secret_kms_key_arn` not set), `Resource` is `"*"` and access is constrained via `kms:ViaService` and `kms:EncryptionContext` as shown below. **With a customer KMS key**, replace `"Resource": "*"` with the specific key ARN.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "CreateRdsManagedMasterSecrets",
          "Effect": "Allow",
          "Action": "secretsmanager:CreateSecret",
          "Resource": [
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!cluster-*",
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!db-*"
          ],
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "rds.amazonaws.com"
            },
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "TagRdsManagedMasterSecretsViaRds",
          "Effect": "Allow",
          "Action": "secretsmanager:TagResource",
          "Resource": [
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!cluster-*",
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!db-*"
          ],
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "rds.amazonaws.com"
            },
            "StringEquals": {
              "aws:RequestTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:RequestTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "DescribeRdsManagedSecretKmsKeyViaRds",
          "Effect": "Allow",
          "Action": "kms:DescribeKey",
          "Resource": "*",
          "Condition": {
            "ForAnyValue:StringEquals": {
              "aws:CalledVia": "rds.amazonaws.com"
            }
          }
        },
        {
          "Sid": "ReadTaggedRdsManagedMasterSecrets",
          "Effect": "Allow",
          "Action": [
            "secretsmanager:DescribeSecret",
            "secretsmanager:GetSecretValue"
          ],
          "Resource": [
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!cluster-*",
            "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!db-*"
          ],
          "Condition": {
            "StringEquals": {
              "aws:ResourceTag/ManagedBy": "superblocks-native-database-lifecycle",
              "aws:ResourceTag/Vpc": "<VPC_ID>"
            }
          }
        },
        {
          "Sid": "DecryptRdsManagedSecretKmsKey",
          "Effect": "Allow",
          "Action": [
            "kms:Decrypt",
            "kms:DescribeKey"
          ],
          "Resource": "*",
          "Condition": {
            "StringEquals": {
              "kms:ViaService": "secretsmanager.<REGION>.amazonaws.com"
            },
            "StringLike": {
              "kms:EncryptionContext:SecretARN": [
                "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!cluster-*",
                "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:rds!db-*"
              ]
            }
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Connector role: trust policy">
    Allows only the lifecycle worker role for this data plane to assume the connector role. No other principal can assume it.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowOnlyTrustedOpa",
          "Effect": "Allow",
          "Action": "sts:AssumeRole",
          "Principal": {
            "AWS": ["<LIFECYCLE_WORKER_ROLE_ARN>"]
          }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Connector role: policy (superblocks-native-db-connector-<AGENT_NAME>)">
    Grants `rds-db:connect` for each declared data tag, scoped to DB users matching `sbndb_<profile-token>_*_runtime` on both RDS instances and Aurora clusters. `<profile-token>` is the first 16 hex characters of `SHA-256(lowercase(tag))`. For example, `printf '%s' nonprod | shasum -a 256 | cut -c1-16` yields `6fdc0c6b96ee8a74`. Add or remove statements to match your declared `agent_tags`. The example below shows `nonprod` (`6fdc0c6b96ee8a74`) and `production` (`ab8e18ef4ebebedd`).

    This is the outer bound, not the effective permission on a connection. The data plane assumes this role with a session policy allowing `rds-db:connect` on one specific cluster and DB user, so each connection is authorized for exactly the app it is serving. See [How a connection is authenticated](#how-a-connection-is-authenticated).

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "ConnectTagNonprod",
          "Effect": "Allow",
          "Action": "rds-db:connect",
          "Resource": [
            "arn:aws:rds-db:<REGION>:<ACCOUNT_ID>:dbuser:cluster-*/sbndb_6fdc0c6b96ee8a74_*_runtime",
            "arn:aws:rds-db:<REGION>:<ACCOUNT_ID>:dbuser:db-*/sbndb_6fdc0c6b96ee8a74_*_runtime"
          ]
        },
        {
          "Sid": "ConnectTagProduction",
          "Effect": "Allow",
          "Action": "rds-db:connect",
          "Resource": [
            "arn:aws:rds-db:<REGION>:<ACCOUNT_ID>:dbuser:cluster-*/sbndb_ab8e18ef4ebebedd_*_runtime",
            "arn:aws:rds-db:<REGION>:<ACCOUNT_ID>:dbuser:db-*/sbndb_ab8e18ef4ebebedd_*_runtime"
          ]
        }
      ]
    }
    ```
  </Accordion>
</AccordionGroup>
