{"slug":"azure-postgres-ts","title":"azure-postgres-ts","summary":"Connect to Azure Database for PostgreSQL Flexible Server from Node.js/TypeScript using the pg (node-postgres) package. Use for PostgreSQL queries, connection pooling, transactions, and Microsoft Entra ID (passwordless) authentication. Triggers: \"PostgreSQL\", \"postgres\", \"pg clien","platform":"GitHub Copilot","tags":[],"authorName":"Ciza","authorSlug":"ciza","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-08-12T21:05:17.545899Z","repo":{"url":"https://github.com/microsoft/skills","stars":3052,"forks":351,"license":"MIT","updatedAt":"2026-09-24T16:38:17Z"},"bodyHtml":"<hr>\n<h2>name: azure-postgres-ts\ndescription: |\nConnect to Azure Database for PostgreSQL Flexible Server from Node.js/TypeScript using the pg (node-postgres) package. Use for PostgreSQL queries, connection pooling, transactions, and Microsoft Entra ID (passwordless) authentication. Triggers: \"PostgreSQL\", \"postgres\", \"pg client\", \"node-postgres\", \"Azure PostgreSQL connection\", \"PostgreSQL TypeScript\", \"pg Pool\", \"passwordless postgres\".\nlicense: MIT\nmetadata:\nauthor: Microsoft\nversion: \"1.0.0\"\npackage: pg</h2>\n<h1>Azure PostgreSQL for TypeScript (node-postgres)</h1>\n<p>Connect to Azure Database for PostgreSQL Flexible Server using the <code>pg</code> (node-postgres) package with support for password and Microsoft Entra ID (passwordless) authentication.</p>\n<h2>Installation</h2>\n<pre><code>npm install pg @azure/identity\nnpm install -D @types/pg\n</code></pre>\n<h2>Environment Variables</h2>\n<pre><code># Required\nAZURE_POSTGRESQL_HOST=&lt;server&gt;.postgres.database.azure.com\nAZURE_POSTGRESQL_DATABASE=&lt;database&gt;\nAZURE_POSTGRESQL_PORT=5432\n\n# For password authentication\nAZURE_POSTGRESQL_USER=&lt;username&gt;\nAZURE_POSTGRESQL_PASSWORD=&lt;password&gt;\n\n# For Entra ID authentication\nAZURE_POSTGRESQL_USER=&lt;entra-user&gt;@&lt;server&gt;   # e.g., user@contoso.com\nAZURE_POSTGRESQL_CLIENTID=&lt;managed-identity-client-id&gt;  # For user-assigned identity\nAZURE_TOKEN_CREDENTIALS=prod # Required only if DefaultAzureCredential is used in production\n</code></pre>\n<h2>Authentication</h2>\n<h3>Option 1: Password Authentication</h3>\n<pre><code>import { Client, Pool } from \"pg\";\n\nconst client = new Client({\n  host: process.env.AZURE_POSTGRESQL_HOST,\n  database: process.env.AZURE_POSTGRESQL_DATABASE,\n  user: process.env.AZURE_POSTGRESQL_USER,\n  password: process.env.AZURE_POSTGRESQL_PASSWORD,\n  port: Number(process.env.AZURE_POSTGRESQL_PORT) || 5432,\n  ssl: { rejectUnauthorized: true }  // Required for Azure\n});\n\nawait client.connect();\n</code></pre>\n<h3>Option 2: Microsoft Entra ID (Passwordless) - Recommended</h3>\n<pre><code>import { Client, Pool } from \"pg\";\nimport { DefaultAzureCredential, ManagedIdentityCredential } from \"@azure/identity\";\n\n// Local dev: DefaultAzureCredential. Production: set AZURE_TOKEN_CREDENTIALS=prod or AZURE_TOKEN_CREDENTIALS=&lt;specific_credential&gt;\nconst credential = new DefaultAzureCredential({requiredEnvVars: [\"AZURE_TOKEN_CREDENTIALS\"]});\n// Or use a specific credential directly in production:\n// See https://learn.microsoft.com/javascript/api/overview/azure/identity-readme?view=azure-node-latest#credential-classes\n// const credential = new ManagedIdentityCredential();\n\n// For user-assigned managed identity\n// const credential = new DefaultAzureCredential({\n//   managedIdentityClientId: process.env.AZURE_POSTGRESQL_CLIENTID\n// });\n\n// Acquire access token for Azure PostgreSQL\nconst tokenResponse = await credential.getToken(\n  \"https://ossrdbms-aad.database.windows.net/.default\"\n);\n\nconst client = new Client({\n  host: process.env.AZURE_POSTGRESQL_HOST,\n  database: process.env.AZURE_POSTGRESQL_DATABASE,\n  user: process.env.AZURE_POSTGRESQL_USER,  // Entra ID user\n  password: tokenResponse.token,             // Token as password\n  port: Number(process.env.AZURE_POSTGRESQL_PORT) || 5432,\n  ssl: { rejectUnauthorized: true }\n});\n\nawait client.connect();\n</code></pre>\n<h2>Core Workflows</h2>\n<h3>1. Single Client Connection</h3>\n<pre><code>import { Client } from \"pg\";\n\nconst client = new Client({\n  host: process.env.AZURE_POSTGRESQL_HOST,\n  database: process.env.AZURE_POSTGRESQL_DATABASE,\n  user: process.env.AZURE_POSTGRESQL_USER,\n  password: process.env.AZURE_POSTGRESQL_PASSWORD,\n  port: 5432,\n  ssl: { rejectUnauthorized: true }\n});\n\ntry {\n  await client.connect();\n  \n  const result = await client.query(\"SELECT NOW() as current_time\");\n  console.log(result.rows[0].current_time);\n} finally {\n  await client.end();  // Always close connection\n}\n</code></pre>\n<h3>2. Connection Pool (Recommended for Production)</h3>\n<pre><code>import { Pool } from \"pg\";\n\nconst pool = new Pool({\n  host: process.env.AZURE_POSTGRESQL_HOST,\n  database: process.env.AZURE_POSTGRESQL_DATABASE,\n  user: process.env.AZURE_POSTGRESQL_USER,\n  password: process.env.AZURE_POSTGRESQL_PASSWORD,\n  port: 5432,\n  ssl: { rejectUnauthorized: true },\n  \n  // Pool configuration\n  max: 20,                    // Maximum connections in pool\n  idleTimeoutMillis: 30000,   // Close idle connections after 30s\n  connectionTimeoutMillis: 10000  // Timeout for new connections\n});\n\n// Query using pool (automatically acquires and releases connection)\nconst result = await pool.query(\"SELECT * FROM users WHERE id = $1\", [userId]);\n\n// Explicit checkout for multiple queries\nconst client = await pool.connect();\ntry {\n  const res1 = await client.query(\"SELECT * FROM users\");\n  const res2 = await client.query(\"SELECT * FROM orders\");\n} finally {\n  client.release();  // Return connection to pool\n}\n\n// Cleanup on shutdown\nawait pool.end();\n</code></pre>\n<h3>3. Parameterized Queries (Prevent SQL Injection)</h3>\n<pre><code>// ALWAYS use parameterized queries - never concatenate user input\nconst userId = 123;\nconst email = \"user@example.com\";\n\n// Single parameter\nconst result = await pool.query(\n  \"SELECT * FROM users WHERE id = $1\",\n  [userId]\n);\n\n// Multiple parameters\nconst result = await pool.query(\n  \"INSERT INTO users (email, name, created_at) VALUES ($1, $2, NOW()) RETURNING *\",\n  [email, \"John Doe\"]\n);\n\n// Array parameter\nconst ids = [1, 2, 3, 4, 5];\nconst result = await pool.query(\n  \"SELECT * FROM users WHERE id = ANY($1::int[])\",\n  [ids]\n);\n</code></pre>\n<h3>4. Transactions</h3>\n<pre><code>const client = await pool.connect();\n\ntry {\n  await client.query(\"BEGIN\");\n  \n  const userResult = await client.query(\n    \"INSERT INTO users (email) VALUES ($1) RETURNING id\",\n    [\"user@example.com\"]\n  );\n  const userId = userResult.rows[0].id;\n  \n  await client.query(\n    \"INSERT INTO orders (user_id, total) VALUES ($1, $2)\",\n    [userId, 99.99]\n  );\n  \n  await client.query(\"COMMIT\");\n} catch (error) {\n  await client.query(\"ROLLBACK\");\n  throw error;\n} finally {\n  client.release();\n}\n</code></pre>\n<h3>5. Transaction Helper Function</h3>\n<pre><code>async function withTransaction&lt;T&gt;(\n  pool: Pool,\n  fn: (client: PoolClient) =&gt; Promise&lt;T&gt;\n): Promise&lt;T&gt; {\n  const client = await pool.connect();\n  try {\n    await client.query(\"BEGIN\");\n    const result = await fn(client);\n    await client.query(\"COMMIT\");\n    return result;\n  } catch (error) {\n    await client.query(\"ROLLBACK\");\n    throw error;\n  } finally {\n    client.release();\n  }\n}\n\n// Usage\nconst order = await withTransaction(pool, async (client) =&gt; {\n  const user = await client.query(\n    \"INSERT INTO users (email) VALUES ($1) RETURNING *\",\n    [\"user@example.com\"]\n  );\n  const order = await client.query(\n    \"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING *\",\n    [user.rows[0].id, 99.99]\n  );\n  return order.rows[0];\n});\n</code></pre>\n<h3>6. Typed Queries with TypeScript</h3>\n<pre><code>import { Pool, QueryResult } from \"pg\";\n\ninterface User {\n  id: number;\n  email: string;\n  name: string;\n  created_at: Date;\n}\n\n// Type the query result\nconst result: QueryResult&lt;User&gt; = await pool.query&lt;User&gt;(\n  \"SELECT * FROM users WHERE id = $1\",\n  [userId]\n);\n\nconst user: User | undefined = result.rows[0];\n\n// Type-safe insert\nasync function createUser(\n  pool: Pool,\n  email: string,\n  name: string\n): Promise&lt;User&gt; {\n  const result = await pool.query&lt;User&gt;(\n    \"INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *\",\n    [email, name]\n  );\n  return result.rows[0];\n}\n</code></pre>\n<h2>Pool with Entra ID Token Refresh</h2>\n<p>For long-running applications, tokens expire and need refresh:</p>\n<pre><code>import { Pool, PoolConfig } from \"pg\";\nimport { DefaultAzureCredential, AccessToken } from \"@azure/identity\";\n\nclass AzurePostgresPool {\n  private pool: Pool | null = null;\n  private credential: DefaultAzureCredential;\n  private tokenExpiry: Date | null = null;\n  private config: Omit&lt;PoolConfig, \"password\"&gt;;\n\n  constructor(config: Omit&lt;PoolConfig, \"password\"&gt;) {\n    this.credential = new DefaultAzureCredential({requiredEnvVars: [\"AZURE_TOKEN_CREDENTIALS\"]});\n    this.config = config;\n  }\n\n  private async getToken(): Promise&lt;string&gt; {\n    const tokenResponse = await this.credential.getToken(\n      \"https://ossrdbms-aad.database.windows.net/.default\"\n    );\n    this.tokenExpiry = new Date(tokenResponse.expiresOnTimestamp);\n    return tokenResponse.token;\n  }\n\n  private isTokenExpired(): boolean {\n    if (!this.tokenExpiry) return true;\n    // Refresh 5 minutes before expiry\n    return new Date() &gt;= new Date(this.tokenExpiry.getTime() - 5 * 60 * 1000);\n  }\n\n  async getPool(): Promise&lt;Pool&gt; {\n    if (this.pool &amp;&amp; !this.isTokenExpired()) {\n      return this.pool;\n    }\n\n    // Close existing pool if token expired\n    if (this.pool) {\n      await this.pool.end();\n    }\n\n    const token = await this.getToken();\n    this.pool = new Pool({\n      ...this.config,\n      password: token\n    });\n\n    return this.pool;\n  }\n\n  async query&lt;T&gt;(text: string, params?: any[]): Promise&lt;QueryResult&lt;T&gt;&gt; {\n    const pool = await this.getPool();\n    return pool.query&lt;T&gt;(text, params);\n  }\n\n  async end(): Promise&lt;void&gt; {\n    if (this.pool) {\n      await this.pool.end();\n      this.pool = null;\n    }\n  }\n}\n\n// Usage\nconst azurePool = new AzurePostgresPool({\n  host: process.env.AZURE_POSTGRESQL_HOST!,\n  database: process.env.AZURE_POSTGRESQL_DATABASE!,\n  user: process.env.AZURE_POSTGRESQL_USER!,\n  port: 5432,\n  ssl: { rejectUnauthorized: true },\n  max: 20\n});\n\nconst result = await azurePool.query(\"SELECT NOW()\");\n</code></pre>\n<h2>Error Handling</h2>\n<pre><code>import { DatabaseError } from \"pg\";\n\ntry {\n  await pool.query(\"INSERT INTO users (email) VALUES ($1)\", [email]);\n} catch (error) {\n  if (error instanceof DatabaseError) {\n    switch (error.code) {\n      case \"23505\":  // unique_violation\n        console.error(\"Duplicate entry:\", error.detail);\n        break;\n      case \"23503\":  // foreign_key_violation\n        console.error(\"Foreign key constraint failed:\", error.detail);\n        break;\n      case \"42P01\":  // undefined_table\n        console.error(\"Table does not exist:\", error.message);\n        break;\n      case \"28P01\":  // invalid_password\n        console.error(\"Authentication failed\");\n        break;\n      case \"57P03\":  // cannot_connect_now (server starting)\n        console.error(\"Server unavailable, retry later\");\n        break;\n      default:\n        console.error(`PostgreSQL error ${error.code}: ${error.message}`);\n    }\n  }\n  throw error;\n}\n</code></pre>\n<h2>Connection String Format</h2>\n<pre><code>// Alternative: Use connection string\nconst pool = new Pool({\n  connectionString: `postgres://${user}:${password}@${host}:${port}/${database}?sslmode=require`\n});\n\n// With SSL required (Azure)\nconst connectionString = \n  `postgres://user:password@server.postgres.database.azure.com:5432/mydb?sslmode=require`;\n</code></pre>\n<h2>Pool Events</h2>\n<pre><code>const pool = new Pool({ /* config */ });\n\npool.on(\"connect\", (client) =&gt; {\n  console.log(\"New client connected to pool\");\n});\n\npool.on(\"acquire\", (client) =&gt; {\n  console.log(\"Client checked out from pool\");\n});\n\npool.on(\"release\", (err, client) =&gt; {\n  console.log(\"Client returned to pool\");\n});\n\npool.on(\"remove\", (client) =&gt; {\n  console.log(\"Client removed from pool\");\n});\n\npool.on(\"error\", (err, client) =&gt; {\n  console.error(\"Unexpected pool error:\", err);\n});\n</code></pre>\n<h2>Azure-Specific Configuration</h2>\n<table>\n<thead>\n<tr>\n<th>Setting</th>\n<th>Value</th>\n<th>Description</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>ssl.rejectUnauthorized</code></td>\n<td><code>true</code></td>\n<td>Always use SSL for Azure</td>\n</tr>\n<tr>\n<td>Default port</td>\n<td><code>5432</code></td>\n<td>Standard PostgreSQL port</td>\n</tr>\n<tr>\n<td>PgBouncer port</td>\n<td><code>6432</code></td>\n<td>Use when PgBouncer enabled</td>\n</tr>\n<tr>\n<td>Token scope</td>\n<td><code>https://ossrdbms-aad.database.windows.net/.default</code></td>\n<td>Entra ID token scope</td>\n</tr>\n<tr>\n<td>Token lifetime</td>\n<td>~1 hour</td>\n<td>Refresh before expiry</td>\n</tr>\n</tbody>\n</table>\n<h2>Pool Sizing Guidelines</h2>\n<table>\n<thead>\n<tr>\n<th>Workload</th>\n<th><code>max</code></th>\n<th><code>idleTimeoutMillis</code></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Light (dev/test)</td>\n<td>5-10</td>\n<td>30000</td>\n</tr>\n<tr>\n<td>Medium (production)</td>\n<td>20-30</td>\n<td>30000</td>\n</tr>\n<tr>\n<td>Heavy (high concurrency)</td>\n<td>50-100</td>\n<td>10000</td>\n</tr>\n</tbody>\n</table>\n<blockquote>\n<p><strong>Note</strong>: Azure PostgreSQL has connection limits based on SKU. Check your tier's max connections.</p>\n</blockquote>\n<h2>Best Practices</h2>\n<ol>\n<li><strong>Always use connection pools</strong> for production applications</li>\n<li><strong>Use parameterized queries</strong> - Never concatenate user input</li>\n<li><strong>Always close connections</strong> - Use <code>try/finally</code> or connection pools</li>\n<li><strong>Enable SSL</strong> - Required for Azure (<code>ssl: { rejectUnauthorized: true }</code>)</li>\n<li><strong>Handle token refresh</strong> - Entra ID tokens expire after ~1 hour</li>\n<li><strong>Set connection timeouts</strong> - Avoid hanging on network issues</li>\n<li><strong>Use transactions</strong> - For multi-statement operations</li>\n<li><strong>Monitor pool metrics</strong> - Track <code>pool.totalCount</code>, <code>pool.idleCount</code>, <code>pool.waitingCount</code></li>\n<li><strong>Graceful shutdown</strong> - Call <code>pool.end()</code> on application termination</li>\n<li><strong>Use TypeScript generics</strong> - Type your query results for safety</li>\n</ol>\n<h2>Key Types</h2>\n<pre><code>import {\n  Client,\n  Pool,\n  PoolClient,\n  PoolConfig,\n  QueryResult,\n  QueryResultRow,\n  DatabaseError,\n  QueryConfig\n} from \"pg\";\n</code></pre>\n<h2>Reference Links</h2>\n<table>\n<thead>\n<tr>\n<th>Resource</th>\n<th>URL</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>node-postgres Docs</td>\n<td><a href=\"https://node-postgres.com\">https://node-postgres.com</a></td>\n</tr>\n<tr>\n<td>npm Package</td>\n<td><a href=\"https://www.npmjs.com/package/pg\">https://www.npmjs.com/package/pg</a></td>\n</tr>\n<tr>\n<td>GitHub Repository</td>\n<td><a href=\"https://github.com/brianc/node-postgres\">https://github.com/brianc/node-postgres</a></td>\n</tr>\n<tr>\n<td>Azure PostgreSQL Docs</td>\n<td><a href=\"https://learn.microsoft.com/azure/postgresql/flexible-server/\">https://learn.microsoft.com/azure/postgresql/flexible-server/</a></td>\n</tr>\n<tr>\n<td>Passwordless Connection</td>\n<td><a href=\"https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-connect-with-managed-identity\">https://learn.microsoft.com/azure/postgresql/flexible-server/how-to-connect-with-managed-identity</a></td>\n</tr>\n</tbody>\n</table>\n","files":[{"path":"SKILL.md","sizeBytes":13376,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-08-12T21:51:41.737259Z","sha256":"3EFF579DF5AF7B3F5C744E0C2407B821306BA4E277D8A76A00FAEF98B96B35F5","sizeBytes":4256},"review":null,"source":{"repositoryUrl":"https://github.com/microsoft/skills","path":".github/plugins/azure-sdk-typescript/skills/azure-postgres-ts","license":"MIT","commit":"23d0dac5f83f268166a17f0bc7dc6c73dc348a33","subtreeSha":"DB8B5CEEA66E4EC14480C4ED144ABB3A818935762BD33B53B479B9DA473D336C","lastSyncedAt":"2026-09-25T06:48:53.330584Z"},"reviewedAt":"2026-08-12T21:57:01.701751Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/microsoft/skills/tree/main/.github/plugins/azure-sdk-typescript/skills/azure-postgres-ts"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install microsoft-skills@llmmart"},{"target":"git","command":"git clone https://github.com/microsoft/skills.git"}]}