Chapter 2: Architecture#
The Request Lifecycle#
A request arrives. Seven stages later, a response leaves. Every Tina4 application follows this path -- Python, PHP, Ruby, Node.js. The language changes. The architecture does not.
Client sends HTTP request β βΌ βββββββββββββ β Server β Accept connection, parse HTTP βββββββ¬ββββββ β βΌ βββββββββββββ β Request β Build Request object (body, params, headers, files, session) βββββββ¬ββββββ β βΌ βββββββββββββ β Router β Match URL pattern to a registered handler βββββββ¬ββββββ β βΌ βββββββββββββ βMiddleware β Run before-handler functions (auth, logging, rate limit) βββββββ¬ββββββ β βΌ βββββββββββββ β Handler β Your code runs here βββββββ¬ββββββ β βΌ βββββββββββββ β Response β Build response (JSON, HTML, redirect, file) βββββββ¬ββββββ β βΌ βββββββββββββ β Pipeline β Minify HTML, compact JSON, compress (gzip), set ETag βββββββ¬ββββββ β βΌ Client receives HTTP responseLearn this diagram. Everything else in Tina4 is a footnote to it.
1. Server#
The server accepts TCP connections and parses raw HTTP into structured data. Each language uses its native server:
| Language | Server |
|---|---|
| Python | asyncio / ASGI |
| PHP | Built-in server / Swoole |
| Ruby | WEBrick / Puma |
| Node.js | node:http |
You never touch this layer. Tina4 owns it. The server starts, listens, and hands off parsed requests. Your code lives further down the chain.
2. Request#
The framework assembles a Request object. Everything the handler needs is already unpacked and waiting:
request.body # Parsed JSON or form datarequest.params # Path parameters ({id} from /users/{id})request.query # Query string (?page=2&sort=name)request.headers # HTTP headers (case-insensitive)request.files # Uploaded filesrequest.session # Session data (read/write)request.method # GET, POST, PUT, DELETE, etc.request.path # URL path without query stringrequest.ip # Client IP addressrequest.request_id # Unique ID for this request (for log correlation)request.cookies # Parsed cookiesrequest.is_json # True if Content-Type contains "json"No parsing. No extraction. No guessing where the data lives. One object. Twelve properties. Everything accounted for.
3. Router#
The router listens. A request arrives. Method and path are matched against registered routes. Routes live in files under src/routes/:
// PHPRouter::get("/api/products/{id:int}", function ($request, $response) { // Only matches if {id} is an integer});# Python@get("/api/products/{id:int}")async def get_product(request, response): # Only matches if {id} is an integer pass# Rubyget "/api/products/{id:int}" do |request, response| # Only matches if {id} is an integerend// Node.js (file-based routing: src/routes/api/products/[id].ts)export default function handler(request, response) { // id available via request.params.id}The router supports:
- Basic parameters:
/users/{id}matches/users/42 - Typed parameters:
/users/{id:int}only matches integers - Catch-all:
/pages/{slug:.*}matches/pages/about/team/history - Route groups: Prefix multiple routes with a common path and middleware
- Route caching: Cache the response for a given TTL
No match on a registered route? The router checks src/public/ for a static file. Still nothing? A 404 goes back to the client.
4. Middleware#
Middleware functions form a pipeline. Each one inspects the request, decides whether to pass it forward, and optionally modifies what comes back. Think of it as a series of gates. A request must pass through every gate before it reaches your handler.
Tina4 ships four built-in middleware:
- CORS -- configured via environment variables, runs on every request
- Rate limiting -- 60 requests per minute per IP, out of the box
- Auth gating -- attach
.secure()to a route to demand a valid JWT - Request ID tracking -- generates or reads the
X-Request-IDheader
You write your own the same way:
// PHPfunction logRequests($request, $response, $next) { $start = microtime(true); $result = $next($request, $response); $duration = round((microtime(true) - $start) * 1000, 2); Log::info("Request completed", [ "method" => $request->method, "path" => $request->path, "duration_ms" => $duration ]); return $result;}βRouter::get("/api/users", $handler)->middleware([logRequests]);A middleware receives the request, calls $next to continue the chain, and returns the result. Skip the $next call and the request stops right there. Short-circuit. The handler never runs. This is how auth guards work -- no valid token, no entry.
5. Handler#
This is your territory. The handler receives a Request and a Response. What happens in between is your decision. Tina4 does not impose an application architecture. No base controllers. No service containers. No required inheritance. You receive two objects. You return a response.
6. Response#
The Response object covers every common output:
response.json(data, statusCode) # JSON with auto Content-Typeresponse.html(content, statusCode) # HTML responseresponse.text(content, statusCode) # Plain textresponse.xml(content, statusCode) # XML responseresponse.redirect(url, statusCode) # HTTP redirect (302 or 301)response.file(path) # File download with auto MIME typeresponse.render(template, data) # Render a Frond templateresponse.status(code) # Set status code (chainable)response.header(name, value) # Set response header (chainable)response.cookie(name, value, options) # Set a cookieTen methods. JSON, HTML, text, XML, redirects, files, templates, status codes, headers, cookies. Pick the one that fits. Chain what needs chaining.
7. Response Pipeline#
Your handler finishes. The response is not done yet. It passes through an automatic pipeline -- five stages that optimize every response without a single line of configuration:
- Frond rendering -- if you called
response.render(), the template compiles and executes - HTML minification -- in production (
TINA4_DEBUG=false), whitespace collapses, comments vanish. 15-25% smaller output. - JSON compaction -- JSON ships compact. Add
?pretty=trueto the query string during development for readable output. - gzip compression -- the client sends
Accept-Encoding: gzipand the response exceeds 1KB? Compressed. - ETag generation -- a hash of the response body becomes an
ETagheader. The next request with a matchingIf-None-Matchgets a304 Not Modified. Zero bytes transferred.
Five optimizations. Zero configuration. Every response benefits.
Project Structure#
Every Tina4 project follows the same directory layout. Python, PHP, Ruby, Node.js -- the folders are identical. A developer who has seen one Tina4 project has seen them all.
my-project/βββ .env # All configuration lives hereβββ src/β βββ routes/ # Route handlers (auto-discovered)β βββ orm/ # ORM models (auto-discovered)β βββ migrations/ # SQL migration filesβ βββ seeds/ # Database seed filesβ βββ templates/ # Frond templatesβ β βββ errors/ # Custom error pagesβ βββ public/ # Static files (served at /)β β βββ js/β β β βββ frond.js # Auto-providedβ β βββ css/β β βββ scss/ # SCSS source files (auto-compiled)β β βββ images/β β βββ icons/β βββ locales/ # Translation files (JSON)β βββ en.jsonβββ data/ # SQLite databases, .broken filesβββ logs/ # Log files with rotationβββ secrets/ # JWT keysβββ tests/ # Test filesFourteen directories. Each one has a single purpose. No overlap. No ambiguity.
src/routes/ -- Where Your API Lives#
Drop a file here. Tina4 finds the route definitions inside it. Organize however you want:
src/routes/βββ products.php # All product routesβββ orders.php # All order routesβββ admin/ βββ users.php # Admin user routes βββ reports.php # Admin report routesOne file or twenty. Nested folders or flat. Tina4 reads them all. The file name does not affect the route path. Only the route definition inside the file matters. Name it products.php or banana.php -- the URL comes from the code, not the filename.
src/orm/ -- Where Your Models Live#
ORM model classes go here. Auto-discovered on startup:
src/orm/βββ Product.phpβββ Order.phpβββ OrderItem.phpβββ User.phpDrop a class that extends the ORM base. Tina4 registers it. Auto-CRUD endpoints, route model binding, relationship resolution -- all of it flows from discovery.
src/migrations/ -- Database Schema Changes#
Migrations are SQL files with timestamps:
src/migrations/βββ 20260319100000_create_users_table.sqlβββ 20260319100000_create_users_table.down.sqlβββ 20260320090000_create_products_table.sqlβββ 20260320090000_create_products_table.down.sqlThe .sql file runs on tina4 migrate. The .down.sql file runs on tina4 migrate --rollback. Tina4 tracks which migrations have run in a tina4_migrations table. Forward and back. Always reversible.
src/seeds/ -- Test Data#
Seed files populate your database with test or default data. Run them with tina4 seed. Fifty built-in fake data generators handle the rest -- names, emails, addresses, phone numbers, dates.
src/templates/ -- Frond Templates#
Templates use the Frond engine. Inheritance, includes, filters, loops, conditionals -- all covered in detail later. The structure is yours to decide:
src/templates/βββ base.html # Layout with blocksβββ index.html # Extends base.htmlβββ products/β βββ list.html # Product listingβ βββ detail.html # Single productβββ partials/β βββ header.htmlβ βββ footer.htmlβββ errors/ βββ 404.html # Custom 404 page βββ 500.html # Custom 500 pagesrc/public/ -- Static Files#
Files here are served at the root URL path. A file at src/public/images/logo.png appears at /images/logo.png. No route registration. No configuration. Drop it in. It serves.
The framework auto-provides frond.js in src/public/js/ and keeps it in sync with the installed framework version. You never manage this file.
data/ -- Runtime Data#
SQLite databases live here by default (data/app.db). The .broken/ subdirectory holds error marker files used by the health check. This entire directory belongs in .gitignore. Runtime data stays on the machine that runs the application.
logs/ -- Log Files#
Structured log files with automatic rotation. In .gitignore. Compressed after two days. Deleted after thirty. The framework handles all of it.
secrets/ -- JWT Keys#
Private and public keys for RS256 JWT signing. In .gitignore. Generated once, deployed with your application, never committed to source control.
.env Driven Configuration#
One file controls everything. Not YAML. Not TOML. Not JSON config objects. A .env file at the project root. Key-value pairs. Plain text.
# .envTINA4_DEBUG=trueTINA4_PORT=7145TINA4_DATABASE_URL=sqlite:///data/app.dbJWT_SECRET=change-me-in-productionFour lines. A working application.
The Priority Chain#
Tina4 resolves every configuration value through a three-level chain:
Constructor argument > .env file > Hardcoded defaultThe constructor wins. Always. The .env file is second. The hardcoded default is the safety net. This pattern applies to every configurable value in the framework. No exceptions.
Example: Priority Chain in Practice#
# .envTINA4_PORT=8080// In code -- this overrides .env$app = new Tina4\App(["port" => 9000]);// Server starts on port 9000, not 8080// No code override, no .env value$app = new Tina4\App();// Server starts on port 7145 (the default)Three levels. Predictable resolution. Every time.
is_truthy() -- Boolean Environment Values#
Environment variables are strings. Booleans do not exist in .env files. Tina4 bridges this gap with is_truthy(). Four values mean true:
true(any case:True,TRUE,tRuE)1yes(any case)on(any case)
Everything else is false. Empty strings. Unset variables. Typos. If it is not on the list, it is false.
TINA4_DEBUG=trueTINA4_DEBUG=TrueTINA4_DEBUG=1TINA4_DEBUG=yesTINA4_DEBUG=onAll equivalent. All enable debug mode.
TINA4_DEBUG=falseTINA4_DEBUG=0TINA4_DEBUG=noTINA4_DEBUG=offTINA4_DEBUG=# or simply omit the lineAll disable it. No ambiguity.
Dev Mode vs. Production Mode#
One variable. Two personalities. TINA4_DEBUG controls everything.
Dev Mode (TINA4_DEBUG=true)#
The framework opens up. Every diagnostic tool activates:
- Debug overlay injects into every HTML response -- a toolbar showing request details, database queries, template render times, session data, and logs
- Full stack traces appear in the browser with source code context, the triggering request, and the queries that ran
- Swagger UI auto-registers at
/swagger - Admin console becomes available at
/__dev - Live reload watches for file changes and refreshes the browser
- SQL query logging writes every query to
logs/query.log - Pretty JSON is available via
?pretty=trueon any JSON endpoint - 404 pages show helpful route-not-found messages listing similar registered routes
Development mode assumes you want to see everything. It shows you everything.
Production Mode (TINA4_DEBUG=false)#
The framework locks down. Every diagnostic tool disappears:
- No debug overlay -- responses ship clean
- Generic error pages -- no stack traces, no source code, no query details reach the browser
- HTML minification -- comments stripped, whitespace collapsed, 15-25% smaller output
- .broken files -- unhandled exceptions create marker files in
data/.broken/that flip the health check to503 Service Unavailable, triggering container restarts - No Swagger UI -- unless you force it on
- No dev dashboard -- only available when
TINA4_DEBUG=true - No query logging -- unless you enable it
- Compact JSON only -- no
?pretty=true
Production mode assumes you want to expose nothing. It exposes nothing.
The default is TINA4_DEBUG=false. Forget to set it? The safe thing happens. Your application starts locked down.
One mistake will undo all of this: deploying with TINA4_DEBUG=true. Stack traces, database queries, session data -- visible to anyone with a browser. Set TINA4_DEBUG=false in production. Always.
The Frond Template Engine#
Frond is Tina4's template engine. Zero dependencies. Built from scratch in each language. The syntax borrows from Twig -- developers who know Twig, Jinja2, or Nunjucks will recognize every construct. But no third-party template library runs underneath. Frond is Tina4's own.
Basic Syntax#
Variables:
<h1>{{ title }}</h1><p>Welcome, {{ user.name }}</p><p>First item: {{ items[0] }}</p>Filters (pipe syntax):
<p>{{ name | upper }}</p> <!-- JOHN DOE --><p>{{ name | lower }}</p> <!-- john doe --><p>{{ price | number_format(2) }}</p> <!-- 29.99 --><p>{{ text | truncate(100) }}</p> <!-- First 100 chars... --><p>{{ description | raw }}</p> <!-- No auto-escaping --><p>{{ items | length }}</p> <!-- 5 --><p>{{ items | join(", ") }}</p> <!-- apple, banana, cherry -->Fifty-five filters. Strings, numbers, dates, arrays, encoding, formatting. If you need to transform data in a template, a filter exists.
Control structures:
{% if products | length > 0 %} {% for product in products %} <div class="product"> <h2>{{ product.name }}</h2> <p>{{ product.price | number_format(2) }}</p> {% if loop.last %} <hr> {% endif %} </div> {% else %} <p>No products found.</p> {% endfor %}{% endif %}Template inheritance:
{# base.html #}<!DOCTYPE html><html><head> <title>{% block title %}My App{% endblock %}</title></head><body> {% block content %}{% endblock %}</body></html> {# products/list.html #}{% extends "base.html" %} {% block title %}Products{% endblock %} {% block content %} <h1>Our Products</h1> {% for product in products %} <p>{{ product.name }} - ${{ product.price | number_format(2) }}</p> {% endfor %}{% endblock %}Includes:
{% include "partials/header.html" %}<main> {{ content }}</main>{% include "partials/footer.html" %}Fragment caching:
{% cache "product-list" 300 %} {# This block is cached for 300 seconds #} {% for product in products %} <div>{{ product.name }}</div> {% endfor %}{% endcache %}One fact matters above all others: the template syntax is identical across all four languages. A template written for a Python backend works on PHP, Ruby, and Node.js without a single change. The backend is invisible to the frontend. Frond guarantees that.
How Auto-Discovery Works#
Tina4 finds your code without being told where to look. No registration files. No import chains. No bootstrapping rituals. Drop files in the right directories. The framework discovers them.
Routes#
On startup, Tina4 scans every file in src/routes/ -- recursively, through every subdirectory. It finds route registration calls: get(), post(), put(), delete(), any(). Each call registers a route with the router.
Startup β βββ Scan src/routes/ β βββ products.php β registers GET /api/products, POST /api/products, ... β βββ orders.php β registers GET /api/orders, ... β βββ admin/users.php β registers GET /api/admin/users, ... β βββ Router now has all routes in memoryThe scan happens once at startup. In dev mode with live reload, the scan re-runs when files change. New route file saved? The router rebuilds. No restart needed.
The file name and path within src/routes/ do not determine the URL. Only the route definition inside the file matters. Put all your routes in one file called everything.php and it works. Spread them across fifty files in nested folders and it works. The directory structure is for your organization. The framework ignores it.
Models#
ORM model classes in src/orm/ follow the same pattern. The framework scans for classes that extend the ORM base class and registers them. This powers auto-CRUD -- REST endpoints generated from models. It powers route model binding -- URL parameters resolved to model instances. Discovery makes both possible without a single line of registration code.
Templates#
Templates work differently. They are not discovered at startup -- they are loaded on demand when response.render() is called. But the framework knows where to look without being told. Reference "products/list.html" and Tina4 resolves it to src/templates/products/list.html. No path configuration. No template registry.
Static Files#
When the router cannot match a request to a registered route, it falls back to the filesystem. The lookup follows a strict order:
1. Registered route? β Run handler2. File in src/public/? β Serve static file3. Framework built-in asset? β Serve framework file4. Nothing matches β 404 responseFour steps. Tried in order. The first match wins. A file at src/public/css/style.css serves at /css/style.css with the correct MIME type. No route needed. No configuration needed. The file exists, so it serves.
Summary#
| Concept | How It Works in Tina4 |
|---|---|
| Request lifecycle | Request > Router > Middleware > Handler > Response > Pipeline |
| Project structure | Fixed conventions: routes, orm, templates, public, migrations |
| Configuration | .env only, priority: constructor > .env > default |
| Dev vs. production | Single toggle: TINA4_DEBUG |
| Template engine | Frond: Twig-like syntax, zero dependencies, identical across languages |
| Auto-discovery | Files in src/routes/ and src/orm/ are found at startup |
| Static files | Files in src/public/ are served at / |
| Response pipeline | Minification > compression > ETag, all automatic |