Changelog v3.21.0
Version 3.21.0 (2026-07-12)
This release removes the single-connection assumption from multiple-connection setups: routine metadata can now be discovered per connection (ReadMetadataFromConnections) with endpoints executing on the connection they were discovered from, plus opt-in verification of @connection-routed endpoints (VerifyRoutedEndpoints). Also included: environment variable fallback values ({!NAME:fallback}) unified across configuration, static files, and annotations; a .env file loaded by default; the --install-skill command for the Claude Code skill; a new CLI logo; and a set of multi-connection fixes.
Note: version 3.20.1 was never released — its changes are included in this release.
Features
Per-connection routine discovery: ReadMetadataFromConnections
Until now, routine metadata (functions and procedures) was always discovered on a single connection, while the connection annotation only changed where a request executes. That silently assumed every named connection has identical routine metadata — true for read replicas and shards, but wrong for setups where different databases host different routines (OLTP + OLAP/DW): a routine living only on the OLAP database had to be duplicated on the default database (with its annotation) just to be discovered, even though it was never called there.
That requirement is gone:
json
{
"ConnectionStrings": {
"Default": "Host=localhost;Port=5432;Database=oltp;Username=postgres;Password=postgres",
"olap": "Host=warehouse;Port=5432;Database=olap;Username=postgres;Password=postgres"
},
"NpgsqlRest": {
"RoutineOptions": {
"ReadMetadataFromConnections": [ "Default", "olap" ]
}
}
}- Routine metadata is read from each listed connection (one routine source per name; all other
RoutineOptionssettings and the global schema/name filters are shared). - Endpoints execute on the connection they were discovered from — a routine on the
olapdatabase is discovered there and runs there, annotations included, with nothing extra to configure. An explicitconnectioncomment annotation still wins. - The list replaces the default — include the main connection's name (as above) to keep serving its routines.
- Setting the key implicitly enables multiple connections — no
UseMultipleConnections: truerequired (an Information log notes it). nullor absent = exactly the previous behavior (single discovery connection).- Every listed name must exist in
ConnectionStrings— an unknown name stops startup with an error listing the available names. - Composite types now resolve per database: two databases can have a same-named composite type with different fields and each source serializes with its own definition.
- Watch mode (
--watch) polls each discovery connection: a routine created/dropped/commented on any listed connection triggers the restart, not just the primary. - For library (middleware) users:
RoutineSource(andIEndpointSourcevia a default interface member) gains aConnectionNameproperty, andCreateAndOpenSourceConnectionaccepts an optional per-source connection name that overridesMetadataQueryConnectionName.
When two sources discover the same path (a routine existing on both databases), the later source wins and a startup warning names both sources and connections — use schema/name filters or a path annotation to disambiguate.
Use ReadMetadataFromConnections when databases host different routines. For identical databases (replicas, shards), keep using UseMultipleConnections with the connection annotation — single discovery is correct and cheaper there.
Routed endpoint verification: VerifyRoutedEndpoints
Endpoints routed by the connection annotation to a different connection than they were discovered on assume the target database has the same routines. Nothing checked that assumption until the first failing request. Now, opt-in:
json
{
"NpgsqlRest": {
"RoutineOptions": {
"VerifyRoutedEndpoints": "Warn"
}
}
}"None"(default) — no verification."Warn"— at startup, one batched existence check per distinct target connection (to_regprocedurefor functions/procedures;to_regclassfor tables/views when composingCrudSourcein the library); every missing routine is logged as a warning naming the routine, the connection, and the endpoints that need it."Fail"— same check, but any missing routine stops startup with an aggregated error.
This checks content (does the routine exist over there?) — connectivity of every connection string remains a separate concern covered by ConnectionSettings:TestConnectionStrings (opens and closes the connection). The check verifies existence/signature only (result shapes are not compared), and SQL-file endpoints are skipped (no catalog name to check). Cost: one connection and one round-trip per distinct annotated target connection, only when enabled.
Environment variable fallback values: {!NAME:fallback}
Environment variable placeholders now support an inline fallback value, completing the placeholder grammar:
| Form | Behavior |
|---|---|
{NAME} | Optional — replaced when the variable is set, left untouched otherwise. |
{!NAME} | Required — replaced when the variable is set, startup fails otherwise. |
{!NAME:fallback} | Replaced when the variable is set, otherwise the literal fallback text is used — never fails. |
The fallback starts after the first : and runs to the closing brace, so it may itself contain : (e.g. {!BASE_URL:http://localhost:5000}), but not }. Only the {! form takes a fallback — a plain {NAME:...} is never treated as an env placeholder, which keeps brace-colon content like Serilog format specifiers ({Timestamp:HH:mm:ss}), inline CSS (td{border:1px solid}), and TypeScript object types ({status: number}) intact.
The default connection string now uses fallbacks matching the standard PostgreSQL defaults for everything except the database name, which stays required:
json
{
"ConnectionStrings": {
"Default": "Host={!PGHOST:localhost};Port={!PGPORT:5432};Database={!PGDATABASE};Username={!PGUSER:postgres};Password={!PGPASSWORD:postgres}"
}
}With no environment configured at all, startup fails with exactly one actionable error — Required environment variable 'PGDATABASE' (referenced as '{!PGDATABASE}' in configuration) is not set. Setting only PGDATABASE connects to localhost:5432 as postgres/postgres.
Notes:
- The
--configoutput and--validateresolve fallbacks (the fallback is the value the running application will use), while unset{!NAME}placeholders without a fallback are still kept as literals so the configuration can always be inspected before the environment is set up. - Works in every configuration value (connection strings, test-runner connections, any typed value — e.g.
"Port": "{!PORT:5432}").
The same placeholder rules in static content and annotation values
The {!NAME} and {!NAME:fallback} forms now also work in the two request-time substitution systems, with the same grammar and their existing allowlist security model unchanged:
- Static files (
{ "StaticFiles": { "ParseContentOptions": { "AvailableEnvVars": [...] } } }):{!NAME}resolves like{NAME}for listed names;{!NAME:fallback}uses the inline fallback when the listed variable is unset and has no configured default. The same applies to claims:{!claim_name:fallback}uses the fallback when the claim is absent (e.g. anonymous requests). Unlisted names are always left untouched — the strict forms never widen the allowlist. - Comment annotations and HTTP Custom Types (
{ "NpgsqlRest": { "AvailableEnvVars": [...] } }): same rules for response headers, custom parameters, and HTTP custom type URL/body/header values. Routine parameters keep precedence over env vars in both forms, and a null parameter value now yields the inline fallback:X-Plan: {!_plan:free}sendsfreewhen_planis null. - The build-time unknown-placeholder warning understands the new forms and validates the name part, so a typo in
{!NAME:fallback}is still caught at startup. - For library (middleware) users populating
NpgsqlRestOptions.SubstitutionEnvironmentVariablesdirectly: register a second"!NAME"key alongside each"NAME"entry that resolved to a real value — the standalone client does this automatically. See the property documentation.
.env file loaded by default ("EnvFile": "./.env")
The default configuration now ships with:
json
{
"Config": {
"EnvFile": "./.env"
}
}Combined with the new fallback connection string, this makes the minimal setup a single file — create .env next to the configuration with one line, PGDATABASE=mydb, and run npgsqlrest.
Behavior:
- Variables already present in the process environment always win — the file only fills in missing ones (the standard dotenv convention). ⚠️ This is a behavior change for existing
EnvFileusers: before 3.21.0 the file overwrote the environment. Within the file itself, a repeated key keeps its last value. - The default
./.env(relative to the working directory) is optional: when the file does not exist, startup logs an information message —Env file ./.env not found, skipping (set Config:EnvFile to null to disable env file loading)— never a warning, so clean production deployments (env vars from the orchestrator, no file) stay warning-free. - A custom
EnvFilepath that does not exist logs a warning — you explicitly named a file that is not there. - When the file loads, startup logs how many variables it contributed and how many were kept from the real environment:
Loaded 3 environment variable(s) from env file ./.env (1 already set in the environment and kept). - Set
"EnvFile": nullto disable loading entirely. Configurations that omit theEnvFilekey keep the previous behavior (no file is loaded) — the./.envdefault comes from the shipped configuration file, not from code.
--install-skill: install the Claude Code skill from the CLI
The official Claude Code skill can now be installed directly from the binary, replacing the platform-specific curl/Invoke-WebRequest instructions:
console
npgsqlrest --install-skill # project scope: ./.claude/skills/npgsqlrest (commit it for the whole team)
npgsqlrest --install-skill global # user scope: ~/.claude/skills/npgsqlrest (available in every project)- Downloads the three skill files (
SKILL.md,annotations-reference.md,configuration-reference.jsonc) from the release branch matching the running version (e.g.3.21.0), so the skill references always match the installed binary. When that branch is not available, it falls back tomasterwith a printed note. - All files come from the same git ref and are downloaded fully before anything is written — a network failure never leaves a partial install behind.
- Works the same on every platform; upgrading is re-running the same command after upgrading the binary.
- For tests and enterprise mirrors, the download base is overridable via the
NPGSQLREST_SKILL_BASE_URLenvironment variable (same{base}/{ref}/.claude/skills/npgsqlrest/{file}layout).
New CLI logo
The CLI banner (shown by --help, --version, --install-skill, ...) was replaced with a two-tone ANSI-block design and a tagline. The logo now sets UTF-8 console output (guarded), so the block glyphs render correctly on legacy Windows consoles; it never prints in server logs.
Fixes
connection <main-name>annotation now works. The main connection was excluded from the routable connections dictionary, so routing an endpoint to the main connection by its own name failed every request with a 500 (Connection name ... could not be found). The main connection is now registered under its real name — and resolves to the same data source/pool as unrouted requests. The same fix makesConnectionSettings:MetadataQueryConnectionNameaccept the main connection's name (previously a startup error).- No more duplicate pool for a multi-host main connection. The
"_default"data-source alias used to be a secondNpgsqlMultiHostDataSourcebuilt for the same connection string; it now references the same instance as the main data source. - The
connection=nameannotation form is validated. Thekey = valueparse path assigned the connection name with no validation at all; both forms now warn at startup when the name is not registered — and the check consultsDataSources(multi-host) too, so multi-host-only names no longer produce a false warning. - Watch mode polls the metadata connection. The
--watchdatabase poller always fingerprinted the primary connection, even whenConnectionSettings:MetadataQueryConnectionNamepointed discovery at a different connection — it could watch a database that discovery never read. Pollers now follow each source's actual discovery connection. - Connection name matching (
ConnectionStrings/DataSourcesdictionaries built by the client) is now case-insensitive, consistent withIConfigurationkey semantics — a lowercased annotation name finds a mixed-caseConnectionStringsentry. - Fixed the
--annotationsCLI reference output missing six annotations:single,void,skip(aliasesskip_result,no_result),returns,mcp(aliasesmcp_name,mcp_description,mcp_desc) andopenapi. The annotations themselves were always functional — only the JSON reference output was incomplete.