TypeScript Relational Database
This generator creates a new relational database project backed by Amazon Aurora (PostgreSQL or MySQL) and Prisma ORM. It generates the application code and infrastructure needed to provision and manage a database using AWS CDK or Terraform, with declarative schema definition, automatic migration deployment, and a type-safe ORM client.
Generate a Relational Database
Section titled “Generate a Relational Database”You can generate a new relational database project in two ways:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#rdb - Fill in the required parameters
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#rdbyarn nx g @aws/nx-plugin:ts#rdbnpx nx g @aws/nx-plugin:ts#rdbbunx nx g @aws/nx-plugin:ts#rdbYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#rdb --dry-runyarn nx g @aws/nx-plugin:ts#rdb --dry-runnpx nx g @aws/nx-plugin:ts#rdb --dry-runbunx nx g @aws/nx-plugin:ts#rdb --dry-runOptions
Section titled “Options”| Parameter | Type | Default | Description |
|---|---|---|---|
| name Required | string | - | Name of the database project to generate |
| directory | string | packages | The directory to store the application in. |
| subDirectory | string | - | The sub directory the project is placed in. By default this is the project name. |
| infra | aurora | none | aurora | Relational database service to provision. |
| engine | postgres | mysql | postgres | Database engine to use with the selected service. |
| databaseUser | string | dbadmin | Database admin username. Defaults to 'dbadmin'. |
| databaseName | string | - | Initial database name. Defaults to the project name. |
| framework | prisma | prisma | ORM framework to use for the generated project. |
| iac | inherit | cdk | terraform | inherit | The preferred IaC provider. By default this is inherited from your initial selection. |
| preferInstallDependencies | boolean | true | Whether to prefer installing dependencies after the generator runs. Set to false to defer installing when batching multiple generators (an install still runs if needed so subsequent generators can compute the Nx project graph); install once at the end. |
Generator Output
Section titled “Generator Output”The generator will create the following project structure in the <directory>/<name> directory:
Directoryprisma
Directorymodels
- example.prisma Example model definition
- schema.prisma Main Prisma schema (references models)
Directorysrc
- index.ts Project entry point
- prisma.ts Prisma runtime client wrapper
- utils.ts Runtime config and secret helpers
- create-db-user-handler.ts Lambda handler used to create the application database user during deployment
- migration-handler.ts Lambda handler used to run database migrations during deployment
- .gitignore Git ignore entries including generated Prisma client output
- config.json Local development connection details and runtime config key
- Dockerfile Container image definition for the migration handler
- project.json Project configuration and build targets
- prisma.config.ts Configuration for Prisma CLI
Local development scripts are shared across all database projects and generated into packages/common/scripts/:
Directorypackages/common/scripts/src/rdb
- pull-image.ts Pulls the database container image
- start-container.ts Starts a local database container
- wait-for-postgres-db.ts Waits for the local database to be ready (PostgreSQL)
- wait-for-mysql-db.ts Waits for the local database to be ready (MySQL)
Infrastructure
Section titled “Infrastructure”Since this generator vends infrastructure as code based on your chosen iacProvider, it will create a project in packages/common which includes the relevant CDK constructs or Terraform modules.
The common infrastructure as code project is structured as follows:
Directorypackages/common/constructs
Directorysrc
Directoryapp/ Constructs for infrastructure specific to a project/generator
- …
Directorycore/ Generic constructs which are reused by constructs in
app- …
- index.ts Entry point exporting constructs from
app
- project.json Project build targets and configuration
Directorypackages/common/terraform
Directorysrc
Directoryapp/ Terraform modules for infrastructure specific to a project/generator
- …
Directorycore/ Generic modules which are reused by modules in
app- …
- project.json Project build targets and configuration
Directorypackages/common/constructs/src
Directoryapp
Directorydbs
- <name>.ts Infrastructure specific to your database
Directorycore
Directoryrdb
- aurora.ts Generic Aurora database construct
Directorypackages/common/terraform/src
Directoryapp
Directorydbs
Directory<name>
- <name>.tf Module specific to your database
Directorycore
Directoryrdb
Directoryaurora
- aurora.tf Generic Aurora module
Architecture
Section titled “Architecture”The deployed database has the following architecture. By default, an Amazon RDS Proxy sits in front of the Aurora cluster to pool connections and to enable IAM authentication — see Disable RDS Proxy for the alternative. The architecture is the same whether you select the PostgreSQL or MySQL engine; only the Aurora engine flavor differs.
Local Development
Section titled “Local Development”Data Modelling
Section titled “Data Modelling”The generated project uses Prisma ORM to define your database schema and generate a type-safe client. The workflow is model-first: add or update Prisma model files under your database project’s prisma/models/ directory, then generate a migration from those model changes.
Example User model:
model User { id Int @id @default(autoincrement()) firstName String lastName String}For more details, see the official Prisma data modelling guide.
Generating the Database Client
Section titled “Generating the Database Client”The generator automatically configures the generate target to create a type-safe TypeScript Prisma client whenever you build the project. The client is written to generated/prisma (added to .gitignore).
You can also manually generate the client at any time:
pnpm nx generate <your-db-project-name>yarn nx generate <your-db-project-name>npx nx generate <your-db-project-name>bunx nx generate <your-db-project-name>Use the prisma target to run Prisma CLI commands from the workspace root:
pnpm nx run <project>:prisma generateyarn nx run <project>:prisma generatenpx nx run <project>:prisma generatebunx nx run <project>:prisma generateThe runtime wrapper in src/prisma.ts exports:
getPrisma()- loads database connection settings from AWS AppConfig and creates a Prisma client using IAM authentication
The client automatically:
- Retrieves database configuration from AWS AppConfig using
RUNTIME_CONFIG_APP_IDenvironment variable - Generates temporary authentication tokens via AWS RDS Signer for IAM authentication
- Manages SSL/TLS connections with certificate validation
- Handles connection pooling through persistent database connection pools
Creating Migrations
Section titled “Creating Migrations”After adding or updating models under prisma/models/, use migrate dev to generate migration files and apply them to your local database at the same time.
The generated prisma target automatically starts a local database container before running:
pnpm nx run <project>:prisma migrate devyarn nx run <project>:prisma migrate devnpx nx run <project>:prisma migrate devbunx nx run <project>:prisma migrate devIf you only want to generate the migration files without applying them to the local database, add --create-only:
pnpm nx run <project>:prisma migrate dev --create-onlyyarn nx run <project>:prisma migrate dev --create-onlynpx nx run <project>:prisma migrate dev --create-onlybunx nx run <project>:prisma migrate dev --create-onlyThis generates a new migration folder in prisma/migrations each time your schema changes:
Directoryprisma
Directorymigrations
Directory20260405013911_initial_migrations
- migration.sql
- migration_lock.toml
- schema.prisma
When you deploy the AWS stack, the generated infrastructure automatically applies the generated migrations to the deployed database.
Applying Existing Migrations
Section titled “Applying Existing Migrations”When you pull migration files created by other developers, use migrate deploy to apply those existing migrations to your local database.
pnpm nx run <project>:prisma migrate deployyarn nx run <project>:prisma migrate deploynpx nx run <project>:prisma migrate deploybunx nx run <project>:prisma migrate deployIn this local development flow, migrate deploy applies the migration files to your local database; it does not deploy the database to AWS.
Running Prisma Commands
Section titled “Running Prisma Commands”The generated prisma target exposes the Prisma CLI, so you can use it to run any command supported by Prisma against the local database. See the Prisma CLI reference for available commands.
pnpm nx run <project>:prisma <prisma-command>yarn nx run <project>:prisma <prisma-command>npx nx run <project>:prisma <prisma-command>bunx nx run <project>:prisma <prisma-command>Using Prisma Studio
Section titled “Using Prisma Studio”Prisma Studio is a visual editor for your local database. Use it to browse tables, inspect and edit records, filter data, follow relations, and run raw SQL via the built-in SQL console. It is useful for verifying migrations and seeding test data during development. Launch it with:
pnpm nx run <project>:prisma studioyarn nx run <project>:prisma studionpx nx run <project>:prisma studiobunx nx run <project>:prisma studioStopping the Local Database
Section titled “Stopping the Local Database”Stopping dev (e.g. with Ctrl+C) automatically removes the local database container, but preserves the named volume so your data persists across restarts.
Connecting to the Database
Section titled “Connecting to the Database”In any TypeScript project, import getPrisma from your database package and call it to get a type-safe Prisma client:
import { getPrisma } from ':my-scope/db';
const prisma = await getPrisma();const users = await prisma.user.findMany({ orderBy: { id: 'asc' } });getPrisma() returns a lazily-initialised, cached client. Subsequent calls within the same Lambda execution context reuse the existing connection pool rather than opening a new one.
The Prisma client exposes fully typed models derived from your prisma/models/ schema, giving you end-to-end type safety from the database all the way through to your API response.
Deploying your Database
Section titled “Deploying your Database”The relational database generator creates CDK or Terraform infrastructure based on your selected iacProvider.
The CDK construct is created in common/constructs. Example usage:
import { MyDatabase } from ':my-scope/common-constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); ... const db = new MyDatabase(this, 'Db', { vpc, vpcSubnets: { subnetType: SubnetType.PRIVATE_ISOLATED, } }); }}This provisions an Aurora cluster with RDS Proxy, admin credentials, application database user, runtime config registration, and migration handler.
The generated infrastructure creates two database users:
- Admin user - Created during cluster provisioning with credentials stored in AWS Secrets Manager
- Application user - Created via a Lambda custom resource with IAM authentication enabled and DML privileges (SELECT, INSERT, UPDATE, DELETE) on the application database
The Terraform module is created in common/terraform. Example usage:
module "my_database" { source = "../../common/terraform/src/app/dbs/my-database"
# Database subnets have no internet route; Lambda subnets need NAT egress. vpc_id = aws_vpc.main.id database_subnet_ids = aws_subnet.database[*].id lambda_subnet_ids = aws_subnet.private[*].id
tags = local.common_tags}This provisions an Aurora cluster with RDS Proxy, admin credentials, create-db-user Lambda, runtime config registration, migration Lambda, and container registry resources.
The database module registers its connection details under the database runtime configuration namespace. Include this namespace when instantiating the shared runtime configuration AppConfig application:
module "runtime_config_appconfig" { source = "../../common/terraform/src/core/runtime-config/appconfig"
application_name = "my-app-runtime-config" namespaces = ["connection", "agentcore", "database"]}The generated infrastructure creates two database users:
- Admin user - Created during cluster provisioning with credentials stored in AWS Secrets Manager
- Application user - Created via a Lambda function with IAM authentication enabled and DML privileges (SELECT, INSERT, UPDATE, DELETE) on the application database
The application user is automatically created with a random name and IAM authentication. The generated database client is already configured to authenticate as this user using short-lived RDS tokens, so your application code never handles database passwords.
Your VPC should include public subnets, private subnets with egress, and private isolated subnets. The database can run in private isolated subnets, while application Lambda functions should run in private subnets with egress so they can reach AWS services such as AppConfig.
Example VPC configuration
const vpc = new Vpc(this, 'Vpc', { subnetConfiguration: [ { name: 'public', subnetType: SubnetType.PUBLIC, }, { name: 'private_with_egress', subnetType: SubnetType.PRIVATE_WITH_EGRESS, }, { name: 'private_isolated', subnetType: SubnetType.PRIVATE_ISOLATED, }, ],});data "aws_availability_zones" "available" { state = "available"}
resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true}
# Isolated subnets for the database: no route to the internetresource "aws_subnet" "database" { count = 2 vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index) availability_zone = data.aws_availability_zones.available.names[count.index]}
# Private subnets with NAT egress for Lambda functions and runtimes,# so they can reach AWS services such as AppConfigresource "aws_subnet" "private" { count = 2 vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 2) availability_zone = data.aws_availability_zones.available.names[count.index]}
# Public subnet hosting the NAT gatewayresource "aws_subnet" "public" { vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, 4) availability_zone = data.aws_availability_zones.available.names[0]}
resource "aws_internet_gateway" "main" { vpc_id = aws_vpc.main.id}
resource "aws_route_table" "public" { vpc_id = aws_vpc.main.id
route { cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.main.id }}
resource "aws_route_table_association" "public" { subnet_id = aws_subnet.public.id route_table_id = aws_route_table.public.id}
resource "aws_eip" "nat" { domain = "vpc"}
resource "aws_nat_gateway" "main" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public.id depends_on = [aws_internet_gateway.main]}
resource "aws_route_table" "private" { vpc_id = aws_vpc.main.id
route { cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.main.id }}
resource "aws_route_table_association" "private" { count = 2 subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private.id}Use the connection generator to connect a project to this database — see the connection guide for the relevant compute type (e.g. FastAPI, MCP server, agent) for the infrastructure wiring required to reach it.
RDS Proxy Configuration
Section titled “RDS Proxy Configuration”The generated infrastructure includes an RDS Proxy by default, which sits between your application and the Aurora cluster. RDS Proxy provides several benefits:
- Connection pooling - Maintains a pool of database connections that can be shared across application instances, reducing the overhead of establishing new connections
- Connection resilience - Automatically handles failovers and reconnects during Aurora instance replacements or maintenance
- IAM authentication - Supports IAM-based database authentication, eliminating the need to manage database credentials in your application code
- Improved security - Enforces TLS encryption for all connections
Disable RDS Proxy
Section titled “Disable RDS Proxy”You can disable the RDS proxy as follows:
import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... enableRdsProxy: false,});When RDS Proxy is disabled, your application connects directly to the Aurora cluster endpoint.
By default, RDS Proxy is enabled. You can disable it if needed:
module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... enable_rds_proxy = false}When RDS Proxy is disabled, your application connects directly to the Aurora cluster endpoint.
SSL Requirements When Connecting Without RDS Proxy
Section titled “SSL Requirements When Connecting Without RDS Proxy”When connecting directly to the Aurora cluster (without RDS Proxy), the runtime that calls getPrisma() must trust the Amazon RDS CA bundle. The generated Prisma client enables certificate verification; how you make the CA bundle available depends on the runtime that connects to the database.
For Amazon RDS, use the global CA bundle from:
https://truststore.pki.rds.amazonaws.com/global/global-bundle.pemRuntime Container Images
Section titled “Runtime Container Images”If you prepare your own container image for the runtime, download the RDS CA bundle in your Dockerfile and add it to the operating system trust store.
RUN curl -fsSL "https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem" \ -o /etc/pki/ca-trust/source/anchors/rds-bundle.pem && \ update-ca-trustRUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates && \ rm -rf /var/lib/apt/lists/* && \ curl -fsSL "https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem" \ -o /usr/local/share/ca-certificates/rds-bundle.crt && \ update-ca-certificatesZipped Lambda Functions
Section titled “Zipped Lambda Functions”For zipped Lambda functions using Node.js 20 or later runtimes, load the Amazon RDS CA bundle by setting NODE_EXTRA_CA_CERTS:
const api = new Api(this, 'Api', { integrations: Api.defaultIntegrations(this) .withDefaultOptions({ environment: { NODE_EXTRA_CA_CERTS: '/var/runtime/ca-cert.pem', }, }) .build(),});module "api" { source = "..." ...
environment_variables = { NODE_EXTRA_CA_CERTS = "/var/runtime/ca-cert.pem" }}For more details, see the AWS Lambda SSL/TLS requirements for Amazon RDS connections. When using RDS Proxy, you do not need to configure the RDS CA bundle in the runtime that connects to the database.
Cluster Instances
Section titled “Cluster Instances”Configure the writer and reader instances for your Aurora cluster.
import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... writer: ClusterInstance.serverlessV2('writer'), readers: [ClusterInstance.serverlessV2('reader')],});module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... instance_count = 2 # 1 writer + 1 reader}Serverless Capacity
Section titled “Serverless Capacity”Control Aurora Serverless v2 scaling limits to match your workload.
import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... serverlessV2MinCapacity: 0.5, serverlessV2MaxCapacity: 8,});module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... serverless_min_capacity = 0.5 serverless_max_capacity = 8}Engine Version
Section titled “Engine Version”Pin a specific Aurora engine version.
By default, the generated local database container image matches the default Aurora engine version. If you change the Aurora engine version, it’s recommended to also use a matching local container image version for maximum compatibility. See the AWS release notes for Aurora PostgreSQL versions and Aurora MySQL versions to identify the corresponding community database version.
The local database image is configured in the localDev.image field of the generated config.json file in your database project root. Update that value when you change engine versions.
import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... engineVersion: AuroraPostgresEngineVersion.VER_17_7,});module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... engine_version = "17.7"}import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... engineVersion: AuroraMysqlEngineVersion.VER_3_12_0,});module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... engine_version = "8.0.mysql_aurora.3.12.0"}Deletion Protection
Section titled “Deletion Protection”Deletion protection is enabled by default (deletionProtection: true in CDK, deletion_protection = true in Terraform) to protect the Aurora cluster from accidental deletion.
Disable Deletion Protection
Section titled “Disable Deletion Protection”You can disable deletion protection for environments where database deletion is expected, such as short-lived development or preview stacks.
import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... deletionProtection: false,});module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... deletion_protection = false}Removal Policy
Section titled “Removal Policy”The CDK construct retains the Aurora cluster by default (removalPolicy: RemovalPolicy.RETAIN). Change this when you want CDK stack deletion to snapshot or destroy the cluster instead.
When using RemovalPolicy.DESTROY, deletion protection must also be disabled before the cluster can be deleted.
import { RemovalPolicy } from 'aws-cdk-lib';import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... removalPolicy: RemovalPolicy.SNAPSHOT,});For an ephemeral environment where the database should be deleted with the stack:
import { RemovalPolicy } from 'aws-cdk-lib';import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... deletionProtection: false, removalPolicy: RemovalPolicy.DESTROY,});Terraform does not use CDK removal policies. By default, the module creates a final snapshot on deletion (skip_final_snapshot = false). To skip the final snapshot for an ephemeral environment:
module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... deletion_protection = false skip_final_snapshot = true}Logging and Monitoring
Section titled “Logging and Monitoring”Performance Insights is enabled on the Aurora writer instance by default (encrypted with the cluster’s KMS key). You can also export the Aurora engine logs to CloudWatch Logs (postgresql for Aurora PostgreSQL; audit, error, general and slowquery for Aurora MySQL). Enable log export per database:
import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... enableCloudwatchLogs: true, enablePerformanceInsights: false, // disable if not required});module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... enable_cloudwatch_logs = true enable_performance_insights = false # disable if not required}Encryption Key Rotation
Section titled “Encryption Key Rotation”The KMS key used to encrypt the Aurora cluster and its credentials secret has automatic key rotation enabled by default. Disable it if your security policy manages rotation externally.
import { MyDatabase } from ':my-scope/common-constructs';
const db = new MyDatabase(this, 'Db', { ... enableKeyRotation: false,});module "my_database" { source = "../../common/terraform/src/app/dbs/my-database" ... enable_key_rotation = false}Limitations
Section titled “Limitations”MySQL: API Gateway Streaming Mode
Section titled “MySQL: API Gateway Streaming Mode”When using Aurora MySQL with API Gateway streaming responses (e.g. with tRPC’s httpBatchStreamLink), the Prisma MySQL client holds onto the Node.js event loop after a query completes, preventing the Lambda from flushing the stream and ending the request.
To work around this, explicitly disconnect the client in a finally block after each query so the event loop is free to exit and the streaming response can complete.
Option 1: per-procedure
export const listExampleTable = publicProcedure .output(z.array(ExampleTableSchema)) .query(async () => { const prisma = await getPrisma(); try { return await prisma.exampleTable.findMany(); } finally { await prisma.$disconnect(); } });Option 2: tRPC middleware
If you are using the middleware pattern, add the $disconnect() call to the middleware so all procedures built on it are covered automatically:
import { getPrisma } from ':my-scope/db';import { initTRPC } from '@trpc/server';
export interface IDbContext { db: Awaited<ReturnType<typeof getPrisma>>;}
export const createDbPlugin = () => { const t = initTRPC.context<IDbContext>().create(); return t.procedure.use(async (opts) => { const db = await getPrisma(); try { return await opts.next({ ctx: { ...opts.ctx, db, }, }); } finally { await db.$disconnect(); } });};MySQL: IAM Token Expiry
Section titled “MySQL: IAM Token Expiry”RDS IAM authentication tokens expire after 15 minutes. The MySQL Prisma client captures the IAM token as a static value at the time getPrisma() is called. An existing open connection is not affected, but if a new connection needs to be established after the token has expired, authentication will fail. The PostgreSQL adapter avoids this by refreshing the token dynamically each time the pool opens a new connection, but the MySQL adapter has no equivalent mechanism.
For long-running tasks such as batch jobs or data migrations, call getPrisma() at the start of each unit of work rather than once for the entire operation. Because getPrisma() always creates a fresh client and fetches a new IAM token for MySQL, this ensures each connection authenticates with a valid token.
Connections
Section titled “Connections”Use the connection generator to integrate this project with others in your workspace. The following connections involve this project: