tag:github.com,2008:https://github.com/NpgsqlRest/NpgsqlRest/releasesRelease notes from NpgsqlRest2026-03-13T13:25:30Ztag:github.com,2008:Repository/731971286/v3.11.12026-03-13T13:36:17ZNpgsqlRest v3.11.1<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.11.0...3.11.1">Full Changelog</a></p>
<h3>TsClient: <code>proxy</code> Passthrough Endpoint Support</h3>
<p>The TypeScript client generator (<code>NpgsqlRest.TsClient</code>) now recognizes <code>proxy</code> passthrough endpoints and generates functions that return the raw <code>Response</code> object, matching the existing <code>proxy_out</code> behavior. Previously, passthrough proxy endpoints (which typically use <code>returns void</code>) would generate <code>Promise<void></code>, which was incorrect since the actual response comes from the upstream service.</p>
<p>Now, both <code>proxy</code> and <code>proxy_out</code> endpoints generate <code>Promise<Response></code>:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="// Generated for a proxy passthrough endpoint
export async function tsclientTestProxyPassthrough() : Promise<Response> {
const response = await fetch(baseUrl + "/api/tsclient-test/proxy-passthrough", {
method: "GET",
});
return response;
}"><pre><span class="pl-c">// Generated for a proxy passthrough endpoint</span>
<span class="pl-k">export</span> <span class="pl-k">async</span> <span class="pl-k">function</span> <span class="pl-en">tsclientTestProxyPassthrough</span><span class="pl-kos">(</span><span class="pl-kos">)</span> : <span class="pl-smi">Promise</span><span class="pl-c1"><</span><span class="pl-smi">Response</span><span class="pl-c1">></span> <span class="pl-kos">{</span>
<span class="pl-k">const</span> <span class="pl-s1">response</span> <span class="pl-c1">=</span> <span class="pl-k">await</span> <span class="pl-en">fetch</span><span class="pl-kos">(</span><span class="pl-s1">baseUrl</span> <span class="pl-c1">+</span> <span class="pl-s">"/api/tsclient-test/proxy-passthrough"</span><span class="pl-kos">,</span> <span class="pl-kos">{</span>
<span class="pl-c1">method</span>: <span class="pl-s">"GET"</span><span class="pl-kos">,</span>
<span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
<span class="pl-k">return</span> <span class="pl-s1">response</span><span class="pl-kos">;</span>
<span class="pl-kos">}</span></pre></div>
<p>This allows callers to handle the upstream response appropriately (<code>.json()</code>, <code>.blob()</code>, <code>.text()</code>, etc.), just like <code>proxy_out</code> endpoints.</p>
<h3><code>authorize</code> Annotation Now Matches User ID and User Name Claims</h3>
<p>The <code>authorize</code> comment annotation previously only matched against role claims (<code>DefaultRoleClaimType</code>). It now also matches against user ID (<code>DefaultUserIdClaimType</code>) and user name (<code>DefaultNameClaimType</code>) claims, aligning with the behavior that <code>sse_scope authorize</code> already had.</p>
<p>This means you can now restrict endpoint access to specific users, not just roles:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="-- Authorize by role (existing behavior)
comment on function get_reports() is 'authorize admin';
-- Authorize by user name (new)
comment on function get_my_profile() is 'authorize john';
-- Authorize by user ID (new)
comment on function get_account() is 'authorize user123';
-- Mix of roles and user identifiers (new)
comment on function get_data() is 'authorize admin, user123, jane';"><pre><span class="pl-c"><span class="pl-c">--</span> Authorize by role (existing behavior)</span>
<span class="pl-k">comment on function get_reports() is </span><span class="pl-s"><span class="pl-pds">'</span>authorize admin<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">--</span> Authorize by user name (new)</span>
<span class="pl-k">comment on function get_my_profile() is </span><span class="pl-s"><span class="pl-pds">'</span>authorize john<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">--</span> Authorize by user ID (new)</span>
<span class="pl-k">comment on function get_account() is </span><span class="pl-s"><span class="pl-pds">'</span>authorize user123<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">--</span> Mix of roles and user identifiers (new)</span>
<span class="pl-k">comment on function get_data() is </span><span class="pl-s"><span class="pl-pds">'</span>authorize admin, user123, jane<span class="pl-pds">'</span></span>;</pre></div>
<p>The SSE <code>matching</code> scope was also aligned to check all three claim types, making authorization behavior consistent across all features.</p>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.11.02026-03-10T13:54:56ZNpgsqlRest v3.11.0<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.10.0...3.11.0">Full Changelog</a></p>
<h3>New Feature: <code>proxy_out</code> Annotation (Post-Execution Proxy)</h3>
<p>A new proxy mode that reverses the existing proxy flow: <strong>execute the PostgreSQL function first</strong>, then forward the function's result body to an upstream service. The upstream response is returned to the client.</p>
<p>This enables a common pattern where business logic in PostgreSQL prepares a payload, and an external service performs processing that PostgreSQL cannot do — PDF rendering, image processing, ML inference, email sending, etc.</p>
<h4>Syntax</h4>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="@proxy_out [ METHOD ] [ host_url ]"><pre class="notranslate"><code>@proxy_out [ METHOD ] [ host_url ]
</code></pre></div>
<p>Also aliased as <code>forward_proxy</code> (with or without <code>@</code> prefix).</p>
<h4>How It Works</h4>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="Client Request → NpgsqlRest
→ Execute PostgreSQL function
→ Forward function result as request body to upstream service
→ Forward original query string to upstream URL
→ Return upstream response to client"><pre class="notranslate"><code>Client Request → NpgsqlRest
→ Execute PostgreSQL function
→ Forward function result as request body to upstream service
→ Forward original query string to upstream URL
→ Return upstream response to client
</code></pre></div>
<p>Unlike the existing <code>proxy</code> annotation (which forwards the <em>incoming</em> request to upstream), <code>proxy_out</code> forwards the <em>outgoing</em> function result. The original request query string is forwarded to the upstream URL as-is. The client-facing HTTP method and the upstream HTTP method are independent — the client can send a GET while the upstream receives a POST.</p>
<h4>Basic Usage</h4>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create function generate_report(report_id int)
returns json
language plpgsql as $$
begin
return json_build_object(
'title', 'Monthly Report',
'data', (select json_agg(row_to_json(t)) from sales t where month = report_id)
);
end;
$$;
comment on function generate_report(int) is 'HTTP GET
@proxy_out POST https://render-service.internal/render';"><pre><span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">generate_report</span>(report_id <span class="pl-k">int</span>)
returns json
language plpgsql <span class="pl-k">as</span> $$
<span class="pl-k">begin</span>
return json_build_object(
<span class="pl-s"><span class="pl-pds">'</span>title<span class="pl-pds">'</span></span>, <span class="pl-s"><span class="pl-pds">'</span>Monthly Report<span class="pl-pds">'</span></span>,
<span class="pl-s"><span class="pl-pds">'</span>data<span class="pl-pds">'</span></span>, (<span class="pl-k">select</span> json_agg(row_to_json(t)) <span class="pl-k">from</span> sales t <span class="pl-k">where</span> month <span class="pl-k">=</span> report_id)
);
end;
$$;
<span class="pl-k">comment on function generate_report(int) is </span><span class="pl-s"><span class="pl-pds">'</span>HTTP GET</span>
<span class="pl-s">@proxy_out POST https://render-service.internal/render<span class="pl-pds">'</span></span>;</pre></div>
<p>The client calls <code>GET /api/generate-report/?reportId=3</code>. The server:</p>
<ol>
<li>Executes <code>generate_report(3)</code> in PostgreSQL.</li>
<li>Takes the returned JSON and POSTs it to <code>https://render-service.internal/render/api/generate-report/?reportId=3</code> (original query string forwarded).</li>
<li>Returns the upstream response (e.g., a rendered PDF) directly to the client with the upstream's content-type and status code.</li>
</ol>
<h4>Query String Forwarding</h4>
<p>The original client query string is forwarded to the upstream service as-is. This allows the upstream to receive the same parameters that were used to invoke the function:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create function generate_report(p_format text, p_id int)
returns json
language plpgsql as $$
begin
return json_build_object('id', p_id, 'data', 'report');
end;
$$;
comment on function generate_report(text, int) is 'HTTP GET
@proxy_out POST';"><pre><span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">generate_report</span>(p_format <span class="pl-k">text</span>, p_id <span class="pl-k">int</span>)
returns json
language plpgsql <span class="pl-k">as</span> $$
<span class="pl-k">begin</span>
return json_build_object(<span class="pl-s"><span class="pl-pds">'</span>id<span class="pl-pds">'</span></span>, p_id, <span class="pl-s"><span class="pl-pds">'</span>data<span class="pl-pds">'</span></span>, <span class="pl-s"><span class="pl-pds">'</span>report<span class="pl-pds">'</span></span>);
end;
$$;
<span class="pl-k">comment on function generate_report(text, int) is </span><span class="pl-s"><span class="pl-pds">'</span>HTTP GET</span>
<span class="pl-s">@proxy_out POST<span class="pl-pds">'</span></span>;</pre></div>
<p>Calling <code>GET /api/generate-report/?pFormat=pdf&pId=123</code> executes the function, then POSTs the result to the upstream with <code>?pFormat=pdf&pId=123</code> appended to the URL.</p>
<h4>HTTP Method Override</h4>
<p>Specify which HTTP method to use for the upstream request:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function my_func() is 'HTTP GET
@proxy_out PUT';"><pre><span class="pl-k">comment on function my_func() is </span><span class="pl-s"><span class="pl-pds">'</span>HTTP GET</span>
<span class="pl-s">@proxy_out PUT<span class="pl-pds">'</span></span>;</pre></div>
<p>The client sends GET, but the upstream receives PUT with the function's result as the body.</p>
<h4>Custom Host</h4>
<p>Override the default <code>ProxyOptions.Host</code> per-endpoint:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function my_func() is 'HTTP GET
@proxy_out POST https://my-other-service.internal';"><pre><span class="pl-k">comment on function my_func() is </span><span class="pl-s"><span class="pl-pds">'</span>HTTP GET</span>
<span class="pl-s">@proxy_out POST https://my-other-service.internal<span class="pl-pds">'</span></span>;</pre></div>
<h4>Error Handling</h4>
<ul>
<li>If the <strong>function fails</strong> (database error, exception), the error is returned directly to the client — the proxy call is never made.</li>
<li>If the <strong>upstream fails</strong> (5xx, timeout, connection error), the upstream's error status and body are forwarded to the client (502 for connection errors, 504 for timeouts).</li>
</ul>
<h4>Configuration</h4>
<p>Uses the same <code>ProxyOptions</code> configuration as the existing <code>proxy</code> annotation. <code>ProxyOptions.Enabled</code> must be <code>true</code>:</p>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="{
"NpgsqlRest": {
"ProxyOptions": {
"Enabled": true,
"Host": "https://api.example.com",
"DefaultTimeout": "30 seconds"
}
}
}"><pre>{
<span class="pl-ent">"NpgsqlRest"</span>: {
<span class="pl-ent">"ProxyOptions"</span>: {
<span class="pl-ent">"Enabled"</span>: <span class="pl-c1">true</span>,
<span class="pl-ent">"Host"</span>: <span class="pl-s"><span class="pl-pds">"</span>https://api.example.com<span class="pl-pds">"</span></span>,
<span class="pl-ent">"DefaultTimeout"</span>: <span class="pl-s"><span class="pl-pds">"</span>30 seconds<span class="pl-pds">"</span></span>
}
}
}</pre></div>
<h4>Performance</h4>
<ul>
<li><strong>Zero overhead for non-proxy_out endpoints.</strong> The implementation adds only branch-not-taken boolean/null checks on the normal execution path (~4 nanoseconds).</li>
<li><strong>Efficient byte forwarding.</strong> Function output is captured as raw bytes and forwarded directly via <code>ByteArrayContent</code> — no intermediate string allocation or double UTF-8 encoding.</li>
</ul>
<h3>TsClient: <code>proxy_out</code> Endpoint Support</h3>
<p>The TypeScript client generator (<code>NpgsqlRest.TsClient</code>) now recognizes <code>proxy_out</code> endpoints and generates functions that return the raw <code>Response</code> object instead of a typed return value. Since the actual response comes from the upstream proxy service (not from the PostgreSQL function's return type), the generated function returns <code>Promise<Response></code>, allowing the caller to handle the response appropriately (<code>.json()</code>, <code>.blob()</code>, <code>.text()</code>, etc.):</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="// Generated for a proxy_out endpoint
export async function generateReport() : Promise<Response> {
const response = await fetch(baseUrl + "/api/generate-report", {
method: "GET",
});
return response;
}"><pre><span class="pl-c">// Generated for a proxy_out endpoint</span>
<span class="pl-k">export</span> <span class="pl-k">async</span> <span class="pl-k">function</span> <span class="pl-en">generateReport</span><span class="pl-kos">(</span><span class="pl-kos">)</span> : <span class="pl-smi">Promise</span><span class="pl-c1"><</span><span class="pl-smi">Response</span><span class="pl-c1">></span> <span class="pl-kos">{</span>
<span class="pl-k">const</span> <span class="pl-s1">response</span> <span class="pl-c1">=</span> <span class="pl-k">await</span> <span class="pl-en">fetch</span><span class="pl-kos">(</span><span class="pl-s1">baseUrl</span> <span class="pl-c1">+</span> <span class="pl-s">"/api/generate-report"</span><span class="pl-kos">,</span> <span class="pl-kos">{</span>
<span class="pl-c1">method</span>: <span class="pl-s">"GET"</span><span class="pl-kos">,</span>
<span class="pl-kos">}</span><span class="pl-kos">)</span><span class="pl-kos">;</span>
<span class="pl-k">return</span> <span class="pl-s1">response</span><span class="pl-kos">;</span>
<span class="pl-kos">}</span></pre></div>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.10.02026-02-25T14:05:55ZNpgsqlRest v3.10.0<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.9.0...3.10.0">Full Changelog</a></p>
<h3>New Feature: Resolved Parameter Expressions</h3>
<p>When using <a href="https://vb-consulting.github.io/npgsqlrest/http-client-types/" rel="nofollow">HTTP Client Types</a>, sensitive values like API tokens or secrets are often needed in outgoing HTTP requests (e.g., in an <code>Authorization</code> header). Previously, these values had to be supplied as regular HTTP parameters — exposing them to the client and requiring an insecure round-trip: database → client → server → external API.</p>
<p><strong>Resolved parameter expressions</strong> solve this by allowing function parameters to be resolved server-side via SQL expressions defined in comment annotations. The resolved values are used in HTTP Client Type placeholder substitution (headers, URL, body) and are also passed to the PostgreSQL function — but they never appear in or originate from the client HTTP request.</p>
<h4>How It Works</h4>
<p>If a comment annotation uses the existing <code>key = value</code> syntax and the key matches an actual function parameter name, the value is treated as a SQL expression to execute at runtime:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create type my_api_response as (body json, status_code int);
comment on type my_api_response is 'GET https://api.example.com/data
Authorization: Bearer {_token}';
create function get_secure_data(
_user_id int,
_req my_api_response,
_token text default null
)
returns table (body json, status_code int)
language plpgsql as $$
begin
return query select (_req).body, (_req).status_code;
end;
$$;
comment on function get_secure_data(int, my_api_response, text) is '
_token = select api_token from user_tokens where user_id = {_user_id}
';"><pre><span class="pl-k">create</span> <span class="pl-k">type</span> <span class="pl-en">my_api_response</span> <span class="pl-k">as</span> (body json, status_code <span class="pl-k">int</span>);
<span class="pl-k">comment on type my_api_response is </span><span class="pl-s"><span class="pl-pds">'</span>GET https://api.example.com/data</span>
<span class="pl-s">Authorization: Bearer {_token}<span class="pl-pds">'</span></span>;
<span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">get_secure_data</span>(
_user_id <span class="pl-k">int</span>,
_req my_api_response,
_token <span class="pl-k">text</span> default <span class="pl-k">null</span>
)
returns table (body json, status_code <span class="pl-k">int</span>)
language plpgsql <span class="pl-k">as</span> $$
<span class="pl-k">begin</span>
return query <span class="pl-k">select</span> (_req).body, (_req).status_code;
end;
$$;
<span class="pl-k">comment on function get_secure_data(int, my_api_response, text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">_token = select api_token from user_tokens where user_id = {_user_id}</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>The client calls <code>GET /api/get-secure-data/?user_id=42</code>. The server:</p>
<ol>
<li>Fills <code>_user_id</code> from the query string (value <code>42</code>).</li>
<li>Executes the resolved expression: <code>select api_token from user_tokens where user_id = $1</code> (parameterized, with <code>$1 = 42</code>).</li>
<li>Sets <code>_token</code> to the result (e.g., <code>"secret-abc"</code>).</li>
<li>Substitutes <code>{_token}</code> in the outgoing HTTP request header: <code>Authorization: Bearer secret-abc</code>.</li>
<li>Makes the HTTP call and returns the response.</li>
</ol>
<p>The token never leaves the server. The client never sees it.</p>
<h4>Behavior</h4>
<ul>
<li><strong>Server-side only</strong>: Resolved parameters cannot be overridden by client input. Even if the client sends <code>&token=hacked</code>, the DB-resolved value is used.</li>
<li><strong>NULL handling</strong>: If the SQL expression returns no rows or NULL, the parameter is set to <code>DBNull.Value</code> (empty string in placeholder substitution).</li>
<li><strong>Name-based placeholders, parameterized execution</strong>: Placeholders like <code>{_user_id}</code> reference other function parameters by name — the value is always looked up by name, regardless of position. Internally, placeholders are converted to positional <code>$N</code> parameters for safe execution (preventing SQL injection).</li>
<li><strong>Sequential execution</strong>: When multiple parameters are resolved, expressions execute one-by-one on the same connection, in annotation order.</li>
<li><strong>Works with user_params</strong>: Resolved expressions can reference parameters auto-filled from JWT claims via <code>user_params</code>, enabling fully zero-parameter authenticated calls.</li>
</ul>
<h4>Multiple Resolved Parameters</h4>
<p>Multiple parameters can each have their own resolved expression:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function my_func(text, my_type, text, text) is '
_token = select api_token from tokens where user_name = {_name}
_api_key = select ''static-key-'' || api_token from tokens where user_name = {_name}
';"><pre><span class="pl-k">comment on function my_func(text, my_type, text, text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">_token = select api_token from tokens where user_name = {_name}</span>
<span class="pl-s">_api_key = select <span class="pl-pds">'</span><span class="pl-pds">'</span>static-key-<span class="pl-pds">'</span><span class="pl-pds">'</span> || api_token from tokens where user_name = {_name}</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<h4>Resolved Parameters in URL, Headers, and Body</h4>
<p>Resolved values participate in all HTTP Client Type placeholder locations — URL path segments, headers, and request body templates:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="-- URL: GET https://api.example.com/resource/{_secret_path}
-- Header: Authorization: Bearer {_token}
-- Body: {"token": "{_token}", "data": "{_payload}"}"><pre><span class="pl-c"><span class="pl-c">--</span> URL: GET https://api.example.com/resource/{_secret_path}</span>
<span class="pl-c"><span class="pl-c">--</span> Header: Authorization: Bearer {_token}</span>
<span class="pl-c"><span class="pl-c">--</span> Body: {"token": "{_token}", "data": "{_payload}"}</span></pre></div>
<h3>New Feature: HTTP Client Type Retry Logic</h3>
<p>When using <a href="https://vb-consulting.github.io/npgsqlrest/http-client-types/" rel="nofollow">HTTP Client Types</a>, outgoing HTTP requests to external APIs can fail transiently — rate limiting (429), temporary server errors (503), network timeouts. Previously, a single failure was passed directly to the PostgreSQL function with no opportunity to retry.</p>
<p>The new <code>@retry_delay</code> directive adds configurable automatic retries with delays, defined in the HTTP type comment alongside existing directives like <code>timeout</code>.</p>
<h4>Syntax</h4>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="-- Retry on any failure (non-2xx status, timeout, or network error):
comment on type my_api_type is '@retry_delay 1s, 2s, 5s
GET https://api.example.com/data';
-- Retry only on specific HTTP status codes:
comment on type my_api_type is '@retry_delay 1s, 2s, 5s on 429, 503
GET https://api.example.com/data';
-- Combined with timeout:
comment on type my_api_type is 'timeout 10s
@retry_delay 1s, 2s, 5s on 429, 503
GET https://api.example.com/data';"><pre><span class="pl-c"><span class="pl-c">--</span> Retry on any failure (non-2xx status, timeout, or network error):</span>
<span class="pl-k">comment on type my_api_type is </span><span class="pl-s"><span class="pl-pds">'</span>@retry_delay 1s, 2s, 5s</span>
<span class="pl-s">GET https://api.example.com/data<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">--</span> Retry only on specific HTTP status codes:</span>
<span class="pl-k">comment on type my_api_type is </span><span class="pl-s"><span class="pl-pds">'</span>@retry_delay 1s, 2s, 5s on 429, 503</span>
<span class="pl-s">GET https://api.example.com/data<span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">--</span> Combined with timeout:</span>
<span class="pl-k">comment on type my_api_type is </span><span class="pl-s"><span class="pl-pds">'</span>timeout 10s</span>
<span class="pl-s">@retry_delay 1s, 2s, 5s on 429, 503</span>
<span class="pl-s">GET https://api.example.com/data<span class="pl-pds">'</span></span>;</pre></div>
<p>The delay list defines both the <strong>number of retries</strong> and the <strong>delay before each retry</strong>. <code>1s, 2s, 5s</code> means 3 retries with 1-second, 2-second, and 5-second delays respectively. Delay values use the same format as <code>timeout</code> — <code>100ms</code>, <code>1s</code>, <code>5m</code>, <code>30</code>, <code>00:00:01</code>, etc.</p>
<h4>Behavior</h4>
<ul>
<li><strong>Without <code>on</code> filter</strong>: Retries on any non-success HTTP response, timeout, or network error.</li>
<li><strong>With <code>on</code> filter</strong>: Retries only when the HTTP response status code matches one of the listed codes (e.g., <code>429, 503</code>). Timeouts and network errors always trigger retry regardless of the filter, since they have no status code.</li>
<li><strong>Retry exhaustion</strong>: If all retries fail, the last error (status code, error message) is passed to the PostgreSQL function — the same as if retries were not configured.</li>
<li><strong>Unexpected exceptions</strong>: Non-HTTP errors (e.g., invalid URL) are never retried.</li>
<li><strong>Parallel execution</strong>: Each HTTP type in a function retries independently within its own parallel task. No changes to the parallel execution model.</li>
<li><strong>No external dependencies</strong>: Built-in retry loop, no Polly or other libraries required. Matches the existing PostgreSQL command retry pattern.</li>
</ul>
<h4>Example</h4>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create type rate_limited_api as (body json, status_code int, error_message text);
comment on type rate_limited_api is '@retry_delay 1s, 2s, 5s on 429, 503
GET https://api.example.com/data
Authorization: Bearer {_token}';
create function get_rate_limited_data(
_token text,
_req rate_limited_api
)
returns table (body json, status_code int, error_message text)
language plpgsql as $$
begin
return query select (_req).body, (_req).status_code, (_req).error_message;
end;
$$;"><pre><span class="pl-k">create</span> <span class="pl-k">type</span> <span class="pl-en">rate_limited_api</span> <span class="pl-k">as</span> (body json, status_code <span class="pl-k">int</span>, error_message <span class="pl-k">text</span>);
<span class="pl-k">comment on type rate_limited_api is </span><span class="pl-s"><span class="pl-pds">'</span>@retry_delay 1s, 2s, 5s on 429, 503</span>
<span class="pl-s">GET https://api.example.com/data</span>
<span class="pl-s">Authorization: Bearer {_token}<span class="pl-pds">'</span></span>;
<span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">get_rate_limited_data</span>(
_token <span class="pl-k">text</span>,
_req rate_limited_api
)
returns table (body json, status_code <span class="pl-k">int</span>, error_message <span class="pl-k">text</span>)
language plpgsql <span class="pl-k">as</span> $$
<span class="pl-k">begin</span>
return query <span class="pl-k">select</span> (_req).body, (_req).status_code, (_req).error_message;
end;
$$;</pre></div>
<p>If the external API returns 429 (rate limited), the request is automatically retried after 1s, then 2s, then 5s. If it returns 400 (bad request), no retry occurs and the error is returned immediately.</p>
<h3>New Feature: Data Protection Encrypt/Decrypt Annotations</h3>
<p>Two new comment annotations — <code>encrypt</code> and <code>decrypt</code> — enable transparent application-level column encryption using ASP.NET <a href="https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction" rel="nofollow">Data Protection</a>. Parameter values are encrypted before being sent to PostgreSQL, and result column values are decrypted before being returned to the API client. The database stores ciphertext; the API consumer sees plaintext. No <code>pgcrypto</code> or client-side encryption required.</p>
<p>This is useful for storing PII (SSN, medical records, credit card numbers) or other sensitive data that must be encrypted at rest but is only ever looked up by an unencrypted key (e.g., <code>user_id</code>, <code>patient_id</code>).</p>
<blockquote>
<p><strong>Prerequisite</strong>: The <code>DataProtection</code> section must be enabled in <code>appsettings.json</code> (it is by default). The <code>DefaultDataProtector</code> is automatically created from Data Protection configuration and passed to the NpgsqlRest authentication options.</p>
</blockquote>
<h4>Encrypt Parameters</h4>
<p>Mark specific parameters to encrypt before they are sent to PostgreSQL:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create function store_patient_ssn(_patient_id int, _ssn text)
returns void
language plpgsql as $$
begin
insert into patients (id, ssn) values (_patient_id, _ssn)
on conflict (id) do update set ssn = excluded.ssn;
end;
$$;
comment on function store_patient_ssn(int, text) is '
HTTP POST
encrypt _ssn
';"><pre><span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">store_patient_ssn</span>(_patient_id <span class="pl-k">int</span>, _ssn <span class="pl-k">text</span>)
returns void
language plpgsql <span class="pl-k">as</span> $$
<span class="pl-k">begin</span>
<span class="pl-k">insert into</span> patients (id, ssn) <span class="pl-k">values</span> (_patient_id, _ssn)
<span class="pl-k">on</span> conflict (id) do <span class="pl-k">update</span> <span class="pl-k">set</span> ssn <span class="pl-k">=</span> <span class="pl-c1">excluded</span>.<span class="pl-c1">ssn</span>;
end;
$$;
<span class="pl-k">comment on function store_patient_ssn(int, text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP POST</span>
<span class="pl-s">encrypt _ssn</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>The client calls <code>POST /api/store-patient-ssn/</code> with <code>{"patientId": 1, "ssn": "123-45-6789"}</code>. The server encrypts <code>_ssn</code> using Data Protection before executing the SQL — the database stores ciphertext like <code>CfDJ8N...</code>, never the plaintext SSN.</p>
<p>Use <code>encrypt</code> without arguments to encrypt <strong>all</strong> text parameters:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function store_all_secrets(text, text) is '
HTTP POST
encrypt
';"><pre><span class="pl-k">comment on function store_all_secrets(text, text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP POST</span>
<span class="pl-s">encrypt</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<h4>Decrypt Result Columns</h4>
<p>Mark specific result columns to decrypt before returning to the client:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create function get_patient(_patient_id int)
returns table(id int, ssn text, name text)
language plpgsql as $$
begin
return query select p.id, p.ssn, p.name from patients p where p.id = _patient_id;
end;
$$;
comment on function get_patient(int) is '
decrypt ssn
';"><pre><span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">get_patient</span>(_patient_id <span class="pl-k">int</span>)
returns table(id <span class="pl-k">int</span>, ssn <span class="pl-k">text</span>, name <span class="pl-k">text</span>)
language plpgsql <span class="pl-k">as</span> $$
<span class="pl-k">begin</span>
return query <span class="pl-k">select</span> <span class="pl-c1">p</span>.<span class="pl-c1">id</span>, <span class="pl-c1">p</span>.<span class="pl-c1">ssn</span>, <span class="pl-c1">p</span>.<span class="pl-c1">name</span> <span class="pl-k">from</span> patients p <span class="pl-k">where</span> <span class="pl-c1">p</span>.<span class="pl-c1">id</span> <span class="pl-k">=</span> _patient_id;
end;
$$;
<span class="pl-k">comment on function get_patient(int) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">decrypt ssn</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>The client calls <code>GET /api/get-patient/?patientId=1</code>. The <code>ssn</code> column is decrypted from ciphertext back to <code>"123-45-6789"</code> before being included in the JSON response. The <code>id</code> and <code>name</code> columns are returned as-is.</p>
<p>Use <code>decrypt</code> without arguments to decrypt <strong>all</strong> result columns:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function get_all_secrets(text) is '
decrypt
';"><pre><span class="pl-k">comment on function get_all_secrets(text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">decrypt</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>Decrypt also works on scalar (single-value) return types:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create function get_secret(_id int) returns text ...
comment on function get_secret(int) is 'decrypt';"><pre><span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">get_secret</span>(_id <span class="pl-k">int</span>) returns <span class="pl-k">text</span> ...
<span class="pl-k">comment on function get_secret(int) is </span><span class="pl-s"><span class="pl-pds">'</span>decrypt<span class="pl-pds">'</span></span>;</pre></div>
<h4>Full Roundtrip Example</h4>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="-- Store with encryption
create function store_secret(_key text, _value text) returns void ...
comment on function store_secret(text, text) is '
HTTP POST
encrypt _value
';
-- Retrieve with decryption
create function get_secret(_key text) returns table(key text, value text) ...
comment on function get_secret(text) is '
decrypt value
';"><pre><span class="pl-c"><span class="pl-c">--</span> Store with encryption</span>
<span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">store_secret</span>(_key <span class="pl-k">text</span>, _value <span class="pl-k">text</span>) returns void ...
<span class="pl-k">comment on function store_secret(text, text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP POST</span>
<span class="pl-s">encrypt _value</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;
<span class="pl-c"><span class="pl-c">--</span> Retrieve with decryption</span>
<span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">get_secret</span>(_key <span class="pl-k">text</span>) returns table(key <span class="pl-k">text</span>, value <span class="pl-k">text</span>) ...
<span class="pl-k">comment on function get_secret(text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">decrypt value</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="POST /api/store-secret/ {"key": "api-key", "value": "sk-abc123"}
GET /api/get-secret/?key=api-key → {"key": "api-key", "value": "sk-abc123"}"><pre class="notranslate"><code>POST /api/store-secret/ {"key": "api-key", "value": "sk-abc123"}
GET /api/get-secret/?key=api-key → {"key": "api-key", "value": "sk-abc123"}
</code></pre></div>
<p>The value is stored encrypted in PostgreSQL and decrypted transparently on read.</p>
<h4>Annotation Aliases</h4>
<table>
<thead>
<tr>
<th>Annotation</th>
<th>Aliases</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>encrypt</code></td>
<td><code>encrypted</code>, <code>protect</code>, <code>protected</code></td>
</tr>
<tr>
<td><code>decrypt</code></td>
<td><code>decrypted</code>, <code>unprotect</code>, <code>unprotected</code></td>
</tr>
</tbody>
</table>
<h4>Behavior Notes</h4>
<ul>
<li><strong>NULL values</strong>: NULL parameters are not encrypted (passed as <code>DBNull</code>). NULL columns are not decrypted (returned as JSON <code>null</code>).</li>
<li><strong>Non-text types</strong>: Only <code>string</code> parameter values are encrypted. Integer, boolean, and other types are unaffected even when <code>encrypt</code> is used without arguments.</li>
<li><strong>Decryption failures</strong>: If a column value cannot be decrypted (e.g., it was not encrypted, or keys were rotated/lost), the raw value is returned as-is — no error is thrown.</li>
<li><strong>Key rotation</strong>: ASP.NET Data Protection maintains a key ring. Old keys still decrypt old ciphertext. Keys rotate based on <code>DefaultKeyLifetimeDays</code> (default: 90 days). Persistent key storage (<code>FileSystem</code> or <code>Database</code>) is strongly recommended.</li>
<li><strong>Encrypted columns are opaque to PostgreSQL</strong>: The database cannot filter, join, sort, or index on encrypted values. Use encryption only for columns that are written and read back, never queried by content.</li>
</ul>
<h4>Key Management</h4>
<p>Encrypt/decrypt relies on the existing <code>DataProtection</code> configuration in <code>appsettings.json</code>. The encryption keys must be persisted — if keys are lost, encrypted data is <strong>permanently unrecoverable</strong>.</p>
<p><strong>Key storage options</strong> (<code>DataProtection:Storage</code>):</p>
<table>
<thead>
<tr>
<th>Storage</th>
<th>Description</th>
<th>Recommendation</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>"Default"</code></td>
<td>OS default location. On Linux, keys are in-memory only and lost on restart.</td>
<td>Windows only</td>
</tr>
<tr>
<td><code>"FileSystem"</code></td>
<td>Keys persisted to a directory (<code>FileSystemPath</code>). In Docker, use a volume mount.</td>
<td>Good for single-instance</td>
</tr>
<tr>
<td><code>"Database"</code></td>
<td>Keys stored in PostgreSQL via <code>GetAllElementsCommand</code> / <code>StoreElementCommand</code>.</td>
<td>Best for multi-instance</td>
</tr>
</tbody>
</table>
<p><strong>Key rotation</strong> (<code>DataProtection:DefaultKeyLifetimeDays</code>, default: <code>90</code>):</p>
<p>Data Protection automatically rotates keys. New <code>Protect()</code> calls use the newest key. Old keys remain in the key ring and can still <code>Unprotect()</code> previously encrypted data. This means values encrypted months ago continue to decrypt correctly — the key ring grows over time, it doesn't replace old keys.</p>
<p><strong>Key encryption at rest</strong> (<code>DataProtection:KeyEncryption</code>):</p>
<p>The keys themselves can be encrypted at rest using <code>"Certificate"</code> (X.509 <code>.pfx</code> file) or <code>"Dpapi"</code> (Windows only). Default is <code>"None"</code>.</p>
<p><strong>Application name isolation</strong> (<code>DataProtection:CustomApplicationName</code>):</p>
<p>The application name acts as an encryption isolation boundary. Different application names produce incompatible ciphertext — they cannot decrypt each other's data. When set to <code>null</code> (default), the current <code>ApplicationName</code> is used.</p>
<p>Example minimal configuration for production use:</p>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="{
"DataProtection": {
"Enabled": true,
"Storage": "FileSystem",
"FileSystemPath": "/var/lib/npgsqlrest/data-protection-keys",
"DefaultKeyLifetimeDays": 90
}
}"><pre>{
<span class="pl-ent">"DataProtection"</span>: {
<span class="pl-ent">"Enabled"</span>: <span class="pl-c1">true</span>,
<span class="pl-ent">"Storage"</span>: <span class="pl-s"><span class="pl-pds">"</span>FileSystem<span class="pl-pds">"</span></span>,
<span class="pl-ent">"FileSystemPath"</span>: <span class="pl-s"><span class="pl-pds">"</span>/var/lib/npgsqlrest/data-protection-keys<span class="pl-pds">"</span></span>,
<span class="pl-ent">"DefaultKeyLifetimeDays"</span>: <span class="pl-c1">90</span>
}
}</pre></div>
<p>Or using database storage:</p>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="{
"DataProtection": {
"Enabled": true,
"Storage": "Database",
"GetAllElementsCommand": "select get_data_protection_keys()",
"StoreElementCommand": "call store_data_protection_keys($1,$2)"
}
}"><pre>{
<span class="pl-ent">"DataProtection"</span>: {
<span class="pl-ent">"Enabled"</span>: <span class="pl-c1">true</span>,
<span class="pl-ent">"Storage"</span>: <span class="pl-s"><span class="pl-pds">"</span>Database<span class="pl-pds">"</span></span>,
<span class="pl-ent">"GetAllElementsCommand"</span>: <span class="pl-s"><span class="pl-pds">"</span>select get_data_protection_keys()<span class="pl-pds">"</span></span>,
<span class="pl-ent">"StoreElementCommand"</span>: <span class="pl-s"><span class="pl-pds">"</span>call store_data_protection_keys($1,$2)<span class="pl-pds">"</span></span>
}
}</pre></div>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.9.02026-02-23T11:32:45ZNpgsqlRest v3.9.0<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.8.0...3.9.0">Full Changelog</a></p>
<h3>Commented Configuration Output (<code>--config</code>)</h3>
<p>The <code>--config</code> output now includes inline JSONC comments with descriptions for every setting, matching the <code>appsettings.json</code> file exactly. This makes it easy to understand what each setting does without consulting the documentation. The default configuration file can be constructed with:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="npgsqlrest --config > appsettings.json"><pre class="notranslate"><code>npgsqlrest --config > appsettings.json
</code></pre></div>
<h3>Configuration Search and Filter (<code>--config [filter]</code>)</h3>
<p>Added an optional filter argument to <code>--config</code> that searches keys, comments, and values (case-insensitive) and returns only matching settings as valid JSONC:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="npgsqlrest --config cors
npgsqlrest --config=timeout
npgsqlrest --config minworker"><pre class="notranslate"><code>npgsqlrest --config cors
npgsqlrest --config=timeout
npgsqlrest --config minworker
</code></pre></div>
<p>Output preserves the full section hierarchy so it can be copy-pasted directly into <code>appsettings.json</code>. When a key inside a section matches, the parent section wrapper is included. When a section name or its comment matches, the entire section is shown. Matched terms are highlighted with inverted colors in the terminal; piped output is plain text.</p>
<h3>CLI Improvements</h3>
<ul>
<li><strong>Case-insensitive config overrides</strong>: Command-line config overrides like <code>--Applicationname=test</code> now correctly update the existing <code>ApplicationName</code> key instead of creating a duplicate entry with different casing.</li>
<li><strong>Config validation on <code>--config</code></strong>: The <code>--config</code> command now validates configuration keys before dumping. Unknown keys (e.g., <code>--xxx=test</code>) produce an error on stderr and exit with code 1.</li>
<li><strong>Redirected output fix</strong>: Formatted CLI output (<code>--help</code>, <code>--version</code>) no longer crashes when stdout is redirected (e.g., piped or captured by a parent process).</li>
<li><strong>CLI test suite</strong>: Added process-based tests for all CLI commands (<code>--help</code>, <code>--version</code>, <code>--hash</code>, <code>--basic_auth</code>, <code>--config-schema</code>, <code>--annotations</code>, <code>--config</code>, <code>--config [filter]</code>, invalid args).</li>
</ul>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.8.02026-02-11T11:58:54ZNpgsqlRest v3.8.0<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.7.0...3.8.0">Full Changelog</a></p>
<h3>New Feature: Configuration Key Validation</h3>
<p>Added startup validation that checks all configuration keys in <code>appsettings.json</code> against the known defaults schema. This catches typos and unknown keys that would otherwise be silently ignored (e.g., <code>LogCommand</code> instead of <code>LogCommands</code>).</p>
<p>Controlled by the new <code>Config:ValidateConfigKeys</code> setting with three modes:</p>
<ul>
<li><code>"Warning"</code> (default) — logs warnings for unknown keys, startup continues.</li>
<li><code>"Error"</code> — logs errors for unknown keys and exits the application.</li>
<li><code>"Ignore"</code> — no validation.</li>
</ul>
<div class="highlight highlight-source-json notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content=""Config": {
"ValidateConfigKeys": "Warning"
}"><pre><span class="pl-ent">"Config"</span>: {
<span class="pl-ent">"ValidateConfigKeys"</span>: <span class="pl-s"><span class="pl-pds">"</span>Warning<span class="pl-pds">"</span></span>
}</pre></div>
<p>Example output:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="[12:34:56 WRN] Unknown configuration key: NpgsqlRest:KebabCaselUrls"><pre class="notranslate"><code>[12:34:56 WRN] Unknown configuration key: NpgsqlRest:KebabCaselUrls
</code></pre></div>
<h3>Removed</h3>
<ul>
<li>Removed the <code>Config:ExposeAsEndpoint</code> option. Use the <code>--config</code> CLI switch to inspect configuration instead.</li>
</ul>
<h3>Kestrel Configuration Validation</h3>
<p>Configuration key validation also covers the <code>Kestrel</code> section, checking against the known Kestrel schema including <code>Limits</code>, <code>Http2</code>, <code>Http3</code>, and top-level flags like <code>DisableStringReuse</code> and <code>AllowSynchronousIO</code>. User-defined endpoint and certificate names under <code>Endpoints</code> and <code>Certificates</code> remain open-ended and won't trigger warnings.</p>
<h3>Syntax Highlighted <code>--config</code> Output</h3>
<p>The <code>--config</code> CLI switch now outputs JSON with syntax highlighting (keys, strings, numbers/booleans, and structural characters in distinct colors). When output is redirected to a file, plain JSON is emitted without color codes. The <code>--config</code> switch can now appear anywhere in the argument list and be combined with config files and <code>--key=value</code> overrides.</p>
<h3>Improved CLI Error Handling</h3>
<p>Unknown command-line parameters now display a clear error message with a <code>--help</code> hint instead of an unhandled exception stack trace.</p>
<h3>Universal <code>fallback_handler</code> for All Upload Handlers</h3>
<p>The <code>fallback_handler</code> parameter, previously Excel-only, is now available on all upload handlers via <code>BaseUploadHandler</code>. When a handler's format validation fails and a <code>fallback_handler</code> is configured, processing is automatically delegated to the named fallback handler.</p>
<p>This enables scenarios like: CSV format check fails on a binary file → fall back to <code>large_object</code> or <code>file_system</code> to save the raw file for analysis.</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function my_csv_upload(json) is '
@upload for csv
@check_format = true
@fallback_handler = large_object
@row_command = select process_row($1,$2)
';"><pre><span class="pl-k">comment on function my_csv_upload(json) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">@upload for csv</span>
<span class="pl-s">@check_format = true</span>
<span class="pl-s">@fallback_handler = large_object</span>
<span class="pl-s">@row_command = select process_row($1,$2)</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<h3>Optional Path Parameters</h3>
<p>Path parameters now support the ASP.NET Core optional parameter syntax <code>{param?}</code>. When a path parameter is marked as optional and the corresponding PostgreSQL function parameter has a default value, omitting the URL segment will use the PostgreSQL default:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create function get_item(p_id int default 42) returns text ...
comment on function get_item(int) is '
HTTP GET /items/{p_id?}
';"><pre><span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">get_item</span>(p_id <span class="pl-k">int</span> default <span class="pl-c1">42</span>) returns <span class="pl-k">text</span> ...
<span class="pl-k">comment on function get_item(int) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP GET /items/{p_id?}</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<ul>
<li><code>GET /items/5</code> → uses the provided value <code>5</code></li>
<li><code>GET /items/</code> → uses the PostgreSQL default <code>42</code></li>
</ul>
<p>This also works with <code>query_string_null_handling null_literal</code> to pass NULL via the literal string <code>"null"</code> in the path for any parameter type:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="create function get_item(p_id int default null) returns text ...
comment on function get_item(int) is '
HTTP GET /items/{p_id}
query_string_null_handling null_literal
';"><pre><span class="pl-k">create</span> <span class="pl-k">function</span> <span class="pl-en">get_item</span>(p_id <span class="pl-k">int</span> default <span class="pl-k">null</span>) returns <span class="pl-k">text</span> ...
<span class="pl-k">comment on function get_item(int) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP GET /items/{p_id}</span>
<span class="pl-s">query_string_null_handling null_literal</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<ul>
<li><code>GET /items/null</code> → passes SQL NULL to the function</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Fixed query string overload resolution not accounting for path parameters. GET endpoints with path parameters and overloaded functions (same name, different signatures) would resolve to the wrong function. The body JSON overload resolution already handled this correctly.</li>
<li>Added missing <code>QueryStringNullHandling</code> and <code>TextResponseNullHandling</code> entries to <code>ConfigDefaults</code>, which caused them to be absent from <code>--config</code> output.</li>
<li>Added missing <code>Pattern</code>, <code>MinLength</code>, and <code>MaxLength</code> properties to default validation rule schemas in <code>ConfigDefaults</code>.</li>
</ul>
<h3>Machine-Readable CLI Commands for Tool Integration</h3>
<p>Added new CLI commands designed for programmatic consumption by tools like pgdev. All JSON-outputting commands use syntax highlighting when run in a terminal and emit plain JSON when piped or redirected.</p>
<h4><code>--version --json</code></h4>
<p>Outputs version information as structured JSON including all assembly versions, runtime, platform RID, and directories:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="npgsqlrest --version --json"><pre class="notranslate"><code>npgsqlrest --version --json
</code></pre></div>
<h4><code>--validate [--json]</code></h4>
<p>Pre-flight check that validates configuration keys against known defaults and tests the database connection, then exits with code 0 (success) or 1 (failure):</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="npgsqlrest --validate
npgsqlrest --validate --json"><pre class="notranslate"><code>npgsqlrest --validate
npgsqlrest --validate --json
</code></pre></div>
<h4><code>--config-schema</code></h4>
<p>Outputs a <a href="https://json-schema.org" rel="nofollow">JSON Schema (draft-07)</a> describing the full <code>appsettings.json</code> configuration structure — types, defaults, and enum constraints. Can be used for IDE autocomplete via the <code>$schema</code> property or as the foundation for config editing UIs:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="npgsqlrest --config-schema"><pre class="notranslate"><code>npgsqlrest --config-schema
</code></pre></div>
<h4><code>--annotations</code></h4>
<p>Outputs all 44 supported SQL comment annotations as a JSON array with name, aliases, syntax, and description for each:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="npgsqlrest --annotations"><pre class="notranslate"><code>npgsqlrest --annotations
</code></pre></div>
<h4><code>--endpoints</code></h4>
<p>Connects to the database, discovers all generated REST endpoints, outputs full metadata (method, path, routine info, parameters, return columns, authorization, custom parameters), then exits. Logging is suppressed to keep output clean:</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="npgsqlrest --endpoints"><pre class="notranslate"><code>npgsqlrest --endpoints
</code></pre></div>
<h4><code>--config</code> (updated)</h4>
<p>The <code>--config --json</code> flag has been removed. The <code>--config</code> command now always uses automatic detection: syntax highlighted in terminal, plain JSON when output is piped or redirected.</p>
<h3>Stats Endpoints: <code>format</code> Query String Override</h3>
<p>Stats endpoints now accept an optional <code>format</code> query string parameter that overrides the configured <code>Stats:OutputFormat</code> setting per-request. Valid values are <code>html</code> and <code>json</code>.</p>
<div class="snippet-clipboard-content notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="GET /api/stats/routines?format=json
GET /api/stats/tables?format=html"><pre class="notranslate"><code>GET /api/stats/routines?format=json
GET /api/stats/tables?format=html
</code></pre></div>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.7.02026-02-08T07:10:15ZNpgsqlRest v3.7.0<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.6.3...3.7.0">Full Changelog</a></p>
<h3>Fixes</h3>
<ul>
<li>
<p>Fixed comma separator bug in Excel Upload Handler error response when processing multiple files. The <code>fileId</code> counter was not incremented on error, causing malformed JSON output when an invalid file was followed by additional files.</p>
</li>
<li>
<p>Fixed <code>CustomHost</code> configuration in <code>ClientCodeGen</code> not accepting an empty string value. Setting <code>"CustomHost": ""</code> was treated the same as <code>null</code> (triggering host auto-detection) because <code>GetConfigStr</code> uses <code>string.IsNullOrEmpty</code>. Now an explicit empty string correctly produces <code>const baseUrl = "";</code> in generated TypeScript, which is useful for relative URL paths.</p>
</li>
</ul>
<h3>New Features</h3>
<ul>
<li>Added <code>fallback_handler</code> parameter to the Excel Upload Handler. When set (e.g., <code>fallback_handler = csv</code>), if ExcelDataReader fails to parse an uploaded file (invalid Excel format), the handler automatically delegates processing to the named fallback handler. This allows a single upload endpoint to accept both Excel and CSV files transparently:</li>
</ul>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function my_upload(json) is '
@upload for excel
@fallback_handler = csv
@row_command = select process_row($1,$2)
';"><pre><span class="pl-k">comment on function my_upload(json) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">@upload for excel</span>
<span class="pl-s">@fallback_handler = csv</span>
<span class="pl-s">@row_command = select process_row($1,$2)</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<h3>New Feature: Pluggable Table Format Renderers</h3>
<p>Added a pluggable table format rendering system that allows PostgreSQL function results to be rendered as HTML tables or Excel spreadsheet downloads instead of JSON, controlled by the <code>@table_format</code> annotation.</p>
<h4>HTML Table Format</h4>
<p>Renders results as a styled HTML table suitable for browser viewing and copy-paste into Excel:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function get_report() is '
HTTP GET
@table_format = html
';"><pre><span class="pl-k">comment on function get_report() is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP GET</span>
<span class="pl-s">@table_format = html</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>Configuration options in <code>TableFormatOptions</code>: <code>HtmlEnabled</code>, <code>HtmlKey</code>, <code>HtmlHeader</code>, <code>HtmlFooter</code>.</p>
<h4>Excel Table Format</h4>
<p>Renders results as an <code>.xlsx</code> Excel spreadsheet download using the SpreadCheetah library (streaming, AOT-compatible):</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function get_report() is '
HTTP GET
@table_format = excel
';"><pre><span class="pl-k">comment on function get_report() is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP GET</span>
<span class="pl-s">@table_format = excel</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>Configuration options in <code>TableFormatOptions</code>: <code>ExcelEnabled</code>, <code>ExcelKey</code>, <code>ExcelSheetName</code>, <code>ExcelDateTimeFormat</code>, <code>ExcelNumericFormat</code>.</p>
<ul>
<li><code>ExcelDateTimeFormat</code> — Excel Format Code for DateTime cells (default: <code>yyyy-MM-dd HH:mm:ss</code>). Examples: <code>yyyy-mm-dd</code>, <code>dd/mm/yyyy hh:mm</code>.</li>
<li><code>ExcelNumericFormat</code> — Excel Format Code for numeric cells (default: General). Examples: <code>#,##0.00</code>, <code>0.00</code>.</li>
</ul>
<h4>Per-Endpoint Custom Parameters</h4>
<p>The download filename and worksheet name can be overridden per-endpoint via custom parameter annotations:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function get_report() is '
HTTP GET
@table_format = excel
@excel_file_name = monthly_report.xlsx
@excel_sheet = Report Data
';"><pre><span class="pl-k">comment on function get_report() is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP GET</span>
<span class="pl-s">@table_format = excel</span>
<span class="pl-s">@excel_file_name = monthly_report.xlsx</span>
<span class="pl-s">@excel_sheet = Report Data</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>These also support dynamic placeholders resolved from function parameters:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function get_report(_format text, _file_name text, _sheet_name text) is '
HTTP GET
@table_format = {_format}
@excel_file_name = {_file_name}
@excel_sheet = {_sheet_name}
';"><pre><span class="pl-k">comment on function get_report(_format text, _file_name text, _sheet_name text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP GET</span>
<span class="pl-s">@table_format = {_format}</span>
<span class="pl-s">@excel_file_name = {_file_name}</span>
<span class="pl-s">@excel_sheet = {_sheet_name}</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<h3>TsClient: Per-Endpoint URL Export Control</h3>
<p>Added two new custom parameter annotations to control TypeScript client code generation per-endpoint:</p>
<h4><code>tsclient_export_url</code></h4>
<p>Overrides the global <code>ExportUrls</code> configuration setting for a specific endpoint:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function login(_username text, _password text) is '
HTTP POST
@login
@tsclient_export_url = true
';"><pre><span class="pl-k">comment on function login(_username text, _password text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP POST</span>
<span class="pl-s">@login</span>
<span class="pl-s">@tsclient_export_url = true</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>When enabled, the generated TypeScript exports a URL constant for that endpoint:</p>
<div class="highlight highlight-source-ts notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="export const loginUrl = () => baseUrl + "/api/login";"><pre><span class="pl-k">export</span> <span class="pl-k">const</span> <span class="pl-en">loginUrl</span> <span class="pl-c1">=</span> <span class="pl-kos">(</span><span class="pl-kos">)</span> <span class="pl-c1">=></span> <span class="pl-s1">baseUrl</span> <span class="pl-c1">+</span> <span class="pl-s">"/api/login"</span><span class="pl-kos">;</span></pre></div>
<h4><code>tsclient_url_only</code></h4>
<p>When set, only the URL constant is exported — the fetch function and response type interface are skipped entirely. Implies <code>tsclient_export_url = true</code>:</p>
<div class="highlight highlight-source-sql notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="comment on function get_data(_format text) is '
HTTP GET
@table_format = {_format}
@tsclient_url_only = true
';"><pre><span class="pl-k">comment on function get_data(_format text) is </span><span class="pl-s"><span class="pl-pds">'</span></span>
<span class="pl-s">HTTP GET</span>
<span class="pl-s">@table_format = {_format}</span>
<span class="pl-s">@tsclient_url_only = true</span>
<span class="pl-s"><span class="pl-pds">'</span></span>;</pre></div>
<p>This generates only the URL constant and request interface, which is useful for endpoints consumed via browser navigation (e.g., table format downloads) rather than fetch calls.</p>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.6.32026-02-03T07:28:47ZNpgsqlRest v3.6.3<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.6.2...3.6.3">Full Changelog</a></p>
<h3>Fixes</h3>
<ul>
<li>Fixed <code>ParseEnvironmentVariables</code> feature not working for Kestrel configuration values. Previously, environment variable placeholders (e.g., <code>{MY_HOST}</code>) in Kestrel settings like Endpoints URLs, Certificate paths/passwords, and Limits were not being replaced because Kestrel uses ASP.NET Core's direct binding which bypassed the custom placeholder processing. Now all Kestrel configuration values properly support environment variable replacement when <code>ParseEnvironmentVariables</code> is enabled.</li>
</ul>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.6.22026-02-02T13:33:56ZNpgsqlRest v3.6.2<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.6.1...3.6.2">Full Changelog</a></p>
<h3>Fixes</h3>
<ul>
<li>
<p>Fixed <code>NestedJsonForCompositeTypes</code> option from <code>RoutineOptions</code> not being applied to endpoints. Previously, only the <code>nested</code> comment annotation could enable nested JSON serialization for composite types. Now the global configuration option is properly applied as the default for all endpoints.</p>
</li>
<li>
<p>Fixed TypeScript client (<code>NpgsqlRest.TsClient</code>) generating incorrect types for composite columns when <code>NestedJsonForCompositeTypes</code> is <code>false</code> (the default). The client now correctly generates flat field types matching the actual JSON response structure, instead of always generating nested interfaces.</p>
</li>
</ul>
<h3>Breaking Changes</h3>
<ul>
<li>Added <code>NestedJsonForCompositeTypes</code> property to <code>IRoutineSource</code> interface. Custom implementations of <code>IRoutineSource</code> will need to add this property.</li>
</ul>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.6.12026-02-02T10:47:19ZNpgsqlRest v3.6.1<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.6.0...3.6.1">Full Changelog</a></p>
<h3>Fixes</h3>
<ul>
<li>Fixed <code>RequireAuthorization</code> on Stats and Health endpoints to use manual authorization check consistent with NpgsqlRest endpoints.</li>
<li>Fixed <code>ActivityQuery</code> in Stats endpoints.</li>
<li>Fixed <code>OutputFormat</code> default value in Stats endpoints.</li>
</ul>
<hr>github-actions[bot]tag:github.com,2008:Repository/731971286/v3.6.02026-02-01T10:41:30ZNpgsqlRest v3.6.0<p><a href="https://github.com/NpgsqlRest/NpgsqlRest/compare/3.5.0...3.6.0">Full Changelog</a></p>
<h3>New Feature: Security Headers Middleware</h3>
<p>Added configurable security headers middleware to protect against common web vulnerabilities. The middleware adds HTTP security headers to all responses:</p>
<ul>
<li><strong>X-Content-Type-Options</strong> - Prevents MIME-sniffing attacks (default: <code>nosniff</code>)</li>
<li><strong>X-Frame-Options</strong> - Prevents clickjacking attacks (default: <code>DENY</code>, skipped if Antiforgery is enabled)</li>
<li><strong>Referrer-Policy</strong> - Controls referrer information (default: <code>strict-origin-when-cross-origin</code>)</li>
<li><strong>Content-Security-Policy</strong> - Defines approved content sources (configurable)</li>
<li><strong>Permissions-Policy</strong> - Controls browser feature access (configurable)</li>
<li><strong>Cross-Origin-Opener-Policy</strong> - Controls document sharing with popups</li>
<li><strong>Cross-Origin-Embedder-Policy</strong> - Controls cross-origin resource loading</li>
<li><strong>Cross-Origin-Resource-Policy</strong> - Controls resource sharing cross-origin</li>
</ul>
<p>Configuration:</p>
<div class="highlight highlight-source-json-comments notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="//
// Security Headers: Adds HTTP security headers to all responses to protect against common web vulnerabilities.
// These headers instruct browsers how to handle your content securely.
// Note: X-Frame-Options is automatically handled by the Antiforgery middleware when enabled (see Antiforgery.SuppressXFrameOptionsHeader).
// Reference: https://owasp.org/www-project-secure-headers/
//
"SecurityHeaders": {
//
// Enable security headers middleware. When enabled, configured headers are added to all HTTP responses.
//
"Enabled": false,
//
// X-Content-Type-Options: Prevents browsers from MIME-sniffing a response away from the declared content-type.
// Recommended value: "nosniff"
// Set to null to not include this header.
//
"XContentTypeOptions": "nosniff",
//
// X-Frame-Options: Controls whether the browser should allow the page to be rendered in a <frame>, <iframe>, <embed> or <object>.
// Values: "DENY" (never allow), "SAMEORIGIN" (allow from same origin only)
// Note: This header is SKIPPED if Antiforgery is enabled (Antiforgery already sets X-Frame-Options: SAMEORIGIN by default).
// Set to null to not include this header.
//
"XFrameOptions": "DENY",
//
// Referrer-Policy: Controls how much referrer information should be included with requests.
// Values: "no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin",
// "same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url"
// Recommended: "strict-origin-when-cross-origin" (send origin for cross-origin requests, full URL for same-origin)
// Set to null to not include this header.
//
"ReferrerPolicy": "strict-origin-when-cross-origin",
//
// Content-Security-Policy: Defines approved sources of content that the browser may load.
// Helps prevent XSS, clickjacking, and other code injection attacks.
// Example: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
// Set to null to not include this header (recommended to configure based on your application needs).
//
"ContentSecurityPolicy": null,
//
// Permissions-Policy: Controls which browser features and APIs can be used.
// Example: "geolocation=(), microphone=(), camera=()" disables these features entirely.
// Example: "geolocation=(self), microphone=()" allows geolocation only from same origin.
// Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy
// Set to null to not include this header.
//
"PermissionsPolicy": null,
//
// Cross-Origin-Opener-Policy: Controls how your document is shared with cross-origin popups.
// Values: "unsafe-none", "same-origin-allow-popups", "same-origin"
// Set to null to not include this header.
//
"CrossOriginOpenerPolicy": null,
//
// Cross-Origin-Embedder-Policy: Prevents a document from loading cross-origin resources that don't explicitly grant permission.
// Values: "unsafe-none", "require-corp", "credentialless"
// Required for SharedArrayBuffer and high-resolution timers (along with COOP: same-origin).
// Set to null to not include this header.
//
"CrossOriginEmbedderPolicy": null,
//
// Cross-Origin-Resource-Policy: Indicates how the resource should be shared cross-origin.
// Values: "same-site", "same-origin", "cross-origin"
// Set to null to not include this header.
//
"CrossOriginResourcePolicy": null
}"><pre><span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Security Headers: Adds HTTP security headers to all responses to protect against common web vulnerabilities.</span>
<span class="pl-c"><span class="pl-c">//</span> These headers instruct browsers how to handle your content securely.</span>
<span class="pl-c"><span class="pl-c">//</span> Note: X-Frame-Options is automatically handled by the Antiforgery middleware when enabled (see Antiforgery.SuppressXFrameOptionsHeader).</span>
<span class="pl-c"><span class="pl-c">//</span> Reference: https://owasp.org/www-project-secure-headers/</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-s"><span class="pl-pds">"</span>SecurityHeaders<span class="pl-pds">"</span></span>: {
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Enable security headers middleware. When enabled, configured headers are added to all HTTP responses.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"Enabled"</span>: <span class="pl-c1">false</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> X-Content-Type-Options: Prevents browsers from MIME-sniffing a response away from the declared content-type.</span>
<span class="pl-c"><span class="pl-c">//</span> Recommended value: "nosniff"</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to not include this header.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"XContentTypeOptions"</span>: <span class="pl-s"><span class="pl-pds">"</span>nosniff<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> X-Frame-Options: Controls whether the browser should allow the page to be rendered in a <frame>, <iframe>, <embed> or <object>.</span>
<span class="pl-c"><span class="pl-c">//</span> Values: "DENY" (never allow), "SAMEORIGIN" (allow from same origin only)</span>
<span class="pl-c"><span class="pl-c">//</span> Note: This header is SKIPPED if Antiforgery is enabled (Antiforgery already sets X-Frame-Options: SAMEORIGIN by default).</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to not include this header.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"XFrameOptions"</span>: <span class="pl-s"><span class="pl-pds">"</span>DENY<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Referrer-Policy: Controls how much referrer information should be included with requests.</span>
<span class="pl-c"><span class="pl-c">//</span> Values: "no-referrer", "no-referrer-when-downgrade", "origin", "origin-when-cross-origin",</span>
<span class="pl-c"><span class="pl-c">//</span> "same-origin", "strict-origin", "strict-origin-when-cross-origin", "unsafe-url"</span>
<span class="pl-c"><span class="pl-c">//</span> Recommended: "strict-origin-when-cross-origin" (send origin for cross-origin requests, full URL for same-origin)</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to not include this header.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"ReferrerPolicy"</span>: <span class="pl-s"><span class="pl-pds">"</span>strict-origin-when-cross-origin<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Content-Security-Policy: Defines approved sources of content that the browser may load.</span>
<span class="pl-c"><span class="pl-c">//</span> Helps prevent XSS, clickjacking, and other code injection attacks.</span>
<span class="pl-c"><span class="pl-c">//</span> Example: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"</span>
<span class="pl-c"><span class="pl-c">//</span> Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to not include this header (recommended to configure based on your application needs).</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"ContentSecurityPolicy"</span>: <span class="pl-c1">null</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Permissions-Policy: Controls which browser features and APIs can be used.</span>
<span class="pl-c"><span class="pl-c">//</span> Example: "geolocation=(), microphone=(), camera=()" disables these features entirely.</span>
<span class="pl-c"><span class="pl-c">//</span> Example: "geolocation=(self), microphone=()" allows geolocation only from same origin.</span>
<span class="pl-c"><span class="pl-c">//</span> Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to not include this header.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"PermissionsPolicy"</span>: <span class="pl-c1">null</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Cross-Origin-Opener-Policy: Controls how your document is shared with cross-origin popups.</span>
<span class="pl-c"><span class="pl-c">//</span> Values: "unsafe-none", "same-origin-allow-popups", "same-origin"</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to not include this header.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"CrossOriginOpenerPolicy"</span>: <span class="pl-c1">null</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Cross-Origin-Embedder-Policy: Prevents a document from loading cross-origin resources that don't explicitly grant permission.</span>
<span class="pl-c"><span class="pl-c">//</span> Values: "unsafe-none", "require-corp", "credentialless"</span>
<span class="pl-c"><span class="pl-c">//</span> Required for SharedArrayBuffer and high-resolution timers (along with COOP: same-origin).</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to not include this header.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"CrossOriginEmbedderPolicy"</span>: <span class="pl-c1">null</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Cross-Origin-Resource-Policy: Indicates how the resource should be shared cross-origin.</span>
<span class="pl-c"><span class="pl-c">//</span> Values: "same-site", "same-origin", "cross-origin"</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to not include this header.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"CrossOriginResourcePolicy"</span>: <span class="pl-c1">null</span>
}</pre></div>
<h3>New Feature: Forwarded Headers Middleware</h3>
<p>Added support for processing proxy headers when running behind a reverse proxy (nginx, Apache, Azure App Service, AWS ALB, Cloudflare, etc.). This is critical for getting the correct client IP address and protocol.</p>
<ul>
<li><strong>X-Forwarded-For</strong> - Gets real client IP instead of proxy IP</li>
<li><strong>X-Forwarded-Proto</strong> - Gets original protocol (http/https)</li>
<li><strong>X-Forwarded-Host</strong> - Gets original host header</li>
</ul>
<p>Configuration:</p>
<div class="highlight highlight-source-json-comments notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="//
// Forwarded Headers: Enables the application to read proxy headers (X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host).
// CRITICAL: Required when running behind a reverse proxy (nginx, Apache, Azure App Service, AWS ALB, Cloudflare, etc.)
// Without this, the application sees the proxy's IP instead of the client's real IP, and HTTP instead of HTTPS.
// Security Warning: Only enable if you're behind a trusted proxy. Malicious clients can spoof these headers.
// Reference: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer
//
"ForwardedHeaders": {
//
// Enable forwarded headers middleware. Must be placed FIRST in the middleware pipeline.
//
"Enabled": false,
//
// Limits the number of proxy entries that will be processed from X-Forwarded-For.
// Default is 1 (trust only the immediate proxy). Increase if you have multiple proxies in a chain.
// Set to null to process all entries (not recommended for security).
//
"ForwardLimit": 1,
//
// List of IP addresses of known proxies to accept forwarded headers from.
// Example: ["10.0.0.1", "192.168.1.1"]
// If empty and KnownNetworks is also empty, forwarded headers are accepted from any source (less secure).
//
"KnownProxies": [],
//
// List of CIDR network ranges of known proxies.
// Example: ["10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"] for private networks
// Useful when proxy IPs are dynamically assigned within a known range.
//
"KnownNetworks": [],
//
// List of allowed values for the X-Forwarded-Host header.
// Example: ["example.com", "www.example.com"]
// If empty, any host is allowed (less secure). Helps prevent host header injection attacks.
//
"AllowedHosts": []
}"><pre><span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Forwarded Headers: Enables the application to read proxy headers (X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host).</span>
<span class="pl-c"><span class="pl-c">//</span> CRITICAL: Required when running behind a reverse proxy (nginx, Apache, Azure App Service, AWS ALB, Cloudflare, etc.)</span>
<span class="pl-c"><span class="pl-c">//</span> Without this, the application sees the proxy's IP instead of the client's real IP, and HTTP instead of HTTPS.</span>
<span class="pl-c"><span class="pl-c">//</span> Security Warning: Only enable if you're behind a trusted proxy. Malicious clients can spoof these headers.</span>
<span class="pl-c"><span class="pl-c">//</span> Reference: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-s"><span class="pl-pds">"</span>ForwardedHeaders<span class="pl-pds">"</span></span>: {
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Enable forwarded headers middleware. Must be placed FIRST in the middleware pipeline.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"Enabled"</span>: <span class="pl-c1">false</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Limits the number of proxy entries that will be processed from X-Forwarded-For.</span>
<span class="pl-c"><span class="pl-c">//</span> Default is 1 (trust only the immediate proxy). Increase if you have multiple proxies in a chain.</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to process all entries (not recommended for security).</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"ForwardLimit"</span>: <span class="pl-c1">1</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> List of IP addresses of known proxies to accept forwarded headers from.</span>
<span class="pl-c"><span class="pl-c">//</span> Example: ["10.0.0.1", "192.168.1.1"]</span>
<span class="pl-c"><span class="pl-c">//</span> If empty and KnownNetworks is also empty, forwarded headers are accepted from any source (less secure).</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"KnownProxies"</span>: [],
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> List of CIDR network ranges of known proxies.</span>
<span class="pl-c"><span class="pl-c">//</span> Example: ["10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"] for private networks</span>
<span class="pl-c"><span class="pl-c">//</span> Useful when proxy IPs are dynamically assigned within a known range.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"KnownNetworks"</span>: [],
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> List of allowed values for the X-Forwarded-Host header.</span>
<span class="pl-c"><span class="pl-c">//</span> Example: ["example.com", "www.example.com"]</span>
<span class="pl-c"><span class="pl-c">//</span> If empty, any host is allowed (less secure). Helps prevent host header injection attacks.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"AllowedHosts"</span>: []
}</pre></div>
<h3>New Feature: Health Check Endpoints</h3>
<p>Added health check endpoints for container orchestration (Kubernetes, Docker Swarm) and monitoring systems:</p>
<ul>
<li><strong>/health</strong> - Overall health status (combines all checks)</li>
<li><strong>/health/ready</strong> - Readiness probe with optional PostgreSQL connectivity check</li>
<li><strong>/health/live</strong> - Liveness probe (always returns healthy if app is running)</li>
</ul>
<p>Configuration:</p>
<div class="highlight highlight-source-json-comments notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="//
// Health Checks: Provides endpoints for monitoring application health, used by container orchestrators (Kubernetes, Docker Swarm),
// load balancers, and monitoring systems to determine if the application is running correctly.
// Three types of checks are supported:
// - /health: Overall health status (combines all checks)
// - /health/ready: Readiness probe - is the app ready to accept traffic? (includes database connectivity)
// - /health/live: Liveness probe - is the app process running? (always returns healthy if app responds)
// Reference: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks
//
"HealthChecks": {
//
// Enable health check endpoints.
//
"Enabled": false,
//
// Cache health check responses server-side in memory for the specified duration.
// Cached responses are served without re-executing the endpoint.
// Value is in PostgreSQL interval format (e.g., '5 seconds', '1 minute', '30s', '1min').
// Set to null to disable caching. Query strings are ignored to prevent cache-busting.
//
"CacheDuration": "5 seconds",
//
// Path for the main health check endpoint that reports overall status.
// Returns "Healthy", "Degraded", or "Unhealthy" with HTTP 200 (healthy/degraded) or 503 (unhealthy).
//
"Path": "/health",
//
// Path for the readiness probe endpoint.
// Kubernetes uses this to know when a pod is ready to receive traffic.
// Includes database connectivity check when IncludeDatabaseCheck is true.
// Returns 503 Service Unavailable if database is unreachable.
//
"ReadyPath": "/health/ready",
//
// Path for the liveness probe endpoint.
// Kubernetes uses this to know when to restart a pod.
// Always returns Healthy (200) if the application process is responding.
// Does NOT check database - a slow database shouldn't trigger a container restart.
//
"LivePath": "/health/live",
//
// Include PostgreSQL database connectivity in health checks.
// When true, the readiness probe will fail if the database is unreachable.
//
"IncludeDatabaseCheck": true,
//
// Name for the database health check (appears in detailed health reports).
//
"DatabaseCheckName": "postgresql",
//
// Require authentication for health check endpoints.
// When true, all health endpoints require a valid authenticated user.
// Security Consideration: Health endpoints can reveal information about your infrastructure
// (database connectivity, service status). Enable this if your health endpoints are publicly accessible.
// Note: Kubernetes/Docker health probes may need to authenticate if this is enabled.
//
"RequireAuthorization": false,
//
// Apply a rate limiter policy to health check endpoints.
// Specify the name of a policy defined in RateLimiterOptions.Policies.
// Security Consideration: Prevents denial-of-service attacks targeting health endpoints.
// Set to null to disable rate limiting on health endpoints.
// Example: "fixed" or "bucket" (must match a policy name from RateLimiterOptions).
//
"RateLimiterPolicy": null
}"><pre><span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Health Checks: Provides endpoints for monitoring application health, used by container orchestrators (Kubernetes, Docker Swarm),</span>
<span class="pl-c"><span class="pl-c">//</span> load balancers, and monitoring systems to determine if the application is running correctly.</span>
<span class="pl-c"><span class="pl-c">//</span> Three types of checks are supported:</span>
<span class="pl-c"><span class="pl-c">//</span> - /health: Overall health status (combines all checks)</span>
<span class="pl-c"><span class="pl-c">//</span> - /health/ready: Readiness probe - is the app ready to accept traffic? (includes database connectivity)</span>
<span class="pl-c"><span class="pl-c">//</span> - /health/live: Liveness probe - is the app process running? (always returns healthy if app responds)</span>
<span class="pl-c"><span class="pl-c">//</span> Reference: https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-s"><span class="pl-pds">"</span>HealthChecks<span class="pl-pds">"</span></span>: {
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Enable health check endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"Enabled"</span>: <span class="pl-c1">false</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Cache health check responses server-side in memory for the specified duration.</span>
<span class="pl-c"><span class="pl-c">//</span> Cached responses are served without re-executing the endpoint. </span>
<span class="pl-c"><span class="pl-c">//</span> Value is in PostgreSQL interval format (e.g., '5 seconds', '1 minute', '30s', '1min').</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to disable caching. Query strings are ignored to prevent cache-busting.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"CacheDuration"</span>: <span class="pl-s"><span class="pl-pds">"</span>5 seconds<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Path for the main health check endpoint that reports overall status.</span>
<span class="pl-c"><span class="pl-c">//</span> Returns "Healthy", "Degraded", or "Unhealthy" with HTTP 200 (healthy/degraded) or 503 (unhealthy).</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"Path"</span>: <span class="pl-s"><span class="pl-pds">"</span>/health<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Path for the readiness probe endpoint.</span>
<span class="pl-c"><span class="pl-c">//</span> Kubernetes uses this to know when a pod is ready to receive traffic.</span>
<span class="pl-c"><span class="pl-c">//</span> Includes database connectivity check when IncludeDatabaseCheck is true.</span>
<span class="pl-c"><span class="pl-c">//</span> Returns 503 Service Unavailable if database is unreachable.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"ReadyPath"</span>: <span class="pl-s"><span class="pl-pds">"</span>/health/ready<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Path for the liveness probe endpoint.</span>
<span class="pl-c"><span class="pl-c">//</span> Kubernetes uses this to know when to restart a pod.</span>
<span class="pl-c"><span class="pl-c">//</span> Always returns Healthy (200) if the application process is responding.</span>
<span class="pl-c"><span class="pl-c">//</span> Does NOT check database - a slow database shouldn't trigger a container restart.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"LivePath"</span>: <span class="pl-s"><span class="pl-pds">"</span>/health/live<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Include PostgreSQL database connectivity in health checks.</span>
<span class="pl-c"><span class="pl-c">//</span> When true, the readiness probe will fail if the database is unreachable.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"IncludeDatabaseCheck"</span>: <span class="pl-c1">true</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Name for the database health check (appears in detailed health reports).</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"DatabaseCheckName"</span>: <span class="pl-s"><span class="pl-pds">"</span>postgresql<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Require authentication for health check endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span> When true, all health endpoints require a valid authenticated user.</span>
<span class="pl-c"><span class="pl-c">//</span> Security Consideration: Health endpoints can reveal information about your infrastructure</span>
<span class="pl-c"><span class="pl-c">//</span> (database connectivity, service status). Enable this if your health endpoints are publicly accessible.</span>
<span class="pl-c"><span class="pl-c">//</span> Note: Kubernetes/Docker health probes may need to authenticate if this is enabled.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"RequireAuthorization"</span>: <span class="pl-c1">false</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Apply a rate limiter policy to health check endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span> Specify the name of a policy defined in RateLimiterOptions.Policies.</span>
<span class="pl-c"><span class="pl-c">//</span> Security Consideration: Prevents denial-of-service attacks targeting health endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to disable rate limiting on health endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span> Example: "fixed" or "bucket" (must match a policy name from RateLimiterOptions).</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"RateLimiterPolicy"</span>: <span class="pl-c1">null</span>
}</pre></div>
<p>Added new dependency: <code>AspNetCore.HealthChecks.NpgSql</code> for PostgreSQL health checks.</p>
<h3>New Feature: PostgreSQL Statistics Endpoints</h3>
<p>Added HTTP endpoints for monitoring PostgreSQL database statistics, useful for debugging, performance analysis, and operational monitoring:</p>
<ul>
<li><strong>/stats/routines</strong> - Function/procedure performance statistics from <code>pg_stat_user_functions</code> (call counts, execution times)</li>
<li><strong>/stats/tables</strong> - Table statistics from <code>pg_stat_user_tables</code> (tuple counts, sizes, scan counts, vacuum info)</li>
<li><strong>/stats/indexes</strong> - Index statistics from <code>pg_stat_user_indexes</code> (scan counts, definitions)</li>
<li><strong>/stats/activity</strong> - Current database activity from <code>pg_stat_activity</code> (active sessions, queries, wait events)</li>
</ul>
<p>Output formats:</p>
<ul>
<li><strong>HTML</strong> (default) - HTML table with Excel-compatible formatting for direct browser copy-paste</li>
<li><strong>JSON</strong> - JSON array with camelCase property names</li>
</ul>
<p>Configuration:</p>
<div class="highlight highlight-source-json-comments notranslate position-relative overflow-auto" data-snippet-clipboard-copy-content="//
// PostgreSQL Statistics Endpoints
// Exposes PostgreSQL statistics through HTTP endpoints for monitoring and debugging.
// Provides access to pg_stat_user_functions, pg_stat_user_tables, pg_stat_user_indexes, and pg_stat_activity.
//
"Stats": {
//
// Enable PostgreSQL statistics endpoints.
//
"Enabled": false,
//
// Cache stats responses server-side in memory for the specified duration.
// Cached responses are served without re-executing the endpoint.
// Value is in PostgreSQL interval format (e.g., '5 seconds', '1 minute', '30s', '1min').
// Set to null to disable caching. Query strings are ignored to prevent cache-busting.
//
"CacheDuration": "5 seconds",
//
// Apply a rate limiter policy to stats endpoints.
// Specify the name of a policy defined in RateLimiterOptions.Policies.
// Set to null to disable rate limiting on stats endpoints.
//
"RateLimiterPolicy": null,
//
// Use a specific named connection for stats queries.
// When null, uses the default connection string.
// Useful when you want to query stats from a different database or use read-only credentials.
//
"ConnectionName": null,
//
// Require authentication for stats endpoints.
// Security Consideration: Stats endpoints can reveal sensitive information about your database
// (table sizes, query patterns, active sessions). Enable this for production environments.
//
"RequireAuthorization": false,
//
// Restrict access to specific roles.
// When null or empty, any authenticated user can access (if RequireAuthorization is true).
// Example: ["admin", "dba"] - only users with admin or dba role can access.
//
"AuthorizedRoles": [],
//
// Output format for stats endpoints: "json" or "html".
// - json: JSON array
// - html: HTML table, Excel-compatible for direct browser copy-paste (default)
//
"OutputFormat": "html",
//
// Filter schemas using PostgreSQL SIMILAR TO pattern.
// When null, all schemas are included.
// Example: "public|myapp%" - includes 'public' and schemas starting with 'myapp'.
//
"SchemaSimilarTo": null,
//
// Path for routine (function/procedure) performance statistics.
// Returns data from pg_stat_user_functions including call counts and execution times.
// Note: Requires track_functions = 'pl' or 'all' in postgresql.conf.
// Enable with: alter system set track_functions = 'all'; select pg_reload_conf();
// Or set track_functions = 'all' directly in postgresql.conf and restart/reload.
//
"RoutinesStatsPath": "/stats/routines",
//
// Path for table statistics.
// Returns data from pg_stat_user_tables including tuple counts, sizes, scan counts, and vacuum info.
//
"TablesStatsPath": "/stats/tables",
//
// Path for index statistics.
// Returns data from pg_stat_user_indexes including scan counts and index definitions.
//
"IndexesStatsPath": "/stats/indexes",
//
// Path for current database activity.
// Returns data from pg_stat_activity showing active sessions, queries, and wait events.
// Security Consideration: Shows currently running queries which may contain sensitive data.
//
"ActivityPath": "/stats/activity"
}"><pre><span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> PostgreSQL Statistics Endpoints</span>
<span class="pl-c"><span class="pl-c">//</span> Exposes PostgreSQL statistics through HTTP endpoints for monitoring and debugging.</span>
<span class="pl-c"><span class="pl-c">//</span> Provides access to pg_stat_user_functions, pg_stat_user_tables, pg_stat_user_indexes, and pg_stat_activity.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-s"><span class="pl-pds">"</span>Stats<span class="pl-pds">"</span></span>: {
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Enable PostgreSQL statistics endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"Enabled"</span>: <span class="pl-c1">false</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Cache stats responses server-side in memory for the specified duration.</span>
<span class="pl-c"><span class="pl-c">//</span> Cached responses are served without re-executing the endpoint.</span>
<span class="pl-c"><span class="pl-c">//</span> Value is in PostgreSQL interval format (e.g., '5 seconds', '1 minute', '30s', '1min').</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to disable caching. Query strings are ignored to prevent cache-busting.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"CacheDuration"</span>: <span class="pl-s"><span class="pl-pds">"</span>5 seconds<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Apply a rate limiter policy to stats endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span> Specify the name of a policy defined in RateLimiterOptions.Policies.</span>
<span class="pl-c"><span class="pl-c">//</span> Set to null to disable rate limiting on stats endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"RateLimiterPolicy"</span>: <span class="pl-c1">null</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Use a specific named connection for stats queries.</span>
<span class="pl-c"><span class="pl-c">//</span> When null, uses the default connection string.</span>
<span class="pl-c"><span class="pl-c">//</span> Useful when you want to query stats from a different database or use read-only credentials.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"ConnectionName"</span>: <span class="pl-c1">null</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Require authentication for stats endpoints.</span>
<span class="pl-c"><span class="pl-c">//</span> Security Consideration: Stats endpoints can reveal sensitive information about your database</span>
<span class="pl-c"><span class="pl-c">//</span> (table sizes, query patterns, active sessions). Enable this for production environments.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"RequireAuthorization"</span>: <span class="pl-c1">false</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Restrict access to specific roles.</span>
<span class="pl-c"><span class="pl-c">//</span> When null or empty, any authenticated user can access (if RequireAuthorization is true).</span>
<span class="pl-c"><span class="pl-c">//</span> Example: ["admin", "dba"] - only users with admin or dba role can access.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"AuthorizedRoles"</span>: [],
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Output format for stats endpoints: "json" or "html".</span>
<span class="pl-c"><span class="pl-c">//</span> - json: JSON array</span>
<span class="pl-c"><span class="pl-c">//</span> - html: HTML table, Excel-compatible for direct browser copy-paste (default)</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"OutputFormat"</span>: <span class="pl-s"><span class="pl-pds">"</span>html<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Filter schemas using PostgreSQL SIMILAR TO pattern.</span>
<span class="pl-c"><span class="pl-c">//</span> When null, all schemas are included.</span>
<span class="pl-c"><span class="pl-c">//</span> Example: "public|myapp%" - includes 'public' and schemas starting with 'myapp'.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"SchemaSimilarTo"</span>: <span class="pl-c1">null</span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Path for routine (function/procedure) performance statistics.</span>
<span class="pl-c"><span class="pl-c">//</span> Returns data from pg_stat_user_functions including call counts and execution times.</span>
<span class="pl-c"><span class="pl-c">//</span> Note: Requires track_functions = 'pl' or 'all' in postgresql.conf.</span>
<span class="pl-c"><span class="pl-c">//</span> Enable with: alter system set track_functions = 'all'; select pg_reload_conf();</span>
<span class="pl-c"><span class="pl-c">//</span> Or set track_functions = 'all' directly in postgresql.conf and restart/reload.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"RoutinesStatsPath"</span>: <span class="pl-s"><span class="pl-pds">"</span>/stats/routines<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Path for table statistics.</span>
<span class="pl-c"><span class="pl-c">//</span> Returns data from pg_stat_user_tables including tuple counts, sizes, scan counts, and vacuum info.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"TablesStatsPath"</span>: <span class="pl-s"><span class="pl-pds">"</span>/stats/tables<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Path for index statistics.</span>
<span class="pl-c"><span class="pl-c">//</span> Returns data from pg_stat_user_indexes including scan counts and index definitions.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"IndexesStatsPath"</span>: <span class="pl-s"><span class="pl-pds">"</span>/stats/indexes<span class="pl-pds">"</span></span>,
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-c"><span class="pl-c">//</span> Path for current database activity.</span>
<span class="pl-c"><span class="pl-c">//</span> Returns data from pg_stat_activity showing active sessions, queries, and wait events.</span>
<span class="pl-c"><span class="pl-c">//</span> Security Consideration: Shows currently running queries which may contain sensitive data.</span>
<span class="pl-c"><span class="pl-c">//</span></span>
<span class="pl-ent">"ActivityPath"</span>: <span class="pl-s"><span class="pl-pds">"</span>/stats/activity<span class="pl-pds">"</span></span>
}</pre></div>
<hr>github-actions[bot]