Skip to content
This repository was archived by the owner on Apr 9, 2026. It is now read-only.
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
9 changes: 9 additions & 0 deletions auth-provider/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
DATABASE_URL=postgresql://[user]:[password]@[host]:[port]/[database]
DB_CONNECTION_MODE=direct
DB_SCHEMA=public

# Required when DB_CONNECTION_MODE=connector
# CLOUD_SQL_CONNECTION_NAME=[project-id]:[region]:[instance-id]
# DB_USER=[database-user]
# DB_PASSWORD=[database-password]
# DB_NAME=[database-name]
# CLOUD_SQL_IP_TYPE=PUBLIC

# `openssl rand -hex 32`
NEXTAUTH_SECRET=****
Expand Down
9 changes: 9 additions & 0 deletions auth-provider/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ NEXTAUTH_URL="http://localhost:3000"

# Database
DATABASE_URL="your-postgresql-connection-string"
DB_CONNECTION_MODE="direct" # "direct" (default) or "connector"
DB_SCHEMA="public" # PostgreSQL schema/search_path

# Required when DB_CONNECTION_MODE=connector
CLOUD_SQL_CONNECTION_NAME="project:region:instance"
DB_USER="your-db-user"
DB_PASSWORD="your-db-password"
DB_NAME="your-db-name"
CLOUD_SQL_IP_TYPE="PUBLIC" # PUBLIC or PRIVATE

# Email Verification
TWILIO_SENDGRID_API_KEY="your-sendgrid-api-key"
Expand Down
2 changes: 2 additions & 0 deletions auth-provider/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
import SignOutButton from './components/SignOutButton';

export const dynamic = 'force-dynamic';

interface User {
name: string;
email: string;
Expand Down
35 changes: 35 additions & 0 deletions auth-provider/apphosting.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,46 @@ runConfig:

# Environment variables and secrets.
env:
- variable: DB_CONNECTION_MODE
secret: provider-db-connection-mode
availability:
- BUILD
- RUNTIME
- variable: DB_SCHEMA
secret: provider-db-schema
availability:
- BUILD
- RUNTIME
- variable: DATABASE_URL
secret: provider-database-url
availability:
- BUILD
- RUNTIME
- variable: CLOUD_SQL_CONNECTION_NAME
secret: provider-cloud-sql-connection-name
availability:
- BUILD
- RUNTIME
- variable: DB_USER
secret: provider-db-user
availability:
- BUILD
- RUNTIME
- variable: DB_PASSWORD
secret: provider-db-password
availability:
- BUILD
- RUNTIME
- variable: DB_NAME
secret: provider-db-name
availability:
- BUILD
- RUNTIME
- variable: CLOUD_SQL_IP_TYPE
secret: provider-cloud-sql-ip-type
availability:
- BUILD
- RUNTIME
- variable: NEXTAUTH_SECRET
secret: provider-nextauth-secret
availability:
Expand Down
143 changes: 140 additions & 3 deletions auth-provider/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,150 @@
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import { Connector, IpAddressTypes } from '@google-cloud/cloud-sql-connector';
import * as schema from './schema';

const pool = new Pool({
connectionString: process.env.DATABASE_URL,
let connector: Connector | null = null;
let poolPromise: Promise<Pool> | null = null;

function getSearchPath(): string {
const raw = process.env.DB_SCHEMA ?? 'public';
const schemaNames = raw
.split(',')
.map(schemaName => schemaName.trim())
.filter(Boolean);

if (schemaNames.length === 0) {
throw new Error('DB_SCHEMA must include at least one schema name.');
}

const validSchemaName = /^[A-Za-z_][A-Za-z0-9_]*$/;
for (const schemaName of schemaNames) {
if (!validSchemaName.test(schemaName)) {
throw new Error(
'DB_SCHEMA contains invalid schema names. Use comma-separated schema names with letters, numbers, and underscores only.'
);
}
}

return schemaNames.map(schemaName => `"${schemaName}"`).join(',');
}

function hasSearchPathInConnectionString(connectionString: string): boolean {
try {
const parsed = new URL(connectionString);
if (parsed.searchParams.has('search_path') || parsed.searchParams.has('currentSchema')) {
return true;
}

const options = parsed.searchParams.get('options');
return options ? /\bsearch_path\s*=/.test(decodeURIComponent(options)) : false;
} catch {
return false;
}
}

async function createCloudSqlPool(): Promise<Pool> {
const instanceConnectionName = process.env.CLOUD_SQL_CONNECTION_NAME;
const dbUser = process.env.DB_USER;
const dbPassword = process.env.DB_PASSWORD;
const dbName = process.env.DB_NAME;

if (!instanceConnectionName || !dbUser || !dbPassword || !dbName) {
throw new Error(
'Cloud SQL Connector requires CLOUD_SQL_CONNECTION_NAME, DB_USER, DB_PASSWORD, and DB_NAME.'
);
}

const validTypes = Object.values(IpAddressTypes) as string[];
const ipAddressType = validTypes.includes(process.env.CLOUD_SQL_IP_TYPE ?? '')
? (process.env.CLOUD_SQL_IP_TYPE as IpAddressTypes)
: IpAddressTypes.PUBLIC;

connector = new Connector();
const clientOpts = await connector.getOptions({
instanceConnectionName,
ipType: ipAddressType,
});
const searchPath = getSearchPath();

return new Pool({
...clientOpts,
user: dbUser,
password: dbPassword,
database: dbName,
options: `-c search_path=${searchPath}`,
});
}
Comment thread
evanpetzoldt marked this conversation as resolved.

function createDirectPool(): Pool {
const connectionString = process.env.DATABASE_URL;

if (!connectionString) {
throw new Error('DATABASE_URL is missing. Cannot connect to the database.');
}

const searchPath = getSearchPath();
if (hasSearchPathInConnectionString(connectionString)) {
return new Pool({ connectionString });
}

return new Pool({ connectionString, options: `-c search_path=${searchPath}` });
}

Comment on lines +79 to +93

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@copilot DATABASE_URL already has a search path in it... will this break by adding it again in the options argument of the Pool constructor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — I updated this in 8334ae8. In direct mode, if DATABASE_URL already includes search_path/currentSchema (or options with search_path), we now skip adding Pool options so we don’t duplicate/override it. We only add options when the URL does not already set it. UI screenshot: N/A (backend-only change).

async function createPool(): Promise<Pool> {
const mode = process.env.DB_CONNECTION_MODE ?? 'direct';
return mode === 'connector' ? createCloudSqlPool() : createDirectPool();
}

async function getPool(): Promise<Pool> {
if (!poolPromise) {
poolPromise = createPool();
poolPromise
.then(pool => {
pool.on('error', err => {
console.error('Unexpected error on idle PostgreSQL client:', err);
});
})
.catch(err => {
console.error('Failed to initialize PostgreSQL pool:', err);
});
}

return poolPromise;
}

const poolProxy = {
async query(...args: Parameters<Pool['query']>) {
const pool = await getPool();
return pool.query(...args);
},
async connect(...args: Parameters<Pool['connect']>) {
const pool = await getPool();
return pool.connect(...args);
},
async end(...args: Parameters<Pool['end']>) {
const pool = await getPool();
return pool.end(...args);
},
} as unknown as Pool;

async function closeConnector() {
if (connector) {
await connector.close();
connector = null;
}
}

process.once('SIGTERM', () => {
void closeConnector();
});

process.once('SIGINT', () => {
void closeConnector();
});

// Use drizzle to wrap the PG pool with schema types
export const db = drizzle(pool, { schema });
export const db = drizzle(poolProxy, { schema });

// Export the database type for use in adapters
export type DB = typeof db;
Loading
Loading