Prerequisites
Superblocks Database runs on Aurora PostgreSQL in your own AWS account, so it requires a Hybrid or Cloud-Prem deployment with a Superblocks data plane 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 or EKS. 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 applyto 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 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.
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 theagents map, and each entry names the 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 andsbndb_<profile-token>_<application-token>_runtimefor 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 insidedeployment.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.
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.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, setexisting_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_arnto 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_arnon anagentsentry 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 thesuperblocksteam/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 asecs_env_vars, ready to inject into the ECS task definition.
- ECS Fargate
- EKS
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.Step 1a: Add both modules to your Terraform config.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.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 theagents 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.native_db_prereqs invocation with an AWS provider alias configured for that region. Each region gets its own S3 bucket.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 runstofu apply in your AWS account. You do not re-enter any configuration; it is already in the data plane.
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. 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 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 itrds_iam; the PostgreSQL role name follows sbndb_<profile-token>_<application-token>_runtime (see 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 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_arnto 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 reachmax_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
How everything is tagged
Every resource above carries bothManagedBy=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:
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 issb-native-db-<REGION>-<ACCOUNT_ID>.
When creating it manually, configure the following properties:
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 itsmax_databases limit, another is added to the pool, so your spend grows with usage without another terraform apply. The 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 taggedsb-* 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.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
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,DeleteObjecton the state bucket only - KMS (only when
kms_key_arnis provided):Encrypt,Decrypt,GenerateDataKeyon the specific key ARN. Superblocks cannot use any other KMS key in your account.
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:PubliclyAccessiblemust befalseon all create actions: the policy rejects any create that asks for a public endpointrds:ManageMasterUserPasswordmust betrueon 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), sords:ManageMasterUserPasswordevaluates to false and only the Aurora member statement matches.rds:DatabaseEngineis pinned topostgres(standalone RDS) andaurora-postgresql(Aurora cluster) at create time, so the lifecycle worker cannot provision any other engine typerds:StorageEncryptedmust betrueon 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
ManagedByandVpctag pair - EC2 security group creation is additionally scoped to the specific VPC ID via the
ec2:VpcIDcondition, 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
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:
GetSecretValueandDescribeSecreton RDS-managed secrets tagged withManagedByandVpc: 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:
CreateServiceLinkedRolescoped tords.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 grantsrds-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:- 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 itrds_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. - 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.
- The data plane assumes the connector role, attaching an inline session policy that allows exactly one action on exactly one resource:
rds-db:connecton this cluster, as this one PostgreSQL role. The credentials it gets back are narrower than the connector role itself. - 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.
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:Lifecycle worker role: trust policy (ECS Fargate)
Lifecycle worker role: trust policy (ECS Fargate)
Lifecycle worker role: trust policy (EKS with IRSA)
Lifecycle worker role: trust policy (EKS with IRSA)
<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.Policy 1: Assume connector (sb-native-db-<AGENT_NAME>-assume-connector)
Policy 1: Assume connector (sb-native-db-<AGENT_NAME>-assume-connector)
Policy 2: State bucket (sb-native-db-<REGION>-<ACCOUNT_ID>)
Policy 2: State bucket (sb-native-db-<REGION>-<ACCOUNT_ID>)
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.Policy 3: RDS provisioning (sb-native-db-<AGENT_NAME>-rds-provisioning-<REGION>)
Policy 3: RDS provisioning (sb-native-db-<AGENT_NAME>-rds-provisioning-<REGION>)
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.Policy 4: RDS mutation (sb-native-db-<AGENT_NAME>-rds-mutation-<REGION>)
Policy 4: RDS mutation (sb-native-db-<AGENT_NAME>-rds-mutation-<REGION>)
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.Policy 5: EC2 networking (sb-native-db-<AGENT_NAME>-ec2-provisioning-<REGION>)
Policy 5: EC2 networking (sb-native-db-<AGENT_NAME>-ec2-provisioning-<REGION>)
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.Policy 6: Secrets Manager (sb-native-db-<AGENT_NAME>-secrets-<REGION>)
Policy 6: Secrets Manager (sb-native-db-<AGENT_NAME>-secrets-<REGION>)
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.Connector role: trust policy
Connector role: trust policy
Connector role: policy (superblocks-native-db-connector-<AGENT_NAME>)
Connector role: policy (superblocks-native-db-connector-<AGENT_NAME>)
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.
