Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,6 @@ const config: NextConfig = {
webpackMemoryOptimizations: true,
},
outputFileTracingRoot: workspaceRoot,
outputFileTracingIncludes: {
'/**/*': ['../../packages/db/certs/rds-global-bundle.pem'],
},

// Reduce memory usage during production build
productionBrowserSourceMaps: false,
Expand Down
23 changes: 8 additions & 15 deletions apps/app/prisma/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

import { RDS_CA_BUNDLE } from './rds-ca-bundle';

const globalForPrisma = global as unknown as { prisma?: PrismaClient };

const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);
Expand All @@ -24,27 +26,22 @@ function isLocalhostUrl(connectionString: string): boolean {
function createPrismaClient(): PrismaClient {
const rawUrl = process.env.DATABASE_URL!;
const isLocalhost = isLocalhostUrl(rawUrl);
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
const allowInsecure = process.env.PRISMA_ALLOW_INSECURE_TLS === '1';

let ssl:
| undefined
| { checkServerIdentity: () => undefined }
| { ca: string; checkServerIdentity: () => undefined }
| { rejectUnauthorized: false };
if (isLocalhost) {
ssl = undefined;
} else if (hasCABundle) {
// Verified TLS: rely on Node's TLS context (NODE_EXTRA_CA_CERTS adds the AWS
// RDS CA to the trust store). Skip hostname check because connections may
// traverse an AWS NLB whose hostname isn't in the RDS Proxy cert's SAN list.
// The chain check still rejects forged or wrong-CA certs.
ssl = { checkServerIdentity: () => undefined };
} else if (allowInsecure) {
ssl = { rejectUnauthorized: false };
} else {
throw new Error(
'Refusing to connect to a non-local Postgres without TLS verification. Set NODE_EXTRA_CA_CERTS to a CA bundle, or set PRISMA_ALLOW_INSECURE_TLS=1 if you intentionally want unverified TLS.',
);
// Verified TLS using the inlined AWS RDS CA bundle. Skip hostname check
// because connections may traverse an AWS NLB whose hostname isn't in the
// RDS Proxy cert's SAN list. The chain check still rejects forged or
// wrong-CA certs.
ssl = { ca: RDS_CA_BUNDLE, checkServerIdentity: () => undefined };
}

const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
Expand All @@ -57,10 +54,6 @@ function createPrismaClient(): PrismaClient {
});
}

// Lazy initialization. Importing this module does NOT construct a Prisma client
// — that only happens on first property access on `db`. Critical so that
// Next.js `next build` (which imports every route handler to analyze it) does
// not trigger the strict TLS check at build time when no actual queries run.
function getClient(): PrismaClient {
if (!globalForPrisma.prisma) {
globalForPrisma.prisma = createPrismaClient();
Expand Down
7 changes: 7 additions & 0 deletions apps/app/prisma/rds-ca-bundle.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions apps/app/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"regions": ["iad1"]
}
19 changes: 4 additions & 15 deletions apps/framework-editor/prisma/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

import { RDS_CA_BUNDLE } from './rds-ca-bundle';
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Avoid editing generated prisma client artifacts directly. Apply this TLS change in the canonical source (packages/db/src/client.ts) and regenerate the app client so the change persists.

(Based on your team's feedback about treating apps/*/prisma client files as generated artifacts.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/framework-editor/prisma/client.ts, line 4:

<comment>Avoid editing generated prisma client artifacts directly. Apply this TLS change in the canonical source (packages/db/src/client.ts) and regenerate the app client so the change persists.

(Based on your team's feedback about treating apps/*/prisma client files as generated artifacts.) </comment>

<file context>
@@ -1,6 +1,8 @@
 import { PrismaClient } from '@prisma/client';
 import { PrismaPg } from '@prisma/adapter-pg';
 
+import { RDS_CA_BUNDLE } from './rds-ca-bundle';
+
 const globalForPrisma = global as unknown as { prisma?: PrismaClient };
</file context>
Fix with Cubic


const globalForPrisma = global as unknown as { prisma?: PrismaClient };

const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);
Expand All @@ -24,27 +26,18 @@ function isLocalhostUrl(connectionString: string): boolean {
function createPrismaClient(): PrismaClient {
const rawUrl = process.env.DATABASE_URL!;
const isLocalhost = isLocalhostUrl(rawUrl);
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
const allowInsecure = process.env.PRISMA_ALLOW_INSECURE_TLS === '1';

let ssl:
| undefined
| { checkServerIdentity: () => undefined }
| { ca: string; checkServerIdentity: () => undefined }
| { rejectUnauthorized: false };
if (isLocalhost) {
ssl = undefined;
} else if (hasCABundle) {
// Verified TLS: rely on Node's TLS context (NODE_EXTRA_CA_CERTS adds the AWS
// RDS CA to the trust store). Skip hostname check because connections may
// traverse an AWS NLB whose hostname isn't in the RDS Proxy cert's SAN list.
// The chain check still rejects forged or wrong-CA certs.
ssl = { checkServerIdentity: () => undefined };
} else if (allowInsecure) {
ssl = { rejectUnauthorized: false };
} else {
throw new Error(
'Refusing to connect to a non-local Postgres without TLS verification. Set NODE_EXTRA_CA_CERTS to a CA bundle, or set PRISMA_ALLOW_INSECURE_TLS=1 if you intentionally want unverified TLS.',
);
ssl = { ca: RDS_CA_BUNDLE, checkServerIdentity: () => undefined };
}

const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
Expand All @@ -57,10 +50,6 @@ function createPrismaClient(): PrismaClient {
});
}

// Lazy initialization. Importing this module does NOT construct a Prisma client
// — that only happens on first property access on `db`. Critical so that
// Next.js `next build` (which imports every route handler to analyze it) does
// not trigger the strict TLS check at build time when no actual queries run.
function getClient(): PrismaClient {
if (!globalForPrisma.prisma) {
globalForPrisma.prisma = createPrismaClient();
Expand Down
7 changes: 7 additions & 0 deletions apps/framework-editor/prisma/rds-ca-bundle.ts

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions apps/portal/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ const config = {
},
skipTrailingSlashRedirect: true,
outputFileTracingRoot: path.join(__dirname, '../../'),
outputFileTracingIncludes: {
'/**/*': ['../../packages/db/certs/rds-global-bundle.pem'],
},
...(isStandalone
? {
output: 'standalone' as const,
Expand Down
19 changes: 4 additions & 15 deletions apps/portal/prisma/client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { PrismaClient } from '../src/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';

import { RDS_CA_BUNDLE } from './rds-ca-bundle';

const globalForPrisma = global as unknown as { prisma?: PrismaClient };

const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);
Expand All @@ -24,27 +26,18 @@ function isLocalhostUrl(connectionString: string): boolean {
function createPrismaClient(): PrismaClient {
const rawUrl = process.env.DATABASE_URL!;
const isLocalhost = isLocalhostUrl(rawUrl);
const hasCABundle = !!process.env.NODE_EXTRA_CA_CERTS;
const allowInsecure = process.env.PRISMA_ALLOW_INSECURE_TLS === '1';

let ssl:
| undefined
| { checkServerIdentity: () => undefined }
| { ca: string; checkServerIdentity: () => undefined }
| { rejectUnauthorized: false };
if (isLocalhost) {
ssl = undefined;
} else if (hasCABundle) {
// Verified TLS: rely on Node's TLS context (NODE_EXTRA_CA_CERTS adds the AWS
// RDS CA to the trust store). Skip hostname check because connections may
// traverse an AWS NLB whose hostname isn't in the RDS Proxy cert's SAN list.
// The chain check still rejects forged or wrong-CA certs.
ssl = { checkServerIdentity: () => undefined };
} else if (allowInsecure) {
ssl = { rejectUnauthorized: false };
} else {
throw new Error(
'Refusing to connect to a non-local Postgres without TLS verification. Set NODE_EXTRA_CA_CERTS to a CA bundle, or set PRISMA_ALLOW_INSECURE_TLS=1 if you intentionally want unverified TLS.',
);
ssl = { ca: RDS_CA_BUNDLE, checkServerIdentity: () => undefined };
}

const url = ssl !== undefined ? stripSslMode(rawUrl) : rawUrl;
Expand All @@ -57,10 +50,6 @@ function createPrismaClient(): PrismaClient {
});
}

// Lazy initialization. Importing this module does NOT construct a Prisma client
// — that only happens on first property access on `db`. Critical so that
// Next.js `next build` (which imports every route handler to analyze it) does
// not trigger the strict TLS check at build time when no actual queries run.
function getClient(): PrismaClient {
if (!globalForPrisma.prisma) {
globalForPrisma.prisma = createPrismaClient();
Expand Down
7 changes: 7 additions & 0 deletions apps/portal/prisma/rds-ca-bundle.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions apps/portal/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"regions": ["iad1"]
}
78 changes: 43 additions & 35 deletions docs/plans/secure-rds-tls-deploy-checklist.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,32 @@
# Secure RDS TLS — Deploy Checklist

After merging the secure-rds-tls PR, the following env vars must be set per environment.

## Vercel (apps/app and apps/portal)

Set on each Vercel project, all environments (Production + Preview + Development):

```
NODE_EXTRA_CA_CERTS=/var/task/packages/db/certs/rds-global-bundle.pem
```

Verified on staging (apps/app): `process.cwd()` is `/var/task/apps/app`, the cert is traced
into the deploy at `/var/task/packages/db/certs/rds-global-bundle.pem` (165408 bytes), and
`/api/health` succeeds end-to-end. The cert is bundled via `outputFileTracingIncludes` in
each app's `next.config.ts`.
**No env var or `outputFileTracingIncludes` config required.** The AWS RDS CA
bundle is inlined as a TypeScript constant (`RDS_CA_BUNDLE`) and passed
directly to the Postgres adapter via `ssl.ca`. This works under both Webpack
and Turbopack since it's just a string the bundler always emits.

## Downstream consumers (comp-private/apps/enterprise-api, etc.)
Background: `outputFileTracingIncludes` is silently no-op'd under Turbopack
(`next/dist/build/index.js` line ~1537 gates `collectBuildTraces` on
`bundler !== Bundler.Turbopack`). All current Vercel deployments use Turbopack
(metadata `bundler: "turbopack"`), which is why the file-based approach from
PR #2761 failed in production for app-router page routes.

The CA bundle now ships with the published `@trycompai/db` package (added to the `files` array
in this PR). After the next `@trycompai/db` publish, downstream consumers can ship the cert with
their own Vercel/Docker/Trigger.dev builds without committing a copy.

For Vercel-deployed apps that install `@trycompai/db` from npm:

1. Bump the dependency to the version that includes `certs/`.
2. Add `outputFileTracingIncludes` to `next.config.{ts,mjs}`:
```ts
outputFileTracingIncludes: {
'/**/*': ['./node_modules/@trycompai/db/certs/rds-global-bundle.pem'],
},
```
3. Set the Vercel env var:
```
NODE_EXTRA_CA_CERTS=/var/task/node_modules/@trycompai/db/certs/rds-global-bundle.pem
```
4. Apply the same strict-TLS Prisma client logic (or import a shared helper from `@trycompai/db`).
If `NODE_EXTRA_CA_CERTS` is still set as a shared/team Vercel env var,
**unset it** — when the path doesn't exist on the function, Node logs a
`Warning: Ignoring extra certs from … load failed: error:80000002:system
library` for every cold start.

## Trigger.dev (api and app projects, staging + prod)

After deploying with the `caBundleExtension` (already wired in `trigger.config.ts`), remove the
legacy opt-in that bypasses TLS verification:
The `caBundleExtension` and `NODE_EXTRA_CA_CERTS` setup in `trigger.config.ts`
remain as-is — Trigger.dev images bake the cert into
`/app/certs/rds-global-bundle.pem`. The shared `@trycompai/db` client falls
through to verified TLS via the inline bundle either way.

If `PRISMA_ALLOW_INSECURE_TLS` is still set as a leftover from earlier
debugging, remove it:

```bash
bunx trigger.dev@4.4.3 envvars remove PRISMA_ALLOW_INSECURE_TLS --env staging
Expand All @@ -48,5 +35,26 @@ bunx trigger.dev@4.4.3 envvars remove PRISMA_ALLOW_INSECURE_TLS --env prod

## API Docker (apps/api)

No action — `apps/api/Dockerfile.multistage` already installs the RDS CA bundle and sets
`NODE_EXTRA_CA_CERTS` at the system level.
No action — `apps/api/Dockerfile.multistage` already installs the RDS CA bundle
and sets `NODE_EXTRA_CA_CERTS` at the system level. `apps/api/prisma/client.ts`
still consults the env var, which is the correct path for that runtime.

## Downstream consumers (comp-private/apps/enterprise-api, etc.)

After bumping `@trycompai/db` to a version that includes the inline bundle,
consumers that import `resolveSslConfig` from `@trycompai/db/ssl-config`
automatically get verified TLS via the inline bundle — no env var required.
They can drop their own `NODE_EXTRA_CA_CERTS` and `outputFileTracingIncludes`
on the new version.

## Regenerating the inlined CA bundle

When AWS rotates the RDS CA, replace the PEM and regenerate:

```bash
# overwrite packages/db/certs/rds-global-bundle.pem with the new bundle
node packages/db/scripts/generate-ca-bundle-ts.mjs
```

This rewrites the inlined `rds-ca-bundle.ts` in `packages/db/src` and in each
app's `prisma/` directory.
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@trycompai/db",
"description": "Database package with Prisma client and schema for Comp AI",
"version": "2.1.1",
"version": "2.2.0",
"dependencies": {
"@prisma/adapter-pg": "7.6.0",
"@prisma/client": "7.6.0",
Expand Down
32 changes: 32 additions & 0 deletions packages/db/scripts/generate-ca-bundle-ts.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node
import { readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const pemPath = join(__dirname, '..', 'certs', 'rds-global-bundle.pem');
const monorepoRoot = join(__dirname, '..', '..', '..');

const pem = readFileSync(pemPath, 'utf8');
const escaped = JSON.stringify(pem);

const banner =
'// Auto-generated from packages/db/certs/rds-global-bundle.pem.\n' +
'// Do not edit by hand — run `bun run packages/db/scripts/generate-ca-bundle-ts.mjs` to regenerate.\n' +
'// The cert is inlined as a string so it lands in every bundler\'s output (Webpack, Turbopack, Rollup).\n' +
'// Background: Next.js `outputFileTracingIncludes` is silently ignored under Turbopack builds, so file-based\n' +
'// approaches that rely on tracing don\'t work. See `next/dist/build/index.js` line ~1537.\n\n';

const body = `export const RDS_CA_BUNDLE = ${escaped};\n`;

const targets = [
join(monorepoRoot, 'packages/db/src/rds-ca-bundle.ts'),
join(monorepoRoot, 'apps/app/prisma/rds-ca-bundle.ts'),
join(monorepoRoot, 'apps/portal/prisma/rds-ca-bundle.ts'),
join(monorepoRoot, 'apps/framework-editor/prisma/rds-ca-bundle.ts'),
];

for (const target of targets) {
writeFileSync(target, banner + body);
console.log(`wrote ${target}`);
}
7 changes: 7 additions & 0 deletions packages/db/src/rds-ca-bundle.ts

Large diffs are not rendered by default.

25 changes: 10 additions & 15 deletions packages/db/src/ssl-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { RDS_CA_BUNDLE } from './rds-ca-bundle';

export type SslConfig =
| undefined
| { checkServerIdentity: () => undefined }
| { ca: string; checkServerIdentity: () => undefined }
| { rejectUnauthorized: false };

const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1']);
Expand All @@ -21,18 +23,11 @@ export function resolveSslConfig(
databaseUrl: string,
env: Partial<NodeJS.ProcessEnv> = process.env,
): SslConfig {
const isLocalhost = isLocalhostUrl(databaseUrl);
const hasCABundle = !!env.NODE_EXTRA_CA_CERTS;
const allowInsecure = env.PRISMA_ALLOW_INSECURE_TLS === '1';

if (isLocalhost) return undefined;
// Verified TLS: rely on Node's TLS context (NODE_EXTRA_CA_CERTS adds the AWS
// RDS CA to the trust store). Skip hostname check because connections may
// traverse an AWS NLB whose hostname isn't in the RDS Proxy cert's SAN list.
// The chain check still rejects forged or wrong-CA certs.
if (hasCABundle) return { checkServerIdentity: () => undefined };
if (allowInsecure) return { rejectUnauthorized: false };
throw new Error(
'Refusing to connect to a non-local Postgres without TLS verification. Set NODE_EXTRA_CA_CERTS to a CA bundle, or set PRISMA_ALLOW_INSECURE_TLS=1 if you intentionally want unverified TLS.',
);
if (isLocalhostUrl(databaseUrl)) return undefined;
if (env.PRISMA_ALLOW_INSECURE_TLS === '1') return { rejectUnauthorized: false };
// Verified TLS using the inlined AWS RDS CA bundle. Skip the hostname check
// because connections may traverse an AWS NLB whose hostname isn't in the
// RDS Proxy cert's SAN list. The chain check still rejects forged or
// wrong-CA certs.
return { ca: RDS_CA_BUNDLE, checkServerIdentity: () => undefined };
}
Loading