Skip to content

Chapter 35: Release Notes

v3.12.10 (2026-05-14)

Version-alignment release. PHP ran ahead through three independent patch releases (3.12.7–3.12.9) while Python / Ruby / Node stayed at 3.12.6. This release realigns all four frameworks on 3.12.10 and ships the ORM save() fix.

PHP — ORM->save() no longer swallows write failures (#114)

ORM->save() called update()/insert() but ignored their bool return — it only caught exceptions. The PHP adapter's exec() returns false on a bad statement instead of throwing, so a failed UPDATE (commonly: one referencing a public model property with no matching DB column, since getDbData() includes every public property) slipped through. The empty transaction got committed and save() returned $this — the documented success signal. Callers relying on the save(): static|false contract believed the row persisted when nothing changed. Silent data loss — no exception, no log.

Fix. save() now captures the bool return of update()/insert(), rolls back, and returns false on a falsy result.

php
$ok = $this->_exists || ... ? $this->update() : $this->insert();
if ($ok === false) { $this->_db->rollback(); return false; }
$this->_db->commit();

Cross-framework parity check. Python, Ruby and Node don't have this exact failure mode — they build the write payload from declared fields only (not all public properties), and their DB adapters raise on bad SQL, which the existing try/except already catches. PHP was the outlier on both counts. 3 regression tests in tests/Issue114Test.php; PHP suite 2235 → 2238 passing.

Also in the PHP 3.12.7–3.12.9 patch line

These shipped to PHP between 3.12.6 and this release; folded into the consolidated 3.12.10 line:

  • 3.12.7Request now normalises caller-provided header keys to lowercase. Some upstream entry points (Apache+PHP-FPM custom mappings, certain proxies, hand-written test fixtures) hand headers in with original case. The constructor only looks them up by lowercase key, so without normalisation multipart/form-data content-type detection silently missed and the body fell through as raw bytes — a follow-up to the #135 fix.
  • 3.12.8 / 3.12.9 — Router gained RFC 9110 HTTP method conformance: proper HEAD and OPTIONS handling, 405 Method Not Allowed with an Allow header listing the methods a route does support.

Python / Ruby / Node

Version-only bump 3.12.6 → 3.12.10 to realign with PHP. No behavioural changes in these three since 3.12.6.

Upgrade

Drop in. No .env changes, no API changes. PHP users on 3.12.9 get the save() fix; everyone else gets a version-number realignment.

v3.12.6 (2026-05-06)

Python-only fix release. PHP / Ruby / Node ship the same version stamp for parity but carry no behavioural changes.

Python — psycopg2 % substitution no longer trips PL/pgSQL function bodies (#40)

A migration containing a PL/pgSQL function with literal % characters in a RAISE EXCEPTION (or format()) call used to fail with the misleading:

RuntimeError: Migration failed: list index out of range

The error message gave no hint that the % chars were the problem. The user-facing failure looked like a tina4 internal bug — actually psycopg2's argument-substitution system tripping on the literal percent signs.

Root cause. PostgreSQLAdapter.execute(sql, params) always called cursor.execute(sql, params or []). psycopg2 interprets % as parameter placeholders WHENEVER the params arg is supplied — even an empty list []. So a function body containing RAISE EXCEPTION 'thing % conflicts with %', a, b (perfectly valid PL/pgSQL) blew up because psycopg2 thought % was a placeholder and there were no values to substitute.

Fix. New PostgreSQLAdapter._safe_execute(cursor, sql, params) helper routes empty/None params through cursor.execute(sql) (no second arg), which makes psycopg2 skip the substitution pass entirely. Literal % chars flow through untouched. Applied at every cursor.execute(...) call site in the adapter (5 spots across execute, fetch, fetch_one).

Tests. 5 new unit tests in tests/test_postgres_percent_substitution.py pin the helper's branching. 3 live-Postgres regression tests in tests/test_postgres_plpgsql_percent.py exercise a real CREATE FUNCTION + trigger flow with literal % in the body — skipped automatically when no Postgres is reachable. Full suite: 2453 passing (was 2448).

Cross-framework parity check. PHP (pg_query vs pg_query_params) and Ruby (exec vs exec_params) already branch on params presence so they don't have this bug. Node uses $1 placeholders not %, so the same class of bug doesn't apply.

Long-standing tina4-js #37 confirmed fixed

frond.form.submit not following 3xx redirects — fixed in frond v2.1.2 back on April 11, 2026 (xhr.responseURL comparison + window.location.href navigation). All four framework public/js/frond.min.js copies carry the fix. The original issue stayed open because the reporter never confirmed against the patched build.

Upgrade

Drop in. No .env changes, no API changes.

v3.12.5 (2026-05-06)

PHP-only bug fix release. Python / Ruby / Node ship the same version stamp for parity but carry no behavioural changes.

PHP — multipart bodies with file uploads now parse correctly (#135)

Two stacked bugs in Tina4\Request::__construct made $request->body come through as the raw multipart bytes (~11 KB blobs starting with ------WebKitFormBoundary…) whenever the request included a file upload:

  1. The constructor called $this->parseBody() BEFORE initialising $this->files. Inside parseBody's multipart branch, the line $this->files = array_merge($this->files, $parsed['files']) read an uninitialised typed property — fatal Error.
  2. After fixing the init order, that same line tried to mutate the readonly $files property — another fatal Error.

Both errors got swallowed by the upstream error handler and the route handler received the raw multipart payload instead of the parsed associative array. Routes that worked fine for ordinary form posts broke the moment a file field came along.

Fix. Move $this->files initialisation AFTER parseBody() runs. parseBody stashes extracted multipart files on a new private mutable $multipartFiles; the constructor merges them into the readonly $files in a single assignment that respects the readonly contract.

4 new regression tests in tests/Issue135Test.php pin the constructor's contract. Full PHP suite: 2235 passing (was 2231).

Upgrade

Drop in. No .env changes, no API changes, no other framework changes.

v3.12.4 (2026-05-06)

Documentation-truth release. The audit-truth.py CI gate (introduced post-3.12.3) flagged 39 env vars referenced in docs that no framework actually read. This release closes that gap: 25 of them now exist in code, the other 14 are deleted from docs (11 hallucinations + 6 clustering vars deferred to tina4#2). Both audit gates (CLI drift + env-var drift) are now strict in CI.

25 new env vars across all 4 frameworks

Server: TINA4_HOST, TINA4_SUPPRESS, TINA4_ENV_FILE. Health: TINA4_HEALTH_PATH (default /__health, with /health kept as a legacy alias), TINA4_TRAILING_SLASH_REDIRECT. Sessions: TINA4_SESSION_HTTPONLY, TINA4_SESSION_NAME, TINA4_SESSION_SECURE. Templates: TINA4_TEMPLATE_CACHE_TTL (0 = permanent). GraphQL: TINA4_GRAPHQL_AUTO_SCHEMA, TINA4_GRAPHQL_ENDPOINT. Mail: TINA4_MAIL_IMAP_ENCRYPTION (tls/starttls/none). MCP: TINA4_MCP, TINA4_MCP_PORT. Swagger: TINA4_SWAGGER_ENABLED, TINA4_SWAGGER_CONTACT_EMAIL, TINA4_SWAGGER_LICENSE. Database: TINA4_DB_POOL (env override on the existing Database(url, pool=N) constructor argument).

Logging — env-driven file output + rotation

Six new vars give you full control over logging without touching code:

VarDefaultWhat it does
TINA4_LOG_FILE(empty — stdout only)Path to a log file. Empty leaves you on stdout.
TINA4_LOG_DIRlogsDirectory for log files (joined with _LOG_FILE if relative).
TINA4_LOG_FORMATtexttext or json. JSON mode emits one structured record per line.
TINA4_LOG_OUTPUTstdoutstdout, file, or both. Strict — stdout means stdout only.
TINA4_LOG_CRITICALfalseEnables a Log.critical() level above error. Off = no-op.
TINA4_LOG_ROTATE_SIZE10485760 (10 MB)Rotate when the file exceeds this many bytes. 0 disables rotation.
TINA4_LOG_ROTATE_KEEP5Number of rotated files to retain (app.log.1app.log.N). Older ones are deleted.

Implementation uses each language's stdlib — Python's logging.handlers.RotatingFileHandler, Ruby's Logger.new(path, shift_age, shift_size), and a roll-your-own atomic-rename pattern in PHP and Node. Zero new dependencies in any framework.

Documentation-truth CI gate now strict on both axes

The audit-truth.py script now blocks merges to main of tina4-documentation whenever a doc references a tina4 <command> or TINA4_* env var that doesn't exist in source. Previously CLI drift was strict; env drift was warn-only. Today both are strict.

Tests added

  • Python: +53 tests in tests/test_env_vars.py (2395 → 2448)
  • PHP: +59 tests in tests/EnvVarTest.php (2172 → 2231)
  • Ruby: +51 examples in spec/env_vars_spec.rb (2696 → 2747)
  • Node: +59 tests in test/envVars.test.ts (3204 → 3263)

Cross-framework total: 10,689 tests passing, +222 from 3.12.3.

Upgrade path

Drop in. No breaking changes — every new env var is opt-in with a sensible default. If you were setting any of the 17 deleted vars in your .env, the boot guard will warn (then ignore) — clean them out at your leisure.

v3.12.3 (2026-05-05)

Cross-framework parity sweep. Two minor breaking changes in the Ruby and PHP public API that bring all four frameworks onto the same shape.

Breaking changes (Ruby + PHP only)

Ruby Container — predicate now uses ? suffix.

ruby
# before (3.12.2 and earlier)
Tina4::Container.has(:mailer)        # outdated

# after (3.12.3)
Tina4::Container.has?(:mailer)       # idiomatic Ruby predicate

This brings Ruby in line with Python (has()), PHP (has()), and Node (has()) while still respecting Ruby's ?-suffix idiom for predicates returning bool. The pre-existing resolveget rename happened earlier; only the predicate was lagging.

ResponseCache public surface — middleware-only across all four frameworks.

The cache has always been middleware. Two of the four frameworks (PHP, Ruby) historically exposed lookup/store as public methods, which let users couple to internals. The public API is now consistent across all four: use the middleware on a route, and read stats with module-level helpers.

ruby
# Ruby — module-level helpers (parity with Python)
Tina4.cache_stats   # → { hits:, misses:, size:, backend:, keys: }
Tina4.clear_cache   # flush all entries

# PHP — static methods on the class
\Tina4\Middleware\ResponseCache::cacheStats();
\Tina4\Middleware\ResponseCache::clearCache();

Internal methods that used to be public (get, lookup, store, cache_response) are now private. Tests that needed them retain access via _internal* test seams marked @internal.

Doc parity — CLAUDE.md and book chapter 33

  • CLAUDE.md: every framework's "Key Method Stubs" section now covers the same surface area Python documents — Queue, QueryBuilder, Frond, Api, Background Tasks, ResponseCache, etc. PHP added 4 sections; Ruby added 5; Node added 13.
  • Book chapter 33: env var tables are now grounded in source. Each framework's chapter 33 lists every TINA4_* var its source actually reads. Found and fixed several gaps — Ruby was missing TINA4_CACHE_*, TINA4_QUEUE_*, TINA4_KAFKA_*, TINA4_RABBITMQ_*, TINA4_MONGO_*, TINA4_WS_BACKPLANE, and the entire TINA4_SESSION_VALKEY_* block.

Other fixes

  • Ruby lib/tina4/ai.rb — subprocess output is now force-encoded to UTF-8 before String#strip, fixing Encoding::CompatibilityError that crashed 4 ai specs on systems with non-ASCII pip output.
  • Node test/serverParity.test.ts — sets TINA4_OVERRIDE_CLIENT=true so start() actually runs, plus emits the N passed, M failed summary line the runner expects. The test was effectively a no-op before; now it's recorded properly.

Genuine gaps surfaced by the parity audit (follow-up, not blocking 3.12.3)

The chapter 33 audit flagged env vars Python documents that no other framework actually reads — Ruby/PHP/Node lack TINA4_OPEN_BROWSER, TINA4_DEV_POLL_INTERVAL, TINA4_PUBLIC_DIR, TINA4_TOKEN_EXPIRES_IN alias, plus a few framework-specific gaps (Ruby has no Mongo session backend; Node TINA4_CSRF defaults to false vs Python's true). Tracked for a future patch.

Upgrade path

SymptomFix
Ruby: NoMethodError: undefined method 'has' for Tina4::ContainerReplace has(:key) with has?(:key)
PHP: BadMethodCallException calling $cache->lookup(...)Use the middleware: [ResponseCache::class, 'beforeCache'] / [..., 'afterCache']. Or call _internalLookup if you really need direct access (test code only — @internal).
Ruby: NoMethodError: undefined method 'get' for ResponseCache instanceUse Tina4.cache_stats / Tina4.clear_cache for stats. Lookup goes through the middleware.

No .env changes from 3.12.2.

v3.12.2 (2026-05-05)

Quality-of-life patch. Two related portability fixes — no breaking changes from 3.12.1.

Firebird URL auto-detect

Firebird is the awkward one in the stack. Every other engine has a server-side database name (postgres://host:port/dbname), but Firebird wants either an absolute file path on the server, a Windows drive-letter path, or an alias. The classic URI form needs a double slash to keep the leading / of an absolute path through the URL parser — unintuitive to anyone used to the way postgres / mysql / mssql encode the database name.

The framework now accepts five equivalent forms and normalises all of them transparently:

URL path you writeResolved Firebird identifier
//abs/path/db.fdb (classic double-slash)/abs/path/db.fdb
/abs/path/db.fdb (single-slash, intuitive)/abs/path/db.fdb
/C:/Data/db.fdb (Windows drive letter)C:/Data/db.fdb
/C%3A/Data/db.fdb (URL-encoded colon)C:/Data/db.fdb
/employee (Firebird alias)employee

For ops setups that keep server URL and DB location in separate config layers — or for Windows backslash paths that fight URL encoding — set TINA4_DATABASE_FIREBIRD_PATH. The env override wins over whatever path is in the URL.

bash
TINA4_DATABASE_FIREBIRD_PATH=C:\firebird\data\app.fdb
TINA4_DATABASE_URL=firebird://SYSDBA:masterkey@localhost:3050/ignored

Shipped to all 4 frameworks. 11 regression tests per framework (8 unit + 3 live).

Bug fix specific to PHP — mysqli localhost+port quirk

PHP's mysqli has a long-standing quirk where host == "localhost" triggers a Unix socket lookup and IGNORES the port argument entirely. Connecting to mysql://...:53306 against a Docker container fails with "No such file or directory" — mysqli is hunting for /tmp/mysql.sock instead of opening a TCP connection. MySQLAdapter::rewriteHostForTcp() now rewrites localhost to 127.0.0.1 when a non-zero port is specified, forcing the TCP code path. Bare mysql:///db (no port) is preserved so existing socket-based setups keep working.

Other fixes

  • chore(python): pyproject.toml had drifted to 3.10.41 while __init__.py read 3.12.1. Synced both to 3.12.2 so uv build and runtime introspection now agree.
  • chore(claude.md, all 4): stale framework version banners in CLAUDE.md headers updated.

No .env changes from 3.12.1, no migration needed. Existing 3.12.1 installs upgrade by changing one version number.

v3.12.1 (2026-05-04)

CI-only patch — no framework code changes from 3.12.0.

  • fix(ci, all 4): every publish.yml workflow now declares permissions: contents: write on the publish job. Without this, softprops/action-gh-release 403'd against the default GITHUB_TOKEN on repos whose default Workflow permissions setting was read-only (Ruby and Node hit this every release; PHP and Python worked by luck of repo settings). The explicit declaration makes the workflow self-sufficient.
  • chore(ci): bumped softprops/action-gh-release from @v1 (unmaintained) to @v2.

No .env changes, no API changes, no migration needed. Existing 3.12.0 installs can upgrade without touching anything else.

The version-bump itself is the test: a successful 3.12.1 release proves the workflow fix works on Ruby and Node where 3.12.0 needed manual gh release create.

v3.12.0 (2026-05-04)

⚠️ Breaking change — read before upgrading. Every framework env var now uses the TINA4_ prefix. Existing .env files set with DATABASE_URL, SECRET, SMTP_HOST, HOST_NAME, etc. will cause the framework to refuse to boot. Run tina4 env --migrate to rewrite, or follow the rename table below.

Why this release

Tina4's env vars had grown inconsistent. Some had the TINA4_ prefix (TINA4_DEBUG, TINA4_LOCALE, TINA4_CACHE_BACKEND), others didn't (DATABASE_URL, SECRET, SMTP_HOST). Newcomers had to guess which convention applied to which feature. Existing tools and PaaS dashboards collided with un-prefixed names like SECRET and API_KEY that other libraries also read. Documentation drifted — 91 env-var names appeared in the docs that didn't exist in any framework, and 22 framework-specific env vars in the code didn't match the names users were told to set.

This release closes all three gaps with a single hard rename. No deprecation period, no fallback chain. The framework refuses to boot if it detects a legacy name in the environment, prints a list of every var to rename, and tells you which command to run.

What changed

  • 22 env vars renamed to TINA4_* form. See the migration table below.
  • tina4 env --migrate CLI added to all four frameworks. Reads your .env, rewrites it in place, leaves a .env.bak backup, prints a diff. Idempotent.
  • Boot-time guard scans os.environ (or the language equivalent) for the 22 legacy names. If any are present, prints the rename map and exits with code 2. Bypass with TINA4_ALLOW_LEGACY_ENV=true for migration scripts that need both names set during transition.
  • All 4 framework books rewritten. Chapter 33 (Environment Variables) is now a clean canonical list — every var prefixed, descriptions current, legacy names removed.
  • Doc-vs-code drift closed. Of the 91 stale env vars previously documented, 61 were renames (corrected), 32 were never implemented (removed). The audit-links.py CI gate stays at 0 broken links / 0 broken anchors.
  • Frond bundle rebuilt at v2.1.3 — frond.min.js footer now shows the version explicitly so users can verify what they have.

Bug fixes shipped alongside the rename

  • #38 PostgreSQL UUID-PK transaction abort — the post-INSERT lastval() probe is now wrapped in a SAVEPOINT, so UUID-PK INSERTs no longer poison the outer transaction with InFailedSqlTransaction. Live regression test against PostgreSQL 16. (Affects all 4 frameworks where the PG adapter does this probe.)
  • #39 Landing page + template auto-routing
    • Auto-routing now scans src/templates/pages/ only. Partials, layouts, base.twig, errors/, components/, and _* files never auto-serve from a URL.
    • TINA4_TEMPLATE_ROUTING=off kills the feature entirely.
    • src/public/index.html auto-serves at / (and /foo/ serves src/public/foo/index.html) — SPA hosting Just Works.
    • The framework landing page only renders when TINA4_DEBUG=true. Production never shows it; framework version, dev-admin link, and gallery don't leak to real users.
    • The malformed HTTP/1.1 404 OK status line is fixed — every status code now uses its canonical RFC 7231/9110 reason phrase.
  • #37 frond.form.submit redirect handling — verified shipped at v2.1.x; xhr.responseURL change triggers window.location navigation correctly.
  • #36 Session file handler — re-verified safeguards (lazy save, WebSocket skip, probabilistic GC, new-and-empty skip) all still in place.

Migration — every renamed var

Legacy nameNew name
DATABASE_URLTINA4_DATABASE_URL
DATABASE_USERNAMETINA4_DATABASE_USERNAME
DATABASE_PASSWORDTINA4_DATABASE_PASSWORD
DB_URLTINA4_DATABASE_URL (alias dropped)
SECRETTINA4_SECRET
API_KEYTINA4_API_KEY
JWT_ALGORITHMTINA4_JWT_ALGORITHM
SMTP_HOSTTINA4_MAIL_HOST
SMTP_PORTTINA4_MAIL_PORT
SMTP_USERNAMETINA4_MAIL_USERNAME
SMTP_PASSWORDTINA4_MAIL_PASSWORD
SMTP_FROMTINA4_MAIL_FROM
SMTP_FROM_NAMETINA4_MAIL_FROM_NAME
IMAP_HOSTTINA4_MAIL_IMAP_HOST
IMAP_PORTTINA4_MAIL_IMAP_PORT
IMAP_USERTINA4_MAIL_IMAP_USERNAME
IMAP_PASSTINA4_MAIL_IMAP_PASSWORD
HOST_NAMETINA4_HOST_NAME
SWAGGER_TITLETINA4_SWAGGER_TITLE
SWAGGER_DESCRIPTIONTINA4_SWAGGER_DESCRIPTION
SWAGGER_VERSIONTINA4_SWAGGER_VERSION
ORM_PLURAL_TABLE_NAMESTINA4_ORM_PLURAL_TABLE_NAMES

Names that stay un-prefixed (not framework config)

PORT, HOST, NODE_ENV, RACK_ENV, RUBY_ENV, ENVIRONMENT — these are runtime / PaaS conventions, not framework config. Heroku, Railway, Vercel, and friends set them; we keep reading them.

How to upgrade

  1. Backup your .env: cp .env .env.bak.pre-v3.12
  2. Run the migration: tina4 env --migrate — rewrites your .env in place.
  3. Update PaaS dashboards: Heroku, Railway, Vercel, Render, Fly.io etc — rename the same vars in your provider's env-var UI.
  4. Restart your app. The boot guard verifies nothing legacy remains.

If your app uses SECRET, DATABASE_URL, or any other listed name in places besides .env (e.g. your CI pipeline's env: blocks), update those too — the boot guard checks os.environ, not just .env.

Parity

All 4 frameworks aligned at 3.12.0:

  • tina4-python 3.11.32 → 3.12.0
  • tina4-php 3.11.32 → 3.12.0
  • tina4-ruby 3.11.32 → 3.12.0
  • tina4-nodejs 3.11.32 → 3.12.0

Coordinated release across PyPI, Packagist, RubyGems, npm.

v3.11.32 (2026-04-25)

Critical fix — pool + transactions are now actually atomic. Plus a coordinated parity release that aligns all four frameworks at the same version after months of drift.

Before this release, creating a Database with pool > 0 silently broke transactions. The pool's round-robin checkout rotated to a different adapter on every call — so start_transaction() pinned its flag on adapter A, the executes autocommitted on adapters B and C, and the final commit() / rollback() landed on adapter D, which had nothing to commit. Result: rollback() was a no-op, writes leaked through, and no error or log surfaced the problem.

The fix pins one adapter to the calling context for the lifetime of a transaction. Each language uses its own primitive:

  • Pythonthreading.local() on the Database instance
  • RubyThread.current[:tina4_pinned_adapter_<obj_id>]
  • Node.jsAsyncLocalStorage from node:async_hooks (async-safe across overlapping awaits)
  • PHP — per-instance property (PHP-FPM is one process per request; threading.local is unnecessary)

While pinned, every database call routes to the same adapter. commit() and rollback() release the pin so subsequent calls round-robin again.

  • fix (database / all 4): adapter pinning across transaction scope in Database._get_adapter() (and language equivalents). Every backend is affected — SQLite, PostgreSQL, MySQL, MSSQL, Firebird. Firebird exposed it loudest because of its honest "commit-empty-txn is a real no-op" semantics; the others mostly hid the bug behind eager autocommits but still lost rollback atomicity.
  • tests (all 4): new regression suite — three INSERTs followed by rollback() under pool=4 now leaves zero rows (was leaking three). Three INSERTs followed by commit() persists exactly three. Pin-release after commit/rollback verified. pool=0 regression test added so single-connection mode stays unaffected.
  • parity / version alignment: all 4 frameworks bumped to 3.11.32 — closes the cross-framework version drift that had built up (PHP at 3.11.31, Python at 3.11.24, Ruby and Node at 3.11.19). A single coordinated release across all four registries: PyPI, Packagist, RubyGems, npm.

No migration needed. Code using pool=0 (the default for every adapter except where explicitly raised) is unaffected. Code using pool>0 will now actually honour transactions instead of silently dropping them.

If you've been seeing intermittent "writes vanished" or "rollback didn't help" reports on a pooled Database, this release is the cause and the cure.

v3.11.13 (2026-04-16)

Issue-driven release. Everything reported in the open tina4-book issues either was fixed in this version or is already fixed in 3.11.12; this release consolidates the remaining bits and corrects documentation drift.

  • feat (router / all 4): Explicit typed-parameter system shared across Python, PHP, Ruby, Node. Adds alpha, alnum, slug, uuid, and explicit string types in addition to the existing int/integer, float/number, path/.*. Unknown type names now throw at registration{name:str}, {id:inetger}, etc. raise with a clear message listing the valid types instead of silently falling through to the default matcher. Fixes tina4-book#125. +45 new tests across the four suites.
  • fix (gallery / python+php+ruby): Gallery Try-It / View buttons now open the deployed example in a new tab (window.open(url, '_blank')) instead of navigating away from the gallery home. Fixes tina4-book#115.
  • fix (ruby gemspec): sqlite3 promoted from add_development_dependency to add_dependency. Matches the "zero-config SQLite on first run" promise. Fixes tina4-book#100.
  • docs (tina4-book): PHP Chapter 2 updated — correct port (7145), ->noAuth() on write-method examples, and an explicit callout explaining the secure-by-default policy for POST/PUT/PATCH/DELETE. Addresses tina4-book#87, #94, #123.
  • docs (tina4-book): Python @template decorator ordering corrected (must sit BELOW the route decorator) in book chapters 04 and 10; Python request->query vs request->params distinction in PHP chapter 1.
  • tests (python): Session-handler tests updated to reflect the real default TTL of 3600s (were stale at 1800s).
  • verified already fixed in earlier 3.11.x releases — closed comments posted on all of these:
  • #79 dotted numeric index ({{ items.0.name }})
  • #80 truncate filter
  • #82 {{ parent() }} / {{ super() }} across all 4 frameworks
  • #83 Ruby dashboard — WEBrick is runtime dep
  • #89 load_dotenv rename, DatabaseResult methods, SQLite WAL locking
  • #91 Ruby request.params symbol + string keys via IndifferentHash
  • #93 Ruby /docs/* and bare /* wildcard routes
  • #97 Frond ternary operator
  • parity: All 4 frameworks bumped to 3.11.13.

v3.11.12 (2026-04-16)

Breaking: sqlite:///X URLs are now relative to the project root (cwd), matching the documented convention. For absolute paths use four slashes (sqlite:////abs/path.db) or a Windows drive letter (sqlite:///C:/Users/app.db).

Before this release, TINA4_DATABASE_URL=sqlite:///data/app.db was interpreted differently by every framework. Python/Node/Ruby tried to open /data/app.db (absolute) which crashed on macOS with OSError: [Errno 30] Read-only file system: '/data'. PHP did the same under the hood. All four frameworks now agree: three slashes = relative, four slashes = absolute.

  • fix (all 4): sqlite:///X resolves under cwd; parent directory auto-created only when inside cwd. Absolute paths are trusted and never mkdir'd at root.
  • fix (python): _ensure_folders no longer creates a bogus src/migrations/ directory. The migration runner always looks at migrations/ at the project root — there is only one correct location.
  • parity (php, ruby, node): Same sqlite:///X parsing as Python. Dedicated resolve_path / resolveSqlitePath helpers in each framework so adapters consistently handle :memory:, ./ forms, Windows drive letters.
  • tests: 9 new Python tests in TestSQLiteConnectionPath + TestProjectFolders. 4 new PHP tests in DatabaseUrlTest covering relative/absolute/Windows/bruce-regression. 6 new Ruby specs in database_drivers_spec.rb :: SqliteDriver.resolve_path. Node URL tests expanded in database.test.ts with the full relative/absolute/Windows/:memory: matrix.
  • parity: All 4 frameworks bumped to 3.11.12.

Migration note: If your .env has TINA4_DATABASE_URL=sqlite:///data/app.db, it will now create ./data/app.db in the project root (which is what most users actually want). If you genuinely want an absolute path, change to sqlite:////data/app.db (four slashes).

v3.11.11 (2026-04-16)

  • fix (python ORM): Field.validate no longer re-coerces values that are already the correct type. Previously, any PostgreSQL/MSSQL read of a row containing a DateTimeField crashed because datetime(datetime_instance) raises TypeError. The fix accepts native driver types (datetime, bytes, int, bool, float, str) without re-wrapping, and parses ISO-8601 strings into datetime for SQLite. See tina4-python/plan/orm-field-validate-native-types.md.
  • fix (python ORM): BooleanField vs IntegerField ordering handled explicitly. BooleanField(1) still coerces to True, IntegerField(True) still coerces to 1; no regression for either direction (bool is a subclass of int in Python).
  • tests (python): 10 new TestFieldsNativeTypes cases covering datetime/int/bool/float/bytes/string/ForeignKey round-trips.
  • tests (parity): Regression-guard "datetime round-trip on read path" tests added to PHP (ORMV3Test), Ruby (orm_spec) and Node.js (orm.test.ts) so an equivalent bug can't creep in there later.
  • parity: All 4 frameworks bumped to 3.11.11.

v3.11.10 (2026-04-15)

  • fix (php): Hot-reload loop — DevAdmin's polling fallback used mt=0 as the baseline, so the first poll after every page load triggered location.reload(), which reset mt=0 again. Loop now initialises the baseline on the first poll.
  • fix (php): Reload sentinel removed — PHP was the only framework recursively walking src/ and touching src/.reload_sentinel on every reload POST. The sentinel lived inside the Rust CLI's watched tree and fed back into the watcher, triggering a second loop. Replaced with the same in-memory counter used by Python/Ruby/Node.
  • fix (php): Polling no longer starts more than once when the WebSocket reconnect retry budget is exhausted (added a pollStarted guard).
  • feat (parity): GET /__dev/api/queue/topics and GET /__dev/api/queue/dead-letters added to PHP, Ruby and Node (previously only in Python). PHP queue endpoints now read from the real Tina4\Queue backend instead of returning stubs.
  • feat (devadmin): Refreshed tina4-dev-admin.js bundle (87.8 KB) across all 4 frameworks — adds the topic selector dropdown, inline payload expand/copy, and corrected version display.
  • tests: 4-way parity tests for hot-reload: mtime starts at 0, POST /__dev/api/reload bumps the counter, no sentinel file is written to disk, mtime is monotonic across successive reloads. Mirrored in tina4-php/tests/DevAdminTest.php, tina4-python/tests/test_dev_admin.py, tina4-ruby/spec/dev_admin_spec.rb, tina4-nodejs/test/devAdmin.test.ts.
  • parity: All 4 frameworks bumped to 3.11.10.

v3.11.9 (2026-04-15)

Catch-up release covering v3.11.0 → v3.11.9 across all 4 frameworks.

  • feat (websocket): Full WebSocket parity across Python/PHP/Node/Ruby — get_client_rooms() / getClientRooms(), route() usable as decorator or direct handler registration, matching room/broadcast semantics, plus new parity tests on all 4.
  • feat (graphql): Input validation and field-level @auth directives with context threading.
  • feat (graphql): Auto-discovery of schemas; removed legacy DevAdmin HTML/JS in favour of the new UI.
  • feat (devadmin — Python): Queue tab with topic selector, dead-letter listing and replay endpoints, inline payload expand/copy, version display.
  • feat (cli): Rust CLI now owns file watching — frameworks receive POST /__dev/api/reload and internal watchers are disabled when launched by the Rust CLI (--managed).
  • fix (cli): parseFlags / parse_flags / parseCliArgs no longer swallow host:port or positional args after boolean flags.
  • fix (scss): SCSS recompilation loop fixed; output path corrected to src/public/css/ to match CLI and static serving.
  • fix (frond — Python): Numeric dotted index for lists (items.0.name) now resolves correctly.
  • fix (router — Ruby): Bare /* wildcard capture exposed under "*" key for parity.
  • fix (orm — PHP): Three data-sync bugs fixed: load() double-fill, getPrimaryKeyValue, save() ID sync.
  • fix (graphql): from_orm / fromOrm list resolver used select(skip=) instead of all(offset=).
  • fix (metrics): Windows backslash paths normalised to forward slashes.
  • fix (app — PHP): No longer crashes on notices/deprecations in loaded files; run() now prints the banner when starting the server directly.
  • chore: Example demo store ships with the repo; Windows-friendly setup; .env.example and setup scripts added.
  • parity: All 4 frameworks bumped to 3.11.9. PHP aligned to the 3.x tag scheme on v3.

v3.10.99 (2026-04-12)

  • breaking: auto_map now defaults to true — ORM models automatically map between camelCase properties and snake_case DB columns. Set self.auto_map = false on your model class to restore the old behaviour.
  • feat: to_h(case:) parameter — pass case: 'camel' to get camelCase keys (for JSON APIs) or case: 'snake' (default) for snake_case keys matching DB columns. All aliases (to_dict, to_hash, to_assoc, to_object) support the parameter.
  • feat: Frond replace filter now accepts Hash args — {{ v|replace({"T": " ", "-": "/"}) }} for multiple substitutions in one call.
  • tests: 6 new parity tests covering to_h(case:), auto_map default, replace filter (Hash + positional), and ServiceRunner registration. 2,519 tests passing.
  • parity: All features shipped identically across Python, PHP, Ruby, Node.js.

v3.10.97 (2026-04-11)

  • fix: frond.form.submit redirect handling — XHR follows 3xx redirects transparently; fixed by detecting xhr.responseURL mismatch and navigating instead.
  • dep: Updated frond.min.js to v2.1.2.
  • parity: All 4 frameworks bumped to 3.10.97.

v3.10.93 (2026-04-11)

  • fix: Frond bracket depth tracking in find_outside_quotes — expressions like arr[i % 2] no longer treated as top-level arithmetic.
  • fix: Frond subscript expression evaluation — bracket content uses eval_expr() instead of simple variable lookup, enabling arr[loop.index0 % 2].
  • fix: Frond slice with variable bounds — items[start:end] evaluates bounds through eval_expr().
  • docs: Developer skills updated — Metrics Dashboard guidance, Frond Template Parity rules, @noauth security warnings.
  • parity: All Frond fixes applied identically across Python, PHP, Ruby, Node.js. 2,513 tests passing.

v3.10.92 (2026-04-10)

  • breaking: Rename ErrorOverlay methods — renderrender_error_overlay, render_productionrender_production_error, debug_mode?is_debug_mode.
  • feat: Add Server.handle(env) for cross-framework parity.
  • breaking: Rename WebSocketBackplane.createWebSocketBackplane.create_backplane.
  • feat: Add ScssCompiler.compile, add_import_path, set_variable methods.
  • feat: Add DevAdmin.register method.
  • parity: 44/44 cross-framework features green. 2,487 tests passing.

v3.10.91 (2026-04-10)

  • feat: Add parity methods — Response.send params, Middleware.check/is_preflight, AI/Log aliases, MCP optional router.
  • breaking: Rename from()from_table(), error_envelopeerror_response, remove aliases.

v3.10.90 (2026-04-09)

  • docs: Chapter 4 (Templates) — new "Dumping Values for Debugging" section covering both {{ x|dump }} and {{ dump(x) }} forms, their shared <pre>value.inspect</pre> output, and the TINA4_DEBUG=true production gate. Filter table entry updated to reference the new section.
  • docs: plan/parity/parity-template.md updated with a cross-framework dump helper comparison table and marks dump parity as confirmed across all 4 frameworks at v3.10.89.
  • chore: Version sync release — brings all 4 frameworks to the same patch version (3.10.90) so downstream users can upgrade PHP/Python/Ruby/Node.js in lockstep without hunting version mismatches.

v3.10.89 (2026-04-09)

  • feat: {{ dump(value) }} global function form added to Frond alongside the existing {{ value|dump }} filter. Both call a single Tina4::Frond.render_dump helper and produce identical output (<pre>value.inspect</pre> HTML-escaped).
  • security: Dump is now gated on TINA4_DEBUG=true. In production (env var unset or false) both the filter and function silently return an empty SafeString. This prevents accidental leaks of internal state, object shapes, and sensitive values into rendered HTML when a developer leaves a {{ dump(x) }} call in a template.
  • test: 3 new spec/frond_spec.rb examples covering debug-mode output, production silencing, function/filter parity, and function-form production silencing.

v3.10.86 (2026-04-09)

  • feat: foreign_key_field DSL auto-wires both sides of a foreign key relationship. Declaring foreign_key_field :user_id, references: User registers the integer column, calls belongs_to :user on the declaring class, and calls has_many :posts on the referenced class. Supports related_name: for custom has-many names and deferred wiring via a module-level registry so the referenced class can be defined either before or after the declaring one.
  • feat: Cross-framework parity — same FK auto-wiring semantics now available in Python (ForeignKeyField), PHP ($foreignKeys), and Node.js (type: "foreignKey")
  • docs: Chapter 6 (ORM) updated with a new "foreign_key_field — Auto-Wired Relationships" section

v3.10.85 (2026-04-09)

  • Version bump for parity with Python and PHP releases

v3.10.84 (2026-04-09)

  • fix: Router/middleware was setting request.user / request.auth / auth payload to true (boolean) instead of the actual JWT payload after valid_token? was changed to return bool — any code reading request.user["sub"] etc. would have failed silently or crashed
  • fix: CSRF middleware was not correctly rejecting invalid tokens (nil check on bool result always passed)
  • add: Headless routing auth payload integration tests to prevent regression

v3.10.83 (2026-04-08)

  • feat: WebSocket rooms — join_room, leave_room, broadcast_to_room, room_count, get_room_connections
  • feat: Queue signature parity — instance-scoped push/pop/retry, no topic params on public methods
  • feat: Auth cleanup — canonical getToken/validToken methods
  • Full parity across Python, PHP, Ruby, Node.js

v3.10.70 (2026-04-06)

  • New: SSE (Server-Sent Events) support via response.stream() — pass a generator, framework handles chunked transfer encoding, keep-alive, and text/event-stream content type
  • New: Chapter 24 added to documentation: Server-Sent Events
  • Feature count: 45 (was 44)
  • Full parity across Python, PHP, Ruby, Node.js

Tina4 Ruby follows semantic versioning. The major version (3) marks the initial Ruby launch — Tina4 Ruby is new in the v3 line, alongside Tina4 for Node.js. Minor versions (3.1, 3.2, etc.) introduce features and non-breaking API additions. Patch versions carry bug fixes and small improvements.

This chapter covers every v3 release from the initial launch through the current stable line. Each section groups releases by minor version, highlights the changes that affect your code, and shows migration steps for anything that breaks.


v3.10.68 (2026-04-03) — Full Parity Release

  • 100% API parity across Python, PHP, Ruby, Node.js — 30+ issues fixed
  • ORM: save() returns self/false, arrays not tuples, toDict/toAssoc, scope registers method, where()/all() on Node, count() on PHP
  • Auth: expires_in minutes, PBKDF2 260k, env TINA4_SECRET fallback, API key fallback
  • Session: dual-mode flash(), get_flash, cookieHeader, getSessionId
  • Database: execute() bool/DatabaseResult, get_last_id/get_error, getColumns, cacheStats
  • Request/Response: files dict, query, cookies, contentType, xml(), callable
  • Queue: consume() poll_interval
  • WebSocket: event naming, connection properties
  • GraphQL: schema_sdl() + introspect() on all 4
  • Events: emitAsync() on all 4
  • i18n: zero-dep YAML support

v3.10.67 (2026-04-03)

  • load() is now an instance methodmodel.load(sql, params) calls select_one internally, populates the instance, returns true/false. Use find(id) for PK lookups
  • api.upload() added to tina4-js — sends FormData with Bearer token auth for multipart file uploads
  • ORM CLAUDE.md rewrite — all method stubs now match actual API signatures
  • File upload docsrequest.files format documented in CLAUDE.md

v3.10.66 (2026-04-03)

  • Metrics file detail fix — clicking bubbles in framework scanning mode now resolves paths correctly via scan root tracking

v3.10.65 (2026-04-03)

  • Metrics 3-stage test detection — filename, path, and content matching
  • Metrics framework mode — scans framework source with correct relative paths
  • tina4 console — interactive REPL with framework loaded
  • tina4 env — interactive environment configuration
  • Brand — "TINA4 — The Intelligent Native Application 4ramework"
  • Quick references — 36 sections, DotEnv API documented
  • 37 chapters — 7 new (Events, Localization, Logging, API Client, WSDL/SOAP, DI Container, Service Runner)
  • MongoDB + ODBC adapters across all 4 frameworks
  • Pagination standardized — limit/offset primary, merged dual-key response
  • Port kill-and-take-over on startup

v3.10.60 (2026-04-03)

  • tina4 console — already existed, now matches Python/PHP/Node API
  • tina4 env — interactive environment configuration
  • Brand update — "TINA4 — The Intelligent Native Application 4ramework"
  • Imperative relationships — query_has_one/many/belongs_to for ad-hoc queries
  • Port kill-and-take-over — default port always reclaimed
  • MongoDB adapter (mongo gem), ODBC adapter (ruby-odbc gem)
  • Pagination standardized — limit/offset primary, merged dual-key response
  • CORS fix — returns empty string when origin not allowed

v3.10.57 (2026-04-02)

  • MongoDB adapterDatabase.new("mongodb://host:port/db"), requires gem install mongo
  • ODBC adapterDatabase.new("odbc:///DSN=MyDSN"), requires gem install ruby-odbc
  • Imperative relationshipsquery_has_one/query_has_many/query_belongs_to
  • Pagination standardized — limit/offset primary, merged dual-key to_paginate response
  • Test port at +1000 — user testing port (e.g. 8147) stable, no hot-reload
  • CORS fix — returns empty string when origin not allowed
  • ORM TINA4_DATABASE_URL discovery — auto-connect from env
  • 108 features at 100% parity, 2,333 tests

v3.10.54 (2026-04-02)

  • Auto AI dev port — second WEBrick on port+1 with no-reload when TINA4_DEBUG=true
  • TINA4_NO_RELOAD env var + --no-reload CLI flag
  • CORS fix — returns empty string when origin not allowed (not *)
  • ORM TINA4_DATABASE_URL discovery — auto-connect from env
  • QueryBuilder docs — added to ORM chapter

v3.10.48 — April 2, 2026

Bug Fixes

Puma requires --production flag — Puma no longer auto-selected when TINA4_DEBUG=false. Use tina4ruby serve --production to enable Puma. Added FakeData (46), Gallery (16), and DevReload (37) tests.


v3.10.46 — April 1, 2026

Test Coverage

344 new tests added across cache (56), ORM (19), Frond (28), database drivers (85), auth (21), SCSS (10), dotenv (30), queue backends (10), migration (10), session handlers (11), router (14), log (13), CSRF middleware (17). Fixed session handler DB key bug (symbol vs string). Ruby now at 2,274 tests with full parity across all 49 core areas.


v3.10.45 — April 1, 2026

Notes

Version bump for parity with PHP CLI serve fix. No Ruby-specific changes.


v3.10.44 — April 1, 2026

New Features

Database tab redesign — Split-screen layout with tables navigation on the left and query editor + results on the right. Click-to-select table highlighting.

Copy CSV / Copy JSON — Copy query results to clipboard in CSV or JSON format.

Paste data — Modal for pasting JSON arrays or CSV/tab-separated data. Auto-generates INSERT statements targeting the selected table, or prompts for a new table name with CREATE TABLE generation. SQL input passes through unchanged.

Multi-statement execution — Query runner handles batched SQL statements in a transaction.

Database badge on load — Table count shows immediately without clicking the Database tab.

Star wiggle animation — Empty star (☆) on the landing page with delayed wiggle animation at random intervals.

Bug Fixes

Default port — Ruby default port set to 7147 (PHP=7145, Python=7146, Ruby=7147, Node=7148).

SQLite LIMIT fix — Prevents double-LIMIT errors in the database browser.

browseTable quote escaping — Fixed table name click handlers.

ORM table name pluralization — Fixed default table name resolution. Table names are now pluralized by default (adding "s" suffix), only skipping when TINA4_ORM_PLURAL_TABLE_NAMES is explicitly set to false.

QueryBuilder closed-connection detectionensure_db! now checks if the resolved database connection is still open, raising a proper error instead of crashing with ArgumentError: prepare called on a closed database.

Metrics directory validationquick_metrics and full_analysis now check directory existence before _resolve_root fallback, so missing-directory errors are raised correctly.

Test Coverage

88 new tests added (DevMailbox 40, Static files 18, CLI scaffolding 30), plus 13 v3.10.44 feature specs and 60 pre-existing ORM/metrics bug fixes. 1,913 tests passing, 0 failures.


v3.10.40 — April 1, 2026

Bug Fixes

Dev overlay version check — Fixed misleading "You are up to date" message when running a version ahead of what's published on RubyGems. The overlay now shows a purple "ahead of RubyGems" message. Also added a breaking changes warning (red banner with changelog link) when a major or minor version update is available.


v3.10.39 — April 1, 2026

New Features

Container.singleton(name, &block) — Register a memoized factory. The block is called once on first resolve() and the same instance is returned on all subsequent calls. register() with a block is now always transient (new instance per call), matching Python's behavior.

ruby
Tina4::Container.singleton(:db) { Tina4::Database.new(ENV["TINA4_DATABASE_URL"]) }
db1 = Tina4::Container.resolve(:db)  # creates instance
db2 = Tina4::Container.resolve(:db)  # same instance

Router.match(method, path) — primary route lookup (replaces find_route; consistent with Python, PHP, Node.js). Router.add(method, path, handler) — primary imperative registration (replaces add_route; all convenience methods delegate to this).

Router.get_routes and Router.list_routes — explicit listing methods (remove ambiguous routes alias).

AI installerai_spec.rb and smoke tests updated to reflect the menu-based API (installed?, install_selected, install_all, generate_context).


v3.10.38 -- April 1, 2026

Code Metrics & Bubble Chart

The dev dashboard (/__dev) now includes a Code Metrics tab with a PHPMetrics-style bubble chart visualization. Files appear as animated bubbles sized by LOC and colored by maintainability index. Click any bubble to drill down into per-function cyclomatic complexity.

The metrics engine uses Ripper (Ruby stdlib) for zero-dependency static analysis covering cyclomatic complexity, Halstead volume, maintainability index, coupling, and violation detection. File analysis is sorted worst-first. Results are cached for 60 seconds.

AI Context Installer

tina4ruby ai now presents a simple numbered menu instead of auto-detection. Select tools by number, comma-separated or all. Already-installed tools show green. Generated context includes the full skills table.

Dashboard Improvements

Full-width layout, sticky header/tabs, full-screen overlay.

Cleanup

Removed demo/ directory. Removed old plan/ spec documents, replaced with PARITY.md and TESTS.md. Central parity matrix added to tina4-book.


v3.10.x -- Previous Releases

Released: March 28 -- 30, 2026

The v3.10 line is the most active release series. It delivered Auto-CRUD, ORM transaction safety, Frond template engine hardening, and full cross-language parity with the Python, PHP, and Node.js implementations.

v3.10.29 -- Version Parity (March 30)

Version parity release. All four Tina4 frameworks now share the same version number and feature set.

v3.10.27 -- Frond Macro HTML Escaping Fix (March 30)

Bug fix: Macro output was HTML-escaped when used inside {{ }} expressions. Characters like <, >, and " rendered as &lt;, &gt;, &amp;quot; instead of raw HTML. Nested macro calls double-escaped.

ruby
# BEFORE (broken): macro output escaped

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# Template: {{ my_macro() }}

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# Rendered: &lt;div class=&quot;card&quot;&gt;...&lt;/div&gt;

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js


# AFTER (fixed): macro output treated as safe HTML

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# Template: {{ my_macro() }}

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# Rendered: <div class="card">...</div>

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

v3.10.25 -- ORM Transaction Fix (March 30)

Bug fix: ORM save and delete called commit without an active transaction on SQLite. This raised cannot commit -- no transaction is active errors.

ruby
# BEFORE (broken):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

user = User.new(name: "Alice")
user.save  # => RuntimeError: cannot commit

# AFTER (fixed): save/delete wrap operations in a transaction block

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

user = User.new(name: "Alice")
user.save  # => works on all database engines

v3.10.22 -- Unique Form Tokens (March 30)

Form tokens now include a nonce in the JWT payload. Each token is unique per form render, which prevents replay attacks.

ruby
# In your Frond template:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

<input type="hidden" name="formToken" value="{{ formTokenValue() }}">

v3.10.18 -- Frond Ternary Parser Fix (March 29)

Bug fix: The Frond template ternary/inline-if parser failed on quoted strings containing special characters.

ruby
# BEFORE (broken):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# {{ status == "active" ? "Yes" : "No" }}  =>  parse error

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js


# AFTER (fixed):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# {{ status == "active" ? "Yes" : "No" }}  =>  "Yes"

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

v3.10.16 -- Template Filters: to_json, js_escape (March 28)

Three new Frond template filters for working with data in JavaScript contexts.

ruby
# Convert a Ruby hash to JSON inside a template:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

<script>
  const data = {{ user|to_json }};
  const name = "{{ user.name|js_escape }}";
</script>

v3.10.15 -- Replace Filter Backslash Fix (March 28)

Bug fix: The |replace filter mishandled backslash characters in replacement strings.

twig
{# Before (broken) — backslash produced corrupted output #}
{{ "hello\\world"|replace("\\\\", "/") }}
{# rendered: helo/world (ate a character) #}

{# After (fixed) — backslash escaping works correctly #}
{{ "hello\\world"|replace("\\\\", "/") }}
{# renders: hello/world #}

v3.10.14 -- get_next_id() (March 28)

Pre-generate the next primary key before inserting a record. The method detects your database engine and uses the correct sequence or auto-increment mechanism.

ruby
next_id = User.get_next_id
user = User.new(id: next_id, name: "Alice")
user.save

v3.10.13 -- ORM Auto-Commit on Write (March 28)

Write operations (save, delete) now auto-commit by default. No more forgotten commit calls leaving data uncommitted.

v3.10.12 -- Session GC and NATS Backplane (March 28)

  • Session garbage collection runs on a configurable interval
  • NATS added as a WebSocket backplane option alongside Redis

v3.10.11 -- Frond Variable Key Access (March 28)

Bug fix: Accessing a hash value with a variable key (dict[variable_key]) failed in Frond templates.

ruby
# BEFORE (broken):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# {% set key = "name" %}

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# {{ user[key] }}  =>  empty

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js


# AFTER (fixed):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# {% set key = "name" %}

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# {{ user[key] }}  =>  "Alice"

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

v3.10.10 -- Firebird Migration Runner Fixes (March 28)

Firebird migrations now use generators and VARCHAR instead of AUTOINCREMENT and TEXT. The migration tracking table uses a proper Firebird sequence (GEN_TINA4_MIGRATION_ID).

v3.10.6 -- WSDL/SOAP Rewrite (March 28)

Complete rewrite of the WSDL/SOAP module. Frond templates now support dotted function names in expressions.

v3.10.5 -- Frond Quote-Aware Operator Matching (March 28)

Bug fix: Operators inside quoted strings were incorrectly parsed as expression operators. The Frond engine now respects quote boundaries.

v3.10.4 -- Auto-CRUD REST Endpoint Generator (March 28)

Generate a complete CRUD interface from a single method call. The generator creates searchable, sortable, paginated HTML tables with create/edit/delete modals, plus REST API routes for POST, PUT, and DELETE.

ruby
Tina4::Router.get("/admin/users") do |request, response|
  Tina4::CRUD.to_crud(request, model: User, fields: [:name, :email, :role])
end

v3.10.2 -- Frond Hash Method Calls (March 28)

Frond templates can now call methods on Hash and object values inside expressions.

v3.10.1 -- autoMap and Case Conversion (March 28)

  • auto_map class attribute added to ORM for cross-language API parity (no-op in Ruby since snake_case is native)
  • Tina4.snake_to_camel("my_field") returns "myField"
  • Tina4.camel_to_snake("myField") returns "my_field"

v3.10.0 -- Optimized For-Loops (March 28)

The Frond template engine rewrote its for-loop renderer. Templates with large iteration counts render faster.


v3.9.x

Released: March 26 -- 27, 2026

v3.9.0 -- QueryBuilder, Sessions, Path Injection (March 26)

Three features arrived together.

QueryBuilder. A fluent SQL builder that integrates with the ORM.

ruby
# Through the ORM:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

admins = User.query
  .where("role = ?", ["admin"])
  .order_by("name")
  .limit(10)
  .get

# Standalone:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

rows = Tina4::QueryBuilder.from("users")
  .where("active = ?", [true])
  .select("name", "email")
  .get

The builder supports where, or_where, join, left_join, group_by, having, order_by, limit, first, count, exists, and to_sql.

Path parameter injection. Route handlers receive path parameters as named arguments.

ruby
Tina4::Router.get("/users/{id:int}") do |request, response, id|
  user = User.find(id)
  response.json(user.to_hash)
end

Auto-start sessions. Every route handler has access to request.session with zero configuration. The session API includes get, set, delete, has, clear, destroy, save, regenerate, flash, get_flash, and all.

v3.9.1 -- Security Defaults (March 27)

Breaking change: POST, PUT, PATCH, and DELETE routes now require authentication by default.

ruby
# BEFORE (v3.8.x): all routes open

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

Tina4::Router.post("/api/users") do |request, response|
  # anyone could call this
end

# AFTER (v3.9.1): unauthenticated requests get 401

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

# To allow public access, add .public:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

Tina4::Router.post("/api/users").public do |request, response|
  # open to all
end

This release also added:

  • CSRF middleware with session-bound form tokens
  • Standardized environment variables for CORS headers, session TTL, token limits
  • Queue parity: push with priority/delay, size(status), message.retry

v3.9.2 -- NoSQL QueryBuilder, WebSocket Backplane (March 27)

  • QueryBuilder works with MongoDB
  • WebSocket backplane support for multi-process deployments
  • SameSite=Lax set as the default cookie policy

v3.8.x

Released: March 25 -- 26, 2026

v3.8.0 -- Base64 Filters, Template Cache (March 25)

  • base64encode and base64decode filters in Frond templates
  • Production template cache: single filesystem scan at startup, O(1) lookups after

v3.8.1 -- Security Headers Middleware (March 25)

A built-in middleware that sets X-Frame-Options, Strict-Transport-Security, Content-Security-Policy, and X-Content-Type-Options on every response.

ruby
# In your .env:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

TINA4_MAX_UPLOAD_SIZE=10485760  # 10 MB (default)

Upload size limits and input validation also landed in this release.

v3.8.2 -- Connection Pooling (March 26)

Database connections now pool. Pass pool: N to the constructor for round-robin, mutex-protected pooling.

ruby
db = Tina4::Database.new("sqlite://data.db", pool: 5)

v3.8.3 -- Claude Code Commands (March 26)

Seventeen .claude/commands/ slash commands shipped for AI-assisted development.

v3.8.7 -- Benchmark and Stability (March 26)

  • Keyword argument fix for run!(): port:, host:, and debug: no longer crash the environment loader
  • Updated benchmarks against Roda, Sinatra, and Rails

v3.7.x

Released: March 25, 2026

v3.7.0 -- Template Auto-Serve, Firebird Migrations (March 25)

The framework serves index.html or index.twig from src/templates/ at / without a route definition. User-registered GET / routes take priority.

Firebird migrations now check RDB$RELATION_FIELDS before executing ALTER TABLE ADD. Columns that exist are skipped.

v3.7.1 -- Full Template Auto-Serve (March 25)

Any .twig or .html file in src/templates/ is now browsable by URL path. /hello serves src/templates/hello.twig. Production mode caches the lookup table at startup.


v3.6.x

Released: March 25, 2026

v3.6.0 -- Architectural Parity (March 25)

Breaking change: fetch(skip:) is replaced by fetch(offset:). No alias.

ruby
# BEFORE (v3.5.x):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

users = User.fetch(limit: 10, skip: 20)

# AFTER (v3.6.0):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

users = User.fetch(limit: 10, offset: 20)

Other changes:

  • Source directories follow the src/ prefix convention across all languages
  • TINA4_LOCALE is the only supported locale environment variable (other names removed)
  • Migration file paths standardized to src/migrations/

v3.5.x

Released: March 24, 2026

v3.5.0 -- Bundled Frontend, Swagger CRUD, Middleware (March 24)

  • tina4js.min.js (13.6 KB) ships inside the gem. The reactive frontend library loads without a CDN or npm install
  • Auto-CRUD routes now include Swagger metadata
  • Middleware standardized to before_* and after_* naming with three built-in middlewares

v3.4.x

Released: March 24, 2026

v3.4.0 -- Auth, WebSocket, DatabaseResult (March 24)

Breaking change: Auth method names changed. The old names remain as aliases.

ruby
# BEFORE (v3.3.x):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

token = auth.create_token(payload)
valid = auth.validate_token(token)

# AFTER (v3.4.0 -- preferred):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

token = auth.get_token(payload)
valid = auth.valid_token(token)

# Old names still work but are deprecated.

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

HS256 authentication. Set TINA4_SECRET in your .env and auth uses HS256. Provide RSA key files and it uses RS256. The framework picks the right algorithm.

ruby
# .env for HS256:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

TINA4_SECRET=my-secret-key

# .env for RS256:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

Bug fix: Base64url padding in HS256 tokens caused validation failures. Fixed.

WebSocket improvements:

  • Router.websocket("/ws/chat") for route-based WebSocket handlers
  • Path-scoped broadcast: messages sent to /ws/chat reach only clients connected to that path
  • send_text renamed to send on WebSocketConnection (send_text kept as alias)

DatabaseResult enhancements:

  • columns returns column names
  • column_info provides schema metadata (type, nullable, default) on demand
  • to_paginate formats results for paginated responses

Frond template additions:

  • Ternary-with-filter: {{ value ? value|upper : "default" }}
  • data_uri filter for inline file display in templates

v3.3.x

Released: March 24, 2026

v3.3.0 -- Queue API, Route Chaining (March 24)

Breaking change: Producer and Consumer classes removed. Use queue.produce() and queue.consume() directly.

ruby
# BEFORE (v3.2.x):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

producer = Tina4::Producer.new(queue)
producer.send(message)
consumer = Tina4::Consumer.new(queue)
consumer.listen { |msg| handle(msg) }

# AFTER (v3.3.0):

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

queue.produce("channel", { data: "payload" })
queue.consume("channel") do |job|
  handle(job)
  job.complete
end

Route chaining. Mark routes as authenticated or cached with chainable modifiers.

ruby
Tina4::Router.get("/dashboard").secure do |request, response|
  response.html("<h1>Dashboard</h1>")
end

Tina4::Router.get("/static-page").cache(ttl: 3600) do |request, response|
  response.html("<h1>Cached for one hour</h1>")
end

Other additions:

  • MongoDB queue backend
  • Database session handler for full backend parity
  • Valkey added to session handler options
  • Migration parity: advanced SQL splitting, status tracking, rollback via CLI
  • Auto-increment port if the default is in use; browser opens on startup

v3.2.x

Released: March 23, 2026

v3.2.0 -- Flexible Route Handlers (March 23)

Route handlers now accept zero, one, or two parameters. The framework detects what your block expects and provides the right objects.

ruby
# Zero params -- just return a response:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

Tina4::Router.get("/health") { "OK" }

# One param -- response only:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

Tina4::Router.get("/hello") { |response| response.html("Hello") }

# Two params -- request and response:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

Tina4::Router.get("/echo") do |request, response|
  response.json({ body: request.body })
end

# Named :request or :req -- single param receives the request:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

Tina4::Router.post("/submit") { |request| process(request.body) }

Bug fix: The 500 error overlay crashed because it did not receive the Rack environment. Fixed.


v3.1.x

Released: March 21 -- 22, 2026

v3.1.0 -- ORM Relationships, Caching, Queues (March 22)

The largest feature release after the initial launch. Fourteen capabilities landed in one version.

ORM relationships. Define has_many, has_one, and belongs_to with eager loading.

ruby
class User < Tina4::ORM
  has_many :posts
  has_one :profile
end

class Post < Tina4::ORM
  belongs_to :user
end

user = User.find(1)
user.posts  # => eager-loaded array of Post objects

Caching. Switch between memory, Redis, and file cache by setting one environment variable.

ruby
# .env:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

TINA4_CACHE_BACKEND=redis
TINA4_CACHE_URL=redis://localhost:6379

# Code stays the same:

## v3.10.70 (2026-04-06)

- **New:** SSE (Server-Sent Events) support via `response.stream()` — pass a generator, framework handles chunked transfer encoding, keep-alive, and `text/event-stream` content type
- **New:** Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js

Tina4::Cache.set("key", "value", ttl: 300)
Tina4::Cache.get("key")

Database query caching. Set TINA4_DB_CACHE=true for transparent query result caching.

Queue system. Switch between SQLite, RabbitMQ, and Kafka via .env without changing code.

Messenger. Unified messaging driven by environment configuration.

Scaffolding. tina4 generate model User, tina4 generate route api/users, tina4 generate migration create_users, tina4 generate middleware auth.

Frond template engine. raw/endraw blocks and from imports.

Performance. Frond pre-compilation caches parsed tokens. File rendering runs faster.

Other additions:

  • Production server auto-detection (Puma, cluster mode)
  • GitHub Actions CI/CD
  • Error pages: clean 404/500/403 without branding
  • numeric_field type in ORM
  • truthy?() helper method
  • Log rotation

v3.1.1 -- DevMailbox Fix (March 22)

Bug fix: DevMailbox timestamp precision was insufficient for reliable sort ordering.

v3.1.2 -- Documentation Fixes (March 22)

README code examples updated to match the actual v3 API. Quick start guide added.


v3.0.x

Released: March 21, 2026

v3.0.0 -- Initial Release (March 21)

The initial Ruby release. Zero gem dependencies. Everything the framework needs -- HTTP server, template engine, ORM, migrations, auth, queue, GraphQL, WebSocket, WSDL -- ships inside a single gem.

Core features:

  • Rack-based HTTP server (compatible with Puma, Thin, WEBrick)
  • Frond template engine (Twig-compatible syntax)
  • ORM with support for SQLite, PostgreSQL, MySQL, MSSQL, and Firebird
  • JWT authentication (RS256)
  • Queue system
  • GraphQL endpoint
  • WebSocket server
  • WSDL/SOAP service generation
  • DevAdmin dashboard with developer tooling
  • AI coding tool integration (auto-detect and install context for seven tools)
  • Full test suite passing

Quick start:

ruby
require "tina4"

Tina4::Router.get("/") { |request, response| response.html("<h1>Hello Tina4!</h1>") }

Tina4::App.new.run
bash
gem install tina4-ruby

The server starts on port 7147 by default. Set host: "0.0.0.0" for Docker deployments.


Pre-Release (v0.x)

Released: March 18, 2026

Versions v0.4.0 through v0.5.2 were development previews. They established the gem structure and basic routing but lacked the ORM, template engine, and queue system. If you used a v0.x release, upgrade directly to v3.0.0 -- there is no migration path from v0.x.

Sponsored with 🩵 by Code InfinityCode Infinity