Changelog¶
All notable changes to FirebirdREST are listed here. The project follows semantic versioning; until 1.0, minor versions may change the API.
Unreleased¶
0.6.0 - 2026-10-26¶
Added¶
- OpenTelemetry traces over OTLP/HTTP:
otel-traces-endpoint(or the standardOTEL_EXPORTER_OTLP_*variables) exports a span per request with children for the JWT check, the transaction and each SQL statement; W3Ctraceparentjoins the caller's trace and decides sampling,otel-traces-sample-ratiosamples the rest. No new dependencies: the exporter is written for FirebirdREST, like the metrics. Checked against Jaeger (traces). - Built-in TLS:
server-tls-certandserver-tls-keyserve HTTPS (TLS 1.2+, HTTP/2).SIGHUPreloads the certificate without dropping connections, keeping the current one if the new files don't load, and reloads the schema cache;-checkvalidates the files and prints the expiry, and a certificate expiring within 14 days is logged (TLS). .deband.rpmpackages for amd64 and arm64 with every release: the binary, a hardened systemd service running as afirebirdrestuser with a configuration check before start, and/etc/firebirdrest/firebirdrest.confkept on upgrade (packages).- A tutorial, Sign-in with Keycloak: tokens from an identity provider checked with
jwt-jwks-urlandjwt-aud, a role claim mapped from a user attribute, key rotation, and the settings for Auth0 and Microsoft Entra ID.examples/keycloak/has the Compose file and the realm setup script, tested against Keycloak 26.4. - A fuzz test of the URL grammar, run in CI: arbitrary query strings must produce SQL whose values are all parameters and whose identifiers all come from the schema, and must be refused with a 4xx error rather than a panic or a 500.
Changed¶
SIGHUPnow reloads the schema cache (and the TLS certificate), likeSIGUSR1; the documented systemd unit uses it forsystemctl reload. The HTTP server's own errors, such as failed TLS handshakes, go to the structured log at debug level.
Fixed¶
- Many-to-many embedding found no relationship when the junction table's foreign keys were not linked to a loaded schema; only schemas built in memory (tests, tools) were affected.
0.5.0 - 2026-10-26¶
Added¶
- Realtime events:
GET /events/<name>[,<name>...]streams FirebirdPOST_EVENTnotifications as Server-Sent Events, for the names listed indb-eventsand the roles indb-events-roles. One database attachment serves every stream; keepalives every 15 s; metrics for streams and notifications (events). The example database postsorder_placedfromplace_orderand has anotify(name)procedure. - Query plans for writes:
Accept: application/vnd.pgrst.planonPOST,PATCH,PUTandDELETEruns the request in a transaction that is always rolled back and returns the plans of its statements, as PostgREST does. application/vnd.pgrst.plan+jsonaddstree, the explained plan parsed into{node, children}nodes, next to the text inplan.
Changed¶
- Plain JSON reads (no embedded resources) encode each row as it is read instead of keeping every row and then the JSON: a 200,000-row, 106 MB response peaks at 216 MB of process memory instead of 536 MB and takes 1.7 s instead of 4.5 s.
Prefer: params=multiple-objectson a selectable procedure takes up to 1024 objects (their arguments within 48 KB) instead of 200: the calls are driven by a counting CTE in one statement, so the procedure is one context however many objects there are.
0.4.0 - 2026-10-24¶
Changed¶
- Reads cost 4 round trips to Firebird instead of 7:
GET/HEADrequests reuse a long-lived read-onlyREAD COMMITTEDtransaction per connection (same results as a fresh one, see Transactions;db-read-tx-reuse = falserestores the old behaviour), and the request context block no longer triggers a row-count request. On the benchmark database: a row by key +30%, a function call +60%, a computed field with order +25%, a 25-row embed +30%. - Multi-row inserts no longer run one statement per row. On Firebird 4+, a plain insert with
return=minimalgoes through the wire protocol's batch API (one batch per column set); everything else (Firebird 3, upserts,return=representation, BLOB values) runs inEXECUTE BLOCKs of up to 100 rows that return every row's keys. A 100-row insert takes a third of the time it took, a 1000-row one less. Rows of a view are still inserted one by one. Transactions are now bound to one pooled connection each, which the batch API needs.
Added¶
- Relationships across SQL schemas on Firebird 6: a foreign key between tables of two exposed
schemas (
db-schemas) is an embeddable relationship in both directions, as in PostgREST, also when the request's schema has a table of the same name. Foreign keys into schemas that aren't exposed stay out (schemas). - Benchmarks against PostgREST on identical data, with the PostgreSQL schema and seed in
bench/postgrest/and the results and their interpretation on the Benchmarks page. - A stability promise saying which interfaces are stable, which are experimental, and how deprecations and behaviour changes are announced, and an upgrade guide with the changes to check for each version.
firebirdrest -check [config-file]validates the configuration and prints warnings without connecting; the warnings are also logged at startup. Deprecated setting names, when there are any, keep working for a minor version with a warning.
0.3.0 - 2026-10-23¶
Production readiness: Prometheus metrics, Server-Timing, JWKS keys from identity providers
with a token cache, single values and files in any media type, raw bodies for RPC, and the fixes
of a security review. A JavaScript client with the supabase-js API,
firebirdrest-js, checks compatibility in CI.
Behaviour changes to check before upgrading:
server-cors-allowed-origins = "*"no longer sendsAccess-Control-Allow-Credentials; list the origins explicitly if browsers must send credentials.GET /describes only what the caller's role is granted; setopenapi-mode = "ignore-privileges"for the old document.- Requests are limited to 60 seconds and 16 MB bodies (
server-request-timeout,server-max-body-size), andand=/or=nesting to 16 levels,in.()to 1000 values. - Permission errors no longer include Firebird's "Effective user is ..." line, and unexpected errors return a generic message (the cause is logged).
- Pool and statement cache metrics label roles in upper case.
- Responses carry
X-Content-Type-Options: nosniffand a sandboxingContent-Security-Policy.
Security¶
A review of the SQL generation, authentication and HTTP surface found no injection or authentication bypass, and these weaknesses, all fixed here:
- A deeply nested
and=/or=filter or a longin.()list could take the Firebird connection down or burn CPU for tens of seconds: nesting is limited to 16 levels andin.()to 1000 values (400beyond). - A
Rangeheader with a huge end overflowed the limit arithmetic and removeddb-max-rows; such ranges are refused with416. - A single value requested as
text/html(or SVG, XML, script) was served as a page of the API's origin: stored script in a column could run there. Every response now carriesX-Content-Type-Options: nosniffand a sandboxingContent-Security-Policy, and those types are sent as downloads. - With the default
server-cors-allowed-origins = "*", any origin was reflected together withAccess-Control-Allow-Credentials: true; the wildcard now answers*without credentials, and only listed origins get credentials (withVary: Origin). GET /described every table and routine to anonymous callers. The document now follows the caller's privileges (openapi-mode, defaultfollow-privileges).- Permission errors named the database login ("Effective user is ..."), and unexpected errors returned their internal text; both are now generic for the client and logged on the server.
- Each case spelling of a role name (
web_user,WEB_USER...) opened its own connection pool; names are folded to upper case, as Firebird does. - The HTTP server had no request timeout and read request bodies inside the transaction:
server-request-timeout(60 s),server-max-body-size(16 MB) and header limits are enforced, and bodies are read before the transaction starts. - An empty boolean in the config file (
admin-pprof =) counted as true; it now keeps the default. admin-server-hostbinds the admin server to its own address, e.g.127.0.0.1.
Added¶
- Metrics in the Prometheus format at
GET /metricson the admin port: requests by method and status, a request duration histogram, error codes, connection pool and statement cache usage per role, schema cache loads, JWT cache and JWKS fetches, and Go runtime figures. See Monitoring. server-timing-enabledadds aServer-Timingheader (jwt,parse,transaction,response), as in PostgREST.jwt-secretaccepts a JWK or a JWKS as JSON, PEM certificates and PKCS#1 RSA keys, several PEM keys at once, Ed25519 keys (EdDSA) and@/path/to/file;PS256/384/512tokens are accepted with RSA keys. Tokens with akidare checked against the matching key.jwt-secret-is-base64for base64 encoded HMAC secrets.jwt-jwks-urlfetches the signing keys of an identity provider (Auth0, Keycloak, Okta, Azure AD, ...) at startup and everyjwt-jwks-refreshseconds, and again when a token names an unknownkid, so rotated keys work right away.-
A JWT validation cache (
jwt-cache-max-entries, default 1000) skips the signature check for tokens already accepted, until they expire or for 5 minutes. -
Single values as is: any concrete
Accepttype that isn't a built-in format (text/plain,application/octet-stream,image/png,application/geo+json...) returns the one selected value of the one matching row as the body, with thatContent-Type. Binary columns are sent as their bytes, so files can be served straight from the database. Works for reads, writes withreturn=representationand RPC. - Raw request bodies for RPC: a
POST /rpc/<name>whoseContent-Typeisn't JSON or a form puts the body into the routine's only parameter, as text or bytes (FBRST133otherwise). - The example database has a
coverstable with a binary BLOB and ablob_sizefunction. - A raw single-value response sends a binary value as the driver returns it, without a base64 encode/decode round trip, so serving a file holds one copy of it in memory.
Fixed¶
-
Acceptentries with a parameter that has no value, such as theapplication/vnd.pgrst.plan+text; for="application/json"; options=;that postgrest-js sends forexplain(), were rejected with406. The media type is now read even when a parameter is malformed. -
Responses are sent only after the transaction commits. A failed
COMMITnow returns an error instead of a success response, and headers set before an error (such asContent-Rangeon a416) no longer appear on the error response. - Graceful shutdown waits for requests in flight (up to 10 seconds) before closing the database
connections; before, requests running at
SIGTERMcould fail with500.
0.2.0 - 2026-10-20¶
SQL schemas for Firebird 6, chosen per request with Accept-Profile and Content-Profile.
Schema support is experimental: it is tested against the Firebird 6 snapshot and may change
if Firebird 6 changes before its release. Existing setups keep working as before: Firebird 3-5
are unaffected, and Firebird 6 without db-schemas serves PUBLIC as it did.
Added¶
- SQL schemas (Firebird 6, experimental):
db-schemaslists the schemas to expose, the first being the default (publicwhen unset). Requests pick one withAccept-Profile(reads) orContent-Profile(writes), as in PostgREST; responses carryContent-Profile, and unexposed schemas get406(FBRST106). Each schema has its own cache and OpenAPI description, statements use schema-qualified names, and embedding follows foreign keys within the schema.examples/bookstore-fb6.sqladds aninventoryschema.db-pre-requestacceptsschema.proc.
Fixed¶
- On Firebird 6 the schema cache matches system-table rows by schema as well as by name (domains, constraints, indexes, routine parameters), instead of relying on names being unique.
0.1.2 - 2026-10-19¶
Write guards with max-affected, query plans, and per-request time zones.
Added¶
Prefer: handling=strictrefuses unknown or invalid preferences (FBRST130); the defaultlenientignores them, as before.Prefer: max-affected=n(withhandling=strict, as in PostgREST): aPATCHorDELETEthat would change more thannrows fails withFBRST131and is rolled back.- Query plans: with
db-plan-enabled = true,Accept: application/vnd.pgrst.plan(+textor+json) returns Firebird's explained plan of every statement aGET/HEADrequest runs, including embeds and counts, instead of the data. Prefer: timezone=<IANA name>(Firebird 4+):TIME/TIMESTAMP WITH TIME ZONEvalues are returned in that zone, and the Firebird session runs in it (SET TIME ZONE, reset before the connection is reused). Unknown zones are ignored, or refused withhandling=strict.examples/bookstore-fb4.sqladds ameetingstable with time zone columns for Firebird 4+.- The release workflow can also publish the Docker image to Docker Hub, a Homebrew cask and a
Scoop manifest, each once its secret is configured (see
RELEASING.md).
Changed¶
- Release notes are taken from the version's section of
CHANGELOG.md, also for a release first drafted on GitHub.RELEASING.mddescribes the release steps.
0.1.1 - 2026-10-19¶
Ordering by embedded columns, limited PATCH/DELETE, bulk RPC calls, estimated counts, and
Firebird 6 support (tested against the 6.0 snapshot).
Added¶
- Ordering by a column of a to-one embed, as in PostgREST:
order=authors(name).desc. It works at any level (books.order=authors(name)), by alias and on spread embeds. To-many embeds are refused withFBRST126. PATCHandDELETEwithlimitandoffset, as in PostgREST:?order=created_at&limit=10changes only the first 10 matching rows. A limit needs anorder(FBRST127), and the primary key is added to it as a tie-breaker.db-safe-updatesaccepts a limit instead of a filter.Prefer: params=multiple-objectson/rpc: an array body calls the routine once per object in one transaction. Selectable procedures return the combined rows as one result (oneUNION ALLquery, soselect, filters,orderandlimitapply to all of them; at most 200 objects), executable procedures and functions an array of results.Prefer: params=single-objecton/rpc: the raw JSON body is passed to a routine's only text parameter. The example database haslog_requestfor this.Prefer: count=plannedandcount=estimated.plannedestimates a table's rows from the statistics of its unique indexes without reading the table;estimatedcounts exactly up todb-max-rows(or 1000) and uses the estimate above. Where no estimate applies (filters, views, procedures, no statistics) the count is exact andPreference-Appliedsayscount=exact. Unknowncount=values are ignored instead of being treated asexact. The example database computes its index statistics after loading data.- CI runs the integration suite against the Firebird 6 snapshot (
firebirdsql/firebird:6-snapshot) on every push and weekly. The job may fail without failing the build until Firebird 6 is released.
Fixed¶
- A
PATCHwith?limit=andreturn=representationupdated every matching row but returned onlylimitof them; the limit now applies to the update itself. - Schema loading reports errors that happen while reading the system tables, instead of silently loading an incomplete schema (on Firebird 6 this hid every foreign key).
- Foreign keys and unique constraints load on Firebird 6.
- On Firebird 6, the
hintof a PSQL exception names the exception without the"PUBLIC".schema prefix, as on earlier versions.
0.1.0 - 2026-10-18¶
First release. Supports Firebird 3, 4 and 5.
Reading¶
- Tables and views at
/<name>, with PostgREST's URL grammar:select(aliases, casts), filters (eq,neq,gt,gte,lt,lte,like,ilike,in,is,isdistinct,not.), plus Firebird'scontaining,startingandsimilar; nestedor/andtrees;order;limit/offsetand theRangeheader;Prefer: count=exact. - Resource embedding through foreign keys: many-to-one, one-to-many, many-to-many through junction
tables, nested embeds,
!hintdisambiguation,!inner,embed=is.null, and per-embed filters, order and limits. - Spread embeds (
...rel(cols)): to-one values are merged into the parent; to-many columns become arrays. - Aggregates
count(),sum(),avg(),min(),max()with automatic grouping, inside embeds too (authors?select=name,books(count)). - Computed fields (PSQL functions) and computed relationships (selectable and executable
procedures) declared with
TYPE OF COLUMNparameters. - JSON, CSV and single-object (
application/vnd.pgrst.object+json) responses.
Writing¶
POST(single or bulk), upserts withPrefer: resolution=merge-duplicates|ignore-duplicatesandon_conflict,PATCH,PUT,DELETE,Prefer: return=representationwithselectand embeds,?columns=,Prefer: missing=default,Prefer: tx=rollback.PATCH/DELETEwithout filters are refused unlessdb-safe-updates = false.
Routines¶
/rpc/<name>for selectable procedures (filterable like tables), executable procedures and PSQL functions, plus public routines of packages at/rpc/<package>.<name>.GETrequests run in read-only transactions.
Performance¶
- Prepared statement cache per role (
db-prepared-statements, default 256), with idle expiry (db-prepared-statements-idle, default 30 s) soDROPstatements are not blocked. - The
request.*context variables are only set when the database reads them (db-request-context = auto|always|never), in a singleEXECUTE BLOCKtogether withdb-pre-request. - Faster JSON encoding of result rows; bulk inserts reuse one prepared statement.
- See
docs/benchmarks.mdfor numbers and the load tool.
Security and operations¶
- JWT (HS256/384/512 secret, or RS/ES public key) mapped to Firebird roles; anonymous role; requests are refused if the role isn't granted to the database user.
- JWT claims, role, method, path and selected headers exposed to PSQL through
RDB$GET_CONTEXT('USER_TRANSACTION', ...); optionaldb-pre-requestprocedure. - OpenAPI 3 document at
/. - Schema cache reload with
POST_EVENT 'fbrest_reload',SIGUSR1orPOST /reloadon the admin server, which also serves/liveand/ready. - Config file or
FBREST_*environment variables; CORS; Docker image and docker-compose example. - Documentation site at https://mariuz.github.io/firebirdrest/ (built from
docs/). bench/load tool with a data seeder.admin-pprofserves Go profiling endpoints on the admin port.- Release workflow can be started from GitHub (Actions, then Release, then Run workflow).