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.
$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.7 —
Requestnow 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 normalisationmultipart/form-datacontent-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
HEADandOPTIONShandling,405 Method Not Allowedwith anAllowheader 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:
- 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 — fatalError. - After fixing the init order, that same line tried to mutate the
readonly$filesproperty — another fatalError.
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:
| Var | Default | What it does |
|---|---|---|
TINA4_LOG_FILE | (empty — stdout only) | Path to a log file. Empty leaves you on stdout. |
TINA4_LOG_DIR | logs | Directory for log files (joined with _LOG_FILE if relative). |
TINA4_LOG_FORMAT | text | text or json. JSON mode emits one structured record per line. |
TINA4_LOG_OUTPUT | stdout | stdout, file, or both. Strict — stdout means stdout only. |
TINA4_LOG_CRITICAL | false | Enables a Log.critical() level above error. Off = no-op. |
TINA4_LOG_ROTATE_SIZE | 10485760 (10 MB) | Rotate when the file exceeds this many bytes. 0 disables rotation. |
TINA4_LOG_ROTATE_KEEP | 5 | Number of rotated files to retain (app.log.1 … app.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.
# before (3.12.2 and earlier)
Tina4::Container.has(:mailer) # outdated
# after (3.12.3)
Tina4::Container.has?(:mailer) # idiomatic Ruby predicateThis 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 resolve → get 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 — 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 missingTINA4_CACHE_*,TINA4_QUEUE_*,TINA4_KAFKA_*,TINA4_RABBITMQ_*,TINA4_MONGO_*,TINA4_WS_BACKPLANE, and the entireTINA4_SESSION_VALKEY_*block.
Other fixes
- Ruby
lib/tina4/ai.rb— subprocess output is now force-encoded to UTF-8 beforeString#strip, fixingEncoding::CompatibilityErrorthat crashed 4 ai specs on systems with non-ASCII pip output. - Node
test/serverParity.test.ts— setsTINA4_OVERRIDE_CLIENT=truesostart()actually runs, plus emits theN passed, M failedsummary 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
| Symptom | Fix |
|---|---|
Ruby: NoMethodError: undefined method 'has' for Tina4::Container | Replace 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 instance | Use 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 write | Resolved 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.
TINA4_DATABASE_FIREBIRD_PATH=C:\firebird\data\app.fdb
TINA4_DATABASE_URL=firebird://SYSDBA:masterkey@localhost:3050/ignoredShipped 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.tomlhad drifted to3.10.41while__init__.pyread3.12.1. Synced both to 3.12.2 souv buildand runtime introspection now agree. - chore(claude.md, all 4): stale framework version banners in
CLAUDE.mdheaders 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.ymlworkflow now declarespermissions: contents: writeon the publish job. Without this,softprops/action-gh-release403'd against the defaultGITHUB_TOKENon 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-releasefrom@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.envfiles set withDATABASE_URL,SECRET,SMTP_HOST,HOST_NAME, etc. will cause the framework to refuse to boot. Runtina4 env --migrateto 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 --migrateCLI added to all four frameworks. Reads your.env, rewrites it in place, leaves a.env.bakbackup, 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 withTINA4_ALLOW_LEGACY_ENV=truefor 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.pyCI gate stays at 0 broken links / 0 broken anchors. - Frond bundle rebuilt at v2.1.3 —
frond.min.jsfooter 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 withInFailedSqlTransaction. 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=offkills the feature entirely.src/public/index.htmlauto-serves at/(and/foo/servessrc/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 OKstatus line is fixed — every status code now uses its canonical RFC 7231/9110 reason phrase.
- Auto-routing now scans
- #37 frond.form.submit redirect handling — verified shipped at v2.1.x;
xhr.responseURLchange triggerswindow.locationnavigation 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 name | New name |
|---|---|
DATABASE_URL | TINA4_DATABASE_URL |
DATABASE_USERNAME | TINA4_DATABASE_USERNAME |
DATABASE_PASSWORD | TINA4_DATABASE_PASSWORD |
DB_URL | TINA4_DATABASE_URL (alias dropped) |
SECRET | TINA4_SECRET |
API_KEY | TINA4_API_KEY |
JWT_ALGORITHM | TINA4_JWT_ALGORITHM |
SMTP_HOST | TINA4_MAIL_HOST |
SMTP_PORT | TINA4_MAIL_PORT |
SMTP_USERNAME | TINA4_MAIL_USERNAME |
SMTP_PASSWORD | TINA4_MAIL_PASSWORD |
SMTP_FROM | TINA4_MAIL_FROM |
SMTP_FROM_NAME | TINA4_MAIL_FROM_NAME |
IMAP_HOST | TINA4_MAIL_IMAP_HOST |
IMAP_PORT | TINA4_MAIL_IMAP_PORT |
IMAP_USER | TINA4_MAIL_IMAP_USERNAME |
IMAP_PASS | TINA4_MAIL_IMAP_PASSWORD |
HOST_NAME | TINA4_HOST_NAME |
SWAGGER_TITLE | TINA4_SWAGGER_TITLE |
SWAGGER_DESCRIPTION | TINA4_SWAGGER_DESCRIPTION |
SWAGGER_VERSION | TINA4_SWAGGER_VERSION |
ORM_PLURAL_TABLE_NAMES | TINA4_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
- Backup your
.env:cp .env .env.bak.pre-v3.12 - Run the migration:
tina4 env --migrate— rewrites your.envin place. - Update PaaS dashboards: Heroku, Railway, Vercel, Render, Fly.io etc — rename the same vars in your provider's env-var UI.
- 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:
- Python —
threading.local()on theDatabaseinstance - Ruby —
Thread.current[:tina4_pinned_adapter_<obj_id>] - Node.js —
AsyncLocalStoragefromnode: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()underpool=4now leaves zero rows (was leaking three). Three INSERTs followed bycommit()persists exactly three. Pin-release after commit/rollback verified.pool=0regression 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 explicitstringtypes in addition to the existingint/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):
sqlite3promoted fromadd_development_dependencytoadd_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
@templatedecorator ordering corrected (must sit BELOW the route decorator) in book chapters 04 and 10; Pythonrequest->queryvsrequest->paramsdistinction 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
truncatefilter - #82
{{ parent() }}/{{ super() }}across all 4 frameworks - #83 Ruby dashboard — WEBrick is runtime dep
- #89
load_dotenvrename,DatabaseResultmethods, SQLite WAL locking - #91 Ruby
request.paramssymbol + string keys viaIndifferentHash - #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:///Xresolves under cwd; parent directory auto-created only when inside cwd. Absolute paths are trusted and never mkdir'd at root. - fix (python):
_ensure_foldersno longer creates a bogussrc/migrations/directory. The migration runner always looks atmigrations/at the project root — there is only one correct location. - parity (php, ruby, node): Same
sqlite:///Xparsing as Python. Dedicatedresolve_path/resolveSqlitePathhelpers in each framework so adapters consistently handle:memory:,./forms, Windows drive letters. - tests: 9 new Python tests in
TestSQLiteConnectionPath+TestProjectFolders. 4 new PHP tests inDatabaseUrlTestcovering relative/absolute/Windows/bruce-regression. 6 new Ruby specs indatabase_drivers_spec.rb :: SqliteDriver.resolve_path. Node URL tests expanded indatabase.test.tswith 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.validateno longer re-coerces values that are already the correct type. Previously, any PostgreSQL/MSSQL read of a row containing aDateTimeFieldcrashed becausedatetime(datetime_instance)raisesTypeError. The fix accepts native driver types (datetime,bytes,int,bool,float,str) without re-wrapping, and parses ISO-8601 strings intodatetimefor SQLite. Seetina4-python/plan/orm-field-validate-native-types.md. - fix (python ORM):
BooleanFieldvsIntegerFieldordering handled explicitly.BooleanField(1)still coerces toTrue,IntegerField(True)still coerces to1; no regression for either direction (bool is a subclass of int in Python). - tests (python): 10 new
TestFieldsNativeTypescases 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=0as the baseline, so the first poll after every page load triggeredlocation.reload(), which resetmt=0again. Loop now initialises the baseline on the first poll. - fix (php): Reload sentinel removed — PHP was the only framework recursively walking
src/and touchingsrc/.reload_sentinelon 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
pollStartedguard). - feat (parity):
GET /__dev/api/queue/topicsandGET /__dev/api/queue/dead-lettersadded to PHP, Ruby and Node (previously only in Python). PHP queue endpoints now read from the realTina4\Queuebackend instead of returning stubs. - feat (devadmin): Refreshed
tina4-dev-admin.jsbundle (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
@authdirectives 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/reloadand internal watchers are disabled when launched by the Rust CLI (--managed). - fix (cli):
parseFlags/parse_flags/parseCliArgsno longer swallowhost:portor 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/fromOrmlist resolver usedselect(skip=)instead ofall(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.exampleand 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_mapnow defaults toTrue— ORM models automatically map between camelCase properties and snake_case DB columns. Setauto_map = Falseon your model to restore the old behaviour. - feat:
to_dict(case=)parameter — passcase='camel'to get camelCase keys (for JSON APIs) orcase='snake'(default) for snake_case keys matching DB columns.
- feat: Frond
replacefilter now accepts dict args —{{ v|replace({"T": " ", "-": "/"}) }}for multiple substitutions in one call. - feat:
background(callback, interval)— register periodic tasks that run cooperatively in the asyncio event loop. Replacesthreading.Threadfor background work. - feat: Background task protection — sync callbacks run in a
ThreadPoolExecutorviarun_in_executor()withasyncio.wait_for()timeout, preventing blocking functions from freezing the server. - feat: Docker image now bundles the example store demo —
docker run tina4stack/tina4-python:v3starts a working app out of the box. - fix: Cart nav badge now updates reactively on quantity change and item removal (tina4-js
signal/computed/effect). - fix: Non-blocking queue consumer —
process_orders()usesqueue.pop()(single job per tick) instead of blockingqueue.consume(). - tests: 6 new parity tests covering
to_dict(case=),auto_mapdefault,replacefilter (dict + positional), andbackground()registration. - parity: All features shipped identically across Python, PHP, Ruby, Node.js. 2,304 tests passing.
v3.10.97 (2026-04-11)
- fix: frond.form.submit redirect handling — XHR transparently follows 3xx redirects, so the callback received redirected HTML instead of navigating. Fixed by comparing
xhr.responseURLwith the original URL and callingwindow.location.hrefwhen a redirect is detected. - fix: Currency placeholder — locale files now default to
$for the currency symbol. - fix: Admin sidebar alignment — widened sidebar to 220px with
min-widthto prevent label truncation. - fix: Admin table overflow — added
min-width: 0andoverflow-x: autoon.admin-mainto prevent content clipping. - fix: Order detail template — corrected variable names (
itemsinstead oforder.items,item.nameinstead ofitem.product_name) and used.recordsfromDatabaseResult. - fix: Status badges — dashboard recent orders and order list now show colored badge pills with translated status labels (pending, processing, shipped, delivered, cancelled).
- fix: Date formatting — admin order/dashboard dates trimmed to
YYYY-MM-DD HH:MM:SSinstead of raw ISO with microseconds. - feat: Cart quantity spinner — reactive qty controls using tina4-js signals, computed values, and effects.
- feat: Multi-currency pricing — forex conversion via Api client (frankfurter.app),
|currencytemplate filter, currency selector in navbar. - feat: MCP server tools —
check_stock,low_stock_report,search_productstools andstore://categories,store://inventory-summaryresources for AI assistant integration. - feat: Contact form — built with
HtmlElementandadd_html_helpers()to demonstrate programmatic HTML generation. - feat: ORM named scopes —
Product.scope("active"),Product.scope("low_stock"),Product.scope("expensive"). - feat: Database connection pooling —
Database("sqlite:data/store.db", pool=4). - feat: Inline tests —
@testsdecorators oncart_service.pyandforex_service.py. - feat: Language toggle — flag button (🇫🇷/🇬🇧) in navbar to switch locale.
- feat: Helpdesk chat persistence — chat messages stored in DB, history API (
GET /api/chat/history). - dep: Updated frond.min.js to v2.1.2 across all 4 frameworks (Python, PHP, Ruby, Node.js).
- parity: All 4 frameworks bumped to 3.10.97.
v3.10.93 (2026-04-11)
- fix: Frond array/dict literal support —
{% set items = ["a", "b"] %}and{% set obj = {"k": "v"} %}now parse correctly. - fix: Frond bracket depth tracking in
_find_outside_quotes()and_split_outside_quotes()— expressions likearr[i % 2]no longer treated as top-level arithmetic. - fix: Frond subscript expression evaluation — bracket content uses
_eval_expr()instead of_resolve(), enablingarr[loop.index0 % 2]. - fix: Frond slice with variable bounds —
items[start:end]evaluates bounds through_eval_expr(). - fix: Frond multiline
{% set %}—_SET_REregex now usesre.DOTALLflag. - docs: Developer skills updated — Metrics Dashboard guidance, Frond Template Parity rules,
@noauthsecurity warnings. - demo: Complete e-commerce store example (
example/store/) with GraphQL search, SSE, WebSocket, Queue, Events, 13 test files. - parity: All Frond fixes applied identically across Python, PHP, Ruby, Node.js. 2,304 tests passing.
v3.10.92 (2026-04-10)
- refactor: Extract
RateLimiterfromcore/middleware.pyinto its own filecore/rate_limiter.py. The old import path still works via re-export. - feat: Add
RateLimiterMiddlewarewrapper class withbefore_rate_limit()andcheck()static methods. - breaking: Rename
ErrorOverlaymethods —render()→render_error_overlay(),render_production()→render_production_error(),debug_mode()→is_debug_mode(). - feat: Add
Server.start()andServer.stop()for cross-framework parity. - feat: Add
DatabaseResult.size(),to_array(),to_json(),to_csv()methods. - feat: Add
ScssCompilerclass withcompile(),compile_file(),add_import_path(),set_variable(). - feat: Add
DevAdmin.unresolved_count(),clear_all(),reset(),capture()(5-param),register(). - fix: GraphQL test API — update
add_query()calls to use positional args (args, return_type, resolver). - parity: 44/44 cross-framework features green. 2,263 tests passing.
v3.10.91 (2026-04-10)
- feat: Add parity methods —
GraphQLType.parse(),Response.send()params,QueryBuilder.from_(),Debug.configure(). - breaking: Remove alias methods
from_,configure,template— use canonical names only (from_table, etc.).
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>-wrapped output, and theTINA4_DEBUG=trueproduction gate. Filter table entry updated to reference the new section. - docs:
plan/parity/parity-template.mdupdated 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_render_dump()helper and produce identical output. - security: Dump is now gated on
TINA4_DEBUG=true. In production (env var unset orfalse) both the filter and function silently return an empty string. 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. - refactor: Dump output is wrapped in
<pre>and HTML-escaped via a single shared code path. - test: 6 new tests in
test_frond.py(TestDump) covering debug-mode output, production silencing, unset-env default-to-production, function/filter parity, and circular references.
v3.10.86 (2026-04-09)
- feat:
ForeignKeyFieldis now a properFieldsubclass that auto-wires both sides of the relationship. Declaringauthor_id = ForeignKeyField(to=Author)injectsbelongs_toon the declaring model andhas_manyon the referenced model viaORMMeta— no manual descriptor calls required. Override the has-many name withrelated_name=. - feat: Cross-framework parity — same FK auto-wiring semantics now available in PHP (
$foreignKeys), Ruby (foreign_key_field), and Node.js (type: "foreignKey") - fix:
@orm_bind(db)no longer nulls the decorated class — returns a pass-through decorator - fix:
Auth.get_token/valid_token/get_payload/refresh_token/authenticate_requestcan now be called on the class (e.g.Auth.get_token(payload)) or on an instance via the_DualMethoddescriptor - fix:
SQLiteAdapteruses a class-levelthreading.Lock+PRAGMA busy_timeout = 30000+timeout=30on connect to eliminateSQLITE_BUSYdeadlocks in the dev server under concurrent writes - docs: Chapter 6 (ORM) updated with a new "ForeignKeyField — Auto-Wired Relationships" section
v3.10.85 (2026-04-09)
- refactor: Split queue adapters into separate files —
queue/rabbitmq_backend.py,queue/kafka_backend.py,queue/mongo_backend.py(one class per file, aligning with PHP/Node/Ruby architecture) - fix: Updated remaining tests to use bool
valid_token()+get_payload()pattern
v3.10.84 (2026-04-09)
- fix: Router/middleware was setting
request.user/request.auth/ auth payload totrue(boolean) instead of the actual JWT payload dict aftervalidToken()was changed to return bool — any code readingrequest.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)
- fix:
AuthMiddleware.before_requestcalledget_payloadincorrectly — would TypeError at runtime on valid token - add: Headless routing auth payload integration tests to prevent regression
v3.10.83 (2026-04-08)
- fix: prevent orphaned session files on WebSocket and anonymous requests (#36)
- 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
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, andtext/event-streamcontent type - New: Chapter 24 added to documentation: Server-Sent Events
- Feature count: 45 (was 44)
- Full parity across Python, PHP, Ruby, Node.js
Version History
Tina4 Python follows semantic versioning. The major version (3) marks the ground-up rewrite from v2. Minor versions (3.1, 3.2, ...) introduce features. Patch versions fix bugs and polish edges.
Every release ships through PyPI. Upgrade with:
uv add tina4-python@latest
# or with pip
## 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
pip install --upgrade tina4-pythonCheck your current version:
import tina4_python
print(tina4_python.__version__)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, all/select/where return arrays, toDict/toAssoc standardized, scope() registers reusable method, where()/all() on Node, count() on PHP
- Auth: expires_in in minutes, PBKDF2 260k iterations, env TINA4_SECRET fallback, API key fallback in authenticateRequest
- Session: dual-mode flash(), get_flash/getFlash, cookieHeader on all, getSessionId on Node, save() public on Node
- Database: execute() returns bool/DatabaseResult for RETURNING, get_last_id/get_error on all, getColumns on PHP, cacheStats on Node
- Request/Response: Node files as dict, Python query property, cookies on PHP/Node, contentType on Node, xml() on PHP/Node, Ruby callable response
- Queue: consume() poll_interval (long-running generator with built-in sleep)
- WebSocket: event naming standardized (open/message/close/error), connection ip/headers/params, Python on() string API
- GraphQL: schema_sdl()/schemaSdl() and introspect() on all 4
- WSDL: Node.js zero-dep DOM parser (replaced regex)
- Events: emitAsync()/emit_async() on all 4
- i18n: zero-dep YAML locale file support on Python/PHP/Node (Ruby already had it)
v3.10.67 (2026-04-03)
- BREAKING: request.files content is now raw bytes — previously base64-encoded; remove any
base64.b64decode()calls when saving uploaded files. Writefile["content"]directly to disk - load() is now an instance method —
model.load(sql, params)calls selectOne internally, populates the instance, returnsTrue/False. UseModel.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
- tina4-js skill — critical input binding warning, routing docs (
{param}not:param), file upload pattern
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 — interactive Python REPL with framework loaded (db, Router, ORM, Auth, Api, Log)
- tina4 env — interactive environment configuration (database, cache, session, queue, mail)
- Brand update — "TINA4 — The Intelligent Native Application 4ramework"
- Quick reference — 36 sections covering every framework feature
- Chapter reshuffle — 37 chapters, 7 new (Events, Localization, Logging, API Client, WSDL/SOAP, DI Container, Service Runner)
- RouteGroup fix — double prefix bug resolved
- Port kill-and-take-over — default port always reclaimed on startup
- Metrics test detection — expanded to check spec/, tests/, test/ directories
- MongoDB adapter (pymongo), ODBC adapter (pyodbc)
- Pagination standardized — limit/offset primary, merged dual-key response
- 9,138 tests across all 4 frameworks
v3.10.57 (2026-04-02)
- MongoDB adapter —
Database("mongodb://host:port/db"), requirespip install pymongo - ODBC adapter — parity with PHP/Ruby/Node
- RouteGroup class —
group.get()/group.post()syntax matching PHP/Ruby/Node - Pagination standardized — limit/offset primary, merged dual-key toPaginate() response
- Test port at +1000 — user testing port (e.g. 8145) stable, no hot-reload
- Dynamic version —
__version__read at runtime, no hardcoded constants - ORM TINA4_DATABASE_URL discovery — auto-connect from env
- Firebird path parsing — preserves absolute paths
- 108 features at 100% parity, 2,112 tests
v3.10.54 (2026-04-02)
- Auto AI dev port — second listener on port+1 with no-reload when TINA4_DEBUG=true
- TINA4_NO_RELOAD env var + --no-reload CLI flag
- Firebird path parsing — preserve absolute paths
- ORM TINA4_DATABASE_URL discovery — auto-connect from env
- SQLite transaction safety — commit() no-op without transaction
- QueryBuilder docs — added to ORM chapter
v3.10.48 — April 2, 2026
Bug Fixes
Production server explicit opt-in — All frameworks now require an explicit --production flag to use production servers (Puma for Ruby, FrankenPHP for PHP, cluster mode for Node.js). Previously, production servers activated automatically when TINA4_DEBUG=false, which was surprising behaviour. Now tina4 serve always uses the dev server unless --production is passed.
Python --no-browser — The run() function now accepts no_browser=True and respects the TINA4_NO_BROWSER env var to prevent browser auto-opening on server start.
Test Coverage
Python: 2,132. PHP: 1,992. Ruby: 2,387. Node.js: 2,546. Total: 9,057 tests, 0 failures.
v3.10.46 — April 1, 2026
Test Coverage
Massive test parity push across all 4 frameworks. CSRF middleware tests expanded to 29+ per framework. Dedicated test suites added for FakeData, Cache, DevMailbox, Static files, Metrics, CLI scaffolding, and all remaining gap areas. Python: 2,132 tests. PHP: 1,937. Ruby: 2,274. Node.js: 2,546. Total: 8,889 tests, 0 failures, 49 core areas with full parity.
v3.10.45 — April 1, 2026
Bug Fixes
PHP CLI serve hijack — When index.php calls App::run(), the CLI serve command now sets a TINA4_CLI_SERVE constant so run() returns early, letting the CLI manage the server lifecycle (port, debug mode, browser open). Previously, index.php's run() would start its own server and block the CLI's serve logic.
v3.10.44 — April 1, 2026
New Features
Database tab redesign — The dev admin Database panel now uses a split-screen layout. Tables are listed on the left as a navigation sidebar with click-to-select highlighting. The query editor, toolbar, and results occupy the right panel. Results render immediately below the query box with no gap.
Copy CSV / Copy JSON — Two new buttons in the database toolbar copy query results to the clipboard. CSV uses proper comma-separated format with quoting; JSON copies a formatted array of objects.
Paste data — A new Paste button opens a modal where you can paste JSON arrays or CSV/tab-separated data. The tool auto-detects the format and generates INSERT statements. If a table is selected on the left, it targets that table. If no table is selected, it prompts for a name and generates a CREATE TABLE statement for new tables. If you paste SQL directly, it passes through to the query box unchanged.
Multi-statement execution — The query runner now handles multiple SQL statements separated by semicolons. CREATE TABLE + INSERT batches run in a single transaction with automatic rollback on error.
Database badge on load — The Database tab count badge now shows the table count immediately when the dev admin opens, without needing to click the tab first.
Star wiggle animation — The GitHub star button on the landing page uses an empty star (☆) with a playful wiggle animation: 3-second delay on page load, then wiggles at random 3–18 second intervals.
Bug Fixes
Default port — Python default port changed from 7145 to 7146 to avoid clashes when running multiple Tina4 frameworks (PHP=7145, Python=7146, Ruby=7147, Node=7148).
SQLite LIMIT fix — The SQLite adapter now checks if a query already contains a LIMIT clause before appending one, preventing double-LIMIT errors in the database browser.
browseTable quote escaping — Fixed broken onclick handlers for table names in the database panel. Now uses addEventListener instead of inline onclick with escaped quotes.
Migration UP/DOWN separation — Migration generator no longer puts DOWN SQL in the .sql file. UP SQL stays in the .sql file; DOWN SQL goes in the separate .down.sql file.
Test Coverage
Major test expansion across all 4 frameworks — 8,107 total tests (up from ~5,200), with full parity across 49 core feature areas. New dedicated test suites added for FakeData, Cache, DevMailbox, Static files, Metrics, and CLI scaffolding.
Python: 2,132 tests passing (12 skipped).
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 PyPI (e.g. running v3.10.39 locally while PyPI still has v3.10.24). The overlay now shows a purple "ahead of PyPI" message. Also added a breaking changes warning (red banner with changelog link) when a major or minor version update is available, so developers know to check for breaking changes before upgrading.
v3.10.39 — April 1, 2026
Breaking Changes
This release aligns the Python framework with the other three Tina4 implementations. Two breaking changes affect existing code:
Auth.check_password() parameter order reversed
# BEFORE (v3.10.38 and earlier)
## 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
Auth.check_password(hashed, password)
# AFTER (v3.10.39+)
## 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
Auth.check_password(password, hashed) # password first — matches PHP, Ruby, Node.jsRouter.all() removed — use get_routes() or list_routes()
# BEFORE
## 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
routes = Router.all()
# AFTER
## 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
routes = Router.get_routes() # or Router.list_routes()New Features
Auth.validate_api_key(provided, expected=None)
Compare API keys with constant-time comparison. Optionally pass expected; if omitted, reads TINA4_API_KEY (or TINA4_API_KEY) from environment.
Auth.validate_api_key("sk-abc123") # check against env
Auth.validate_api_key("sk-abc123", "sk-abc123") # check against explicit valueAuth.authenticate_request(headers)
One-call header authentication: checks Bearer JWT, Bearer API key, and Basic auth in order.
payload = Auth.authenticate_request(request.headers)
# Returns: dict on success, None on failure
## 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.jsORM.find_by_id(pk) with find() and load() as aliases
find_by_id() is now the explicit primary method. Both find() and load() continue to work as aliases, ensuring backward compatibility.
Test Coverage
2,054 tests passing (up from 2,051 in v3.10.38).
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 are represented as animated bubbles — sized by lines of code, colored by maintainability index (green = healthy, red = needs attention). Click any bubble to drill down into per-function cyclomatic complexity.
The metrics engine uses Python's ast module for zero-dependency static analysis:
- Cyclomatic complexity per function
- Halstead volume and maintainability index
- Afferent/efferent coupling and instability
- Violation detection (CC > 20, LOC > 500, MI < 20)
File analysis is sorted worst-first so problem areas surface immediately. Results are cached for 60 seconds with mtime-based invalidation.
AI Context Installer
The tina4python ai command now presents a simple numbered menu instead of unreliable auto-detection:
1. Claude Code CLAUDE.md [installed]
2. Cursor .cursorules
3. GitHub Copilot copilot-instructions.md
...
8. Install tina4-ai tools (requires Python)Select by number (comma-separated or all). Already-installed tools show green. The generated context now includes the full skills table across all frameworks.
Dashboard Improvements
- Full-width layout (removed 1400px max-width constraint)
- Sticky header and tab bar when scrolling
- Dashboard overlay fills the screen (was constrained to 1200px)
Cleanup
- Removed
demo/directories from all framework repos (demos live in documentation) - Removed old
plan/spec documents, replaced withPARITY.mdandTESTS.md - Removed junk sample files (broken migrations, test templates)
- Central parity matrix added to tina4-book
v3.10.x — Previous Releases (March 28-31, 2026)
The v3.10 line carries the most patches of any minor release. It refined the Frond template engine, hardened the ORM, and completed cross-framework parity.
Highlights
Singleton Frond engine (v3.10.0). The template engine creates one instance and reuses it. Previous versions spawned a new engine per render call. This cut template rendering overhead across the board.
ORM
auto_mapflag (v3.10.1). Models translate betweensnake_caseandcamelCasecolumn names without manual mapping.
from tina4_python import ORM
class UserProfile(ORM):
auto_map = True # created_at ↔ createdAt handled for you
table_name = "user_profiles"- Frond method calls and slice syntax (v3.10.2). Templates gained the ability to call methods on objects and use Python slice syntax.
<!-- Method calls on template variables -->
{{ user.get_display_name() }}
<!-- Slice syntax -->
{{ long_text[:100] }}...Frond quote-aware operator matching (v3.10.5). Operators inside quoted strings no longer break the parser. Before this fix, a string containing
>=or==could confuse the template engine.ORM auto-commit on write operations (v3.10.13). Save, delete, and update operations commit their transactions without an explicit call.
to_jsonandjs_escapefilters (v3.10.16). Templates gained filters for safe JSON embedding and JavaScript string escaping.
<script>
const config = {{ settings|to_json }};
const message = "{{ user_input|js_escape }}";
</script>formTokenValue()helper (v3.10.23). CSRF tokens gained a dedicated template function for cleaner form markup.
<form method="post">
<input type="hidden" name="form_token" value="{{ formTokenValue() }}">
<!-- form fields -->
</form>- MCP server and TestClient parity (v3.10.32). The built-in MCP server and integration test client reached feature parity with the PHP and Ruby implementations.
- Arithmetic in
{% set %}and expressions (v3.10.31). The template engine handles math operations inside assignment blocks.
{% set total = price * quantity + shipping %}Bug Fixes
Middleware not applied to routes (fixed in v3.10.1). Middleware functions registered with @middleware were silently skipped during route dispatch. Routes ran without their middleware.
# Before (broken) — middleware was silently skipped
## 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
@app.middleware
def log_request(request, response):
print(f"Request: {request.method} {request.url}")
return request, response
@app.get("/users")
def get_users(request, response):
# middleware never ran
return response("OK")# After (fixed in v3.10.1) — middleware runs on every matching route
## 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
@app.middleware
def log_request(request, response):
print(f"Request: {request.method} {request.url}")
return request, response
@app.get("/users")
def get_users(request, response):
# log_request fires before this handler
return response("OK")Wildcard route matching (fixed in v3.10.1). Routes with wildcard segments failed to match incoming requests. The router now handles wildcards as expected.
Auth.valid_token reference error (fixed in v3.10.9). The internal server module referenced the wrong attribute name when calling token validation, which caused a TypeError on every secured request. The fix resolved the reference so Auth.valid_token works as expected.
Frond dict[variable_key] access (fixed in v3.10.11). Accessing a dictionary with a variable key inside templates raised a KeyError. The engine now resolves the variable before the lookup.
<!-- Before (broken) — raised KeyError -->
{% set key = "name" %}
{{ user[key] }}
<!-- After (fixed in v3.10.11) — resolves variable, then looks up the key -->
{% set key = "name" %}
{{ user[key] }} <!-- now outputs user["name"] -->ORM transaction errors on SQLite (fixed in v3.10.25). Calling save() or delete() on an ORM model raised "cannot commit -- no transaction is active" on SQLite. The ORM now wraps every write operation in a proper start_transaction / commit / rollback cycle.
# Before (broken on SQLite)
## 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()
user.name = "Alice"
user.save() # raised "cannot commit -- no transaction is active"
# After (fixed in v3.10.25) — save() wraps in a transaction automatically
## 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()
user.name = "Alice"
user.save() # works on all database engines including SQLiteStale templates in dev mode (fixed in v3.10.24). The dev server cached rendered templates and did not pick up file changes until restart. Templates now reload on every request in debug mode.
Macro output HTML escaping (fixed in v3.10.27). Frond macros returned raw strings that the auto-escaper then double-escaped. Macro output now wraps in SafeString to preserve the intended HTML.
DevReload performance (fixed in v3.10.28). The live-reload watcher polled too fast and triggered duplicate reloads. It now uses a 3-second default interval with debouncing.
Breaking Changes
@noauth and @secured decorator behavior (v3.10.1). Before this fix, these decorators did not update the route's auth flags. If you relied on the broken behavior (routes ignoring auth decorators), your routes will now enforce authentication as intended.
# Before (broken) — decorator had no effect
## 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
@app.get("/public")
@noauth
def public_page(response):
return response("Open to all") # still required auth
# After (fixed in v3.10.1) — decorator works as expected
## 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
@app.get("/public")
@noauth
def public_page(response):
return response("Open to all") # accessible without tokenv3.9.x — QueryBuilder and Sessions (March 26-27, 2026)
Features
QueryBuilder with fluent API (v3.9.0). SQL construction through method chaining, integrated with the ORM.
from tina4_python import ORM
class User(ORM):
table_name = "users"
# Fluent query 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 query
## 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
from tina4_python import QueryBuilder
results = QueryBuilder.table("orders") \
.select("customer_id", "SUM(total) as revenue") \
.where("status = ?", ["completed"]) \
.group_by("customer_id") \
.having("revenue > ?", [1000]) \
.get()Path parameter injection (v3.9.0). Route handlers receive path parameters as named function arguments. No more digging through request.params.
# Before v3.9.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
@app.get("/users/{id}")
def get_user(request, response):
user_id = request.params["id"]
return response(f"User {user_id}")
# v3.9.0 and later
## 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
@app.get("/users/{id:int}")
def get_user(id, request, response):
return response(f"User {id}") # id is already an intAuto-start sessions (v3.9.0). Every route handler receives request.session with zero configuration. The session API covers get, set, delete, has, clear, destroy, regenerate, flash, and get_flash.
@app.get("/dashboard")
def dashboard(request, response):
visits = request.session.get("visits", 0)
request.session.set("visits", visits + 1)
return response(f"Visit #{visits + 1}")CSRF middleware and form tokens (v3.9.1). Session-bound CSRF tokens protect forms by default. Toggle with the TINA4_CSRF environment variable.
# CSRF is on by default in v3.9.1+
## 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
# Disable for API-only apps:
## 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_CSRF=false
## 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.jsFile-based queue backend (v3.9.1). Replaced the SQLite queue with a JSON file-based backend. Zero dependencies. Full cross-platform parity with the PHP and Ruby implementations.
NoSQL QueryBuilder (v3.9.2). The fluent API gained to_mongo() to generate MongoDB queries from the same builder syntax.
query = QueryBuilder.table("users") \
.where("age > ?", [21]) \
.order_by("name") \
.limit(10) \
.to_mongo()
# Returns a MongoDB-compatible query dict
## 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.jsWebSocket backplane (v3.9.2). Redis pub/sub for scaling WebSocket broadcast across multiple server instances.
SameSite cookie default (v3.9.2). Session cookies default to SameSite=Lax. Override with the TINA4_SESSION_SAMESITE environment variable.
Breaking Changes
Session API rename (v3.9.0). The unset() method became delete() for cross-framework parity. unset() still works as an alias but will show a deprecation warning in future releases.
# Before
## 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
request.session.unset("cart")
# After
## 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
request.session.delete("cart")Environment variable standardization (v3.9.1). All framework environment variables now follow the TINA4_* naming convention. TOKEN_LIMIT became TINA4_TOKEN_LIMIT. Check your .env file and rename any bare variables.
Queue backend change (v3.9.1). The SQLite queue backend no longer exists. If you used it, the file-based backend is a drop-in replacement. Queue data from the old SQLite store must be migrated manually.
v3.8.x — Pooling, Validation, and Security (March 25-26, 2026)
Features
Connection pooling (v3.8.1). Pass pool=N to the Database constructor for round-robin, thread-safe connection pooling.
from tina4_python import Database
db = Database("sqlite:///app.db", pool=4)
# Four connections rotate across requests
## 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.jsValidator class (v3.8.1). Input validation with an error response envelope.
from tina4_python import Validator
errors = Validator.validate(request.body, {
"email": "required|email",
"age": "required|integer|min:18"
})
if errors:
return response({"errors": errors}, 422)Upload size limit (v3.8.1). The TINA4_MAX_UPLOAD_SIZE environment variable caps file uploads. Set it in bytes.
TestClient for integration tests (v3.8.1). Test your routes without starting the server.
from tina4_python import TestClient
client = TestClient(app)
result = client.get("/api/users")
assert result.status_code == 200SecurityHeadersMiddleware (v3.8.1). One import adds X-Frame-Options, Strict-Transport-Security, Content-Security-Policy, and X-Content-Type-Options to every response.
from tina4_python import SecurityHeadersMiddleware
app.use(SecurityHeadersMiddleware())Zero core dependencies (v3.8.x). Database drivers, queue backends, and session handlers became optional installs. The core framework runs on Python's standard library alone.
v3.7.x — Template Auto-Serve and Firebird Fixes (March 25, 2026)
Features
Template auto-serve at / (v3.7.0). Place an index.html or index.twig in src/templates/ and the framework serves it at the root path. User-registered GET / routes take priority.
src/
templates/
index.html ← served at / with no route neededFirebird idempotent migrations (v3.7.0). ALTER TABLE ADD statements on Firebird check RDB$RELATION_FIELDS before executing. Columns that already exist are skipped. Other databases and statement types are not affected.
v3.6.x — API Parity (March 24, 2026)
Breaking Changes
Auth method renames (v3.6.0). The authentication API aligned with the PHP, Ruby, and Node.js implementations.
# Before 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
from tina4_python import Auth
token = Auth.create_token(payload) # old name
valid = Auth.validate_token(token) # old name# v3.6.0 and later
## 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
from tina4_python import Auth
token = Auth.get_token(payload) # new primary name
valid = Auth.valid_token(token) # new primary name
# create_token and validate_token still work as aliases
## 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.jsPagination parameter rename (v3.6.0). skip became offset across all query methods.
# Before
## 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.find(skip=10, limit=5)
# After
## 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.find(offset=10, limit=5)Token expiry parameter rename (v3.6.0). token_expiry became expires_in and now accepts minutes instead of seconds.
# Before
## 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, token_expiry=3600) # seconds
# After
## 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, expires_in=60) # minutesLocale environment variable (v3.6.0). LOCALE became TINA4_LOCALE. Update your .env file.
v3.5.x — Bundled Frontend (March 24, 2026)
Features
tina4js bundled (v3.5.0). The reactive frontend library (13.6 KB minified) ships with the framework. No CDN link needed. Import it from your templates and build reactive UIs with signals, components, and declarative routing.
AutoCrud Swagger metadata (v3.5.0). Routes generated by AutoCrud now include Swagger annotations. They appear in the auto-generated API docs without extra configuration.
v3.3.x — WebSockets, File Uploads, and Frond Improvements (March 24, 2026)
Features
Route-based WebSocket handlers (v3.3.0). Define WebSocket endpoints with the same decorator pattern as HTTP routes.
@app.websocket("/ws/chat")
def chat_handler(message, client):
# message is the incoming data
# client.send() to reply
client.send(f"Echo: {message}")File upload improvements (v3.3.0). Uploaded files include raw bytes, a data_uri template filter, and consistent property names across all frameworks.
@app.post("/upload")
def handle_upload(request, response):
file = request.files[0]
print(file.filename) # standardized from file_name
print(file.type) # new in v3.3.0
raw = file.content # raw bytes
return response("Uploaded")Lazy column_info() on DatabaseResult (v3.3.0). Query results expose schema metadata on demand. Call result.column_info() to inspect column names, types, and sizes without a separate query.
@any decorator alias (v3.3.0). @any works as shorthand for @any_method, matching the PHP, Ruby, and Node.js API.
Ternary-with-filter support in Frond (v3.3.0). Templates handle inline conditionals that pipe their result through a filter.
{{ user.name if user else "Anonymous"|upper }}Breaking Changes
job.data renamed to job.payload (v3.3.0). Queue jobs use payload as the primary attribute. job.data remains as a read-only property alias but will be removed in a future release.
# Before
## 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
@app.consume("emails")
def send_email(job):
to = job.data["to"]
# After
## 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
@app.consume("emails")
def send_email(job):
to = job.payload["to"]File upload property rename (v3.3.0). file_name became filename (no underscore). The old name is no longer available.
# Before
## 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
name = request.files[0].file_name
# After
## 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
name = request.files[0].filenameRoute params merged into request.params (v3.3.0). Path parameters now merge into request.params alongside query parameters. If a query parameter shares a name with a path parameter, the path parameter wins.
v3.2.x — Flexible Route Handlers and DevReload (March 24, 2026)
Features
Flexible handler signatures (v3.2.0). Route handlers accept any combination of parameters. The framework inspects the signature and injects what you ask for.
# No parameters — fire and forget
## 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
@app.get("/ping")
def ping():
return "pong"
# 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
@app.get("/hello")
def hello(response):
return response("Hello")
# Request only (type-hinted)
## 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
@app.post("/echo")
def echo(request: Request):
return request.body
# Both
## 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
@app.get("/users")
def users(request, response):
return response({"users": []})DevReload with SCSS compilation (v3.2.0). The development server watches for file changes and reloads the browser. SCSS files compile on change.
Route groups (v3.2.0). Group routes under a shared prefix with shared middleware.
MongoDB queue backend (v3.2.0). Use MongoDB as a queue backend alongside the existing file-based, RabbitMQ, and Kafka options.
Migration naming convention (v3.2.0). Migration files follow the YYYYMMDDHHMMSS timestamp format. The tina4 migrate --status command shows which migrations have run.
Auto-increment port (v3.2.0). If the default port is in use, the framework picks the next available port and opens your browser.
Breaking Changes
Queue constructor simplified (v3.2.0). The db parameter was removed from the Queue constructor. The queue reads its backend from the environment.
# Before
## 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
from tina4_python import Queue, Database
db = Database("sqlite:///queue.db")
queue = Queue("emails", db=db)
# After
## 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
from tina4_python import Queue
queue = Queue("emails")
# Backend set via TINA4_QUEUE_BACKEND env var
## 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.jsProducer/Consumer classes removed (v3.2.0). Use queue.produce() and queue.consume() directly.
# Before
## 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
from tina4_python import Producer, Consumer
producer = Producer(queue)
producer.send({"to": "user@example.com"})
# After
## 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({"to": "user@example.com"})v3.1.x — Benchmarks and Internal Improvements (March 22, 2026)
Features
Automated benchmark suite (v3.1.0). A reproducible benchmark compares Tina4 Python against 17 frameworks across four languages. Run it yourself with python benchmarks/run.py.
No user-facing API changes in this release. Internal improvements to test infrastructure and benchmark tooling.
v3.0.0 — The Rewrite (March 22, 2026)
The v3.0.0 release replaced the entire v2 codebase. Zero external dependencies. Pure Python standard library. 38 features. Over 6,000 tests.
What Changed
- New module structure.
tina4_python.corereplaces the old flat namespace. - Frond template engine. Built-in Twig-compatible templates replace the Jinja2 dependency. Pre-compilation caches tokens for a 2.8x speedup on file renders.
- Decorator-based routing.
@app.get,@app.post,@app.put,@app.delete,@app.patchreplace the oldRouteclass. - Built-in Dev Admin. A browser-based dashboard shows routes, database tables, and queue status.
- Error overlay in debug mode. Stack traces render in the browser with source context, request details, and suggested fixes.
- Swagger auto-registration. Decorated routes appear in the Swagger UI without manual annotation.
- Unified queue system. Switch between file-based, RabbitMQ, and Kafka backends through environment variables. No code changes.
- Database query caching. Set
TINA4_DB_CACHE=truefor transparent caching with a 4x speedup on repeated queries. tina4 generatescaffolding. Generate models, routes, migrations, and middleware from the command line.- Custom error pages. Self-contained 404, 403, and 500 pages with clean, framework-neutral design.
For the full migration guide, see Chapter 36: Upgrading from v2 to v3.
Release Candidate History
Five release candidates preceded the v3.0.0 stable release between March 21-22, 2026. They resolved test failures, polished the Dev Admin UI, added the benchmark suite, and stabilized the Frond template engine. If you tested a release candidate, upgrade to v3.0.0 or later. The RC builds are not supported.