Skip to content

Changelog v3.21.0

Version 3.21.0 (2026-07-12)

Full Changelog

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
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 RoutineOptions settings and the global schema/name filters are shared).
  • Endpoints execute on the connection they were discovered from — a routine on the olap database is discovered there and runs there, annotations included, with nothing extra to configure. An explicit connection comment 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: true required (an Information log notes it).
  • null or 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 (and IEndpointSource via a default interface member) gains a ConnectionName property, and CreateAndOpenSourceConnection accepts an optional per-source connection name that overrides MetadataQueryConnectionName.

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
json
{
  "NpgsqlRest": {
    "RoutineOptions": {
      "VerifyRoutedEndpoints": "Warn"
    }
  }
}
  • "None" (default) — no verification.
  • "Warn" — at startup, one batched existence check per distinct target connection (to_regprocedure for functions/procedures; to_regclass for tables/views when composing CrudSource in 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:

FormBehavior
{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
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 --config output and --validate resolve 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} sends free when _plan is 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.SubstitutionEnvironmentVariables directly: 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
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 EnvFile users: 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 EnvFile path 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": null to disable loading entirely. Configurations that omit the EnvFile key keep the previous behavior (no file is loaded) — the ./.env default 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
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 to master with 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_URL environment variable (same {base}/{ref}/.claude/skills/npgsqlrest/{file} layout).

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 makes ConnectionSettings:MetadataQueryConnectionName accept 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 second NpgsqlMultiHostDataSource built for the same connection string; it now references the same instance as the main data source.
  • The connection=name annotation form is validated. The key = value parse 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 consults DataSources (multi-host) too, so multi-host-only names no longer produce a false warning.
  • Watch mode polls the metadata connection. The --watch database poller always fingerprinted the primary connection, even when ConnectionSettings:MetadataQueryConnectionName pointed 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/DataSources dictionaries built by the client) is now case-insensitive, consistent with IConfiguration key semantics — a lowercased annotation name finds a mixed-case ConnectionStrings entry.
  • Fixed the --annotations CLI reference output missing six annotations: single, void, skip (aliases skip_result, no_result), returns, mcp (aliases mcp_name, mcp_description, mcp_desc) and openapi. The annotations themselves were always functional — only the JSON reference output was incomplete.

Comments