Operations
Running Thallo in production
What a production site needs: the web server, the scheduler, the queue, and what each capability adds.
Everything on this page is stated once, per the capability that creates the obligation. A tier-1 install (the fresh default) needs only the Core rows.
Core (every install)
| Obligation | Required? | Detail |
|---|---|---|
BASE_URL set to the canonical public origin |
Required | Every absolute URL derives from it, never the Host header. HTTPS with no non-default port if you will ever mint payment links. |
| Scheduler cron | Required | * * * * * php /path/to/site/glueful queue:scheduler run — the one tick that evaluates every job in config/schedule.php: scheduled publish/unpublish, the daily update check, the signup and domain-reverification sweeps. Nothing in that file runs without it; queue workers do not tick it. (thallo:schedules:run alone, as earlier guides said, fires only scheduled publishing.) Utilities → Health shows a Scheduler check: ok while the tick is recent, a warning with this line when it is stale or has never happened. |
APP_ENV=production (+ real APP_KEY/JWT_KEY) |
Required | The shipped .env.example is already production (debug off); keep it that way. The API reference stays on unless API_DOCS_ENABLED=false, and HTTPS is the web server's to enforce — Thallo redirects nothing (security) and let thallo:provision generate the keys. thallo:doctor warns if a public BASE_URL runs in development mode. |
Extension cache present (php glueful extensions:cache) |
Required | Production boot refuses live extension discovery. composer create-project and thallo:provision build the cache; rebuild it after every composer update and every extension enable/disable done outside the admin (the admin toggle rebuilds it itself). |
Upgrade with composer update && php glueful thallo:provision |
Required | Thallo is glueful/thallo-core in vendor/ since beta.21; provision publishes the release's admin bundle and adopts the migration ledger. Installs created before beta.21 move once to the template (see upgrading). |
Cache driver with tag invalidation (CACHE_DRIVER=redis) |
Recommended | Rendered pages are purged by surrogate tag on every content change. On the default file driver, which cannot invalidate tags, every content change drops every rendered page instead — always fresh, coarser on a busy site. |
| Clear the route table and rendered pages on every deploy | Required | thallo:provision drops both (the compiled route table — a stale one keeps serving the previous release's routes — and the rendered pages); a deploy that skips provision runs php glueful route:cache:clear && php glueful render:cache:clear itself. The compiled container is signed and recompiles itself. See upgrading. |
| Queue workers | Recommended | Background jobs (mail, maintenance) degrade gracefully without them, but production should run the queue (php glueful queue:work; presets in .env). |
Backups (database + storage/) |
Required | Media, uploads, caches and the database carry all state. |
| HTTPS + proxy configuration | Required | Terminate TLS in front of PHP; forward proto/host headers correctly. |
| PHP-served paths reach PHP | Required | /theme-assets/*, /_thallo/* and the API under /v1/* are generated by Thallo, not files on disk. A static-file rule that serves every .css/.js/.json URL from the document root answers them 404 (the site loads unstyled) and the API's file-shaped paths 405 (saving the site's custom CSS fails). See PHP-served asset paths (web server) for the nginx block. thallo:provision / thallo:doctor warn (asset-routing) when a public BASE_URL answers 404 for one. |
zend.exception_ignore_args=On (php.ini) |
Recommended | Keeps sensitive values out of logged stack-trace arguments. |
logging.sensitive_paths |
Recommended | Committed defaults cover the payment-link paths; if you mount the app under a base path, register prefixed templates too. Reverse-proxy/CDN access logs are outside the app — see the redaction recipes in packages/thallo-commerce/README.md. |
| Signup cleanup / domain reverification / update check | Via the scheduler cron | Jobs in config/schedule.php, run by the scheduler cron above — no extra cron entries. |
Public-site browser floor
The public site's stylesheets use cascade layers, revert-layer and color-mix():
the floor is Chrome 111, Firefox 113 and Safari 16.2 (and their derivatives).
Older browsers get the theme's own CSS without block settings applied. The tested
matrix is the current stable engine of each family that the pinned Playwright ships
(tools/style-proofs, run in CI on every render change).
Upgrading across a settings conversion (the cutover contract)
A release that retires block presentation fields ships a conversion stage: the converter
(thallo:blocks:convert-settings) turns the retired fields of every stored document — drafts,
every retained version, the regions — into typed settings, and provision runs it only when its
preflight is clean; it never bypasses an unresolved decision and never activates incompatible
code after a partial run. No stage ships today (every install is authored in the settings
shape), so provision's conversion step finds nothing pending; the contract below applies the
first time a release adds one.
- Stage the candidate release and verify a restorable backup (database and
storage/). - Preflight content with the candidate converter:
php glueful thallo:blocks:convert-settings --dry-run --report=storage/conversion/report.jsonl. The report is one JSON line per legacy value with its status;unmappablelines (raw hex colours, pixel sizes) need a decision. - Record every decision in
storage/conversion/decisions.json, keyed as the report names the diagnostic and pinned to itsdocument_hashandconverter_version(choose a vocabulary token, a typed value, or discard). A decision is invalid once its document changes. - Enter maintenance or write protection; run the dry run again and confirm nothing is unresolved.
composer update && php glueful thallo:provision: provision converts, compiles the style artifact, clears caches, and stops before any of that with the report path when a diagnostic is unresolved or a document changed underneath it. Reopen writes.
Recovery after a partial conversion is restore from backup; the converter is idempotent per stage, so a retry after an interruption lands on the same state, but idempotence does not replace rollback. A fresh install is the trivial case of this contract.
Running the scheduler and the queue
Two long-lived pieces, both plain PHP; nothing else to install with the default
QUEUE_CONNECTION=database (its tables are created by provision).
The scheduler tick evaluates config/schedule.php and runs the due jobs inside the tick:
* * * * * php /path/to/site/glueful queue:scheduler run >> /path/to/site/storage/logs/scheduler.log 2>&1 A queue worker processes jobs the application dispatches (mail, extension operations,
maintenance, block-type schema backfills, and a style class's detach-everywhere or
remove-everywhere job — the class stays locked until that job completes, so without a worker
run it by hand with php glueful thallo:style-classes:run-job <id>). It exits on its memory and
job limits, so keep it under a supervisor. A systemd unit:
[Unit]
Description=Thallo queue worker
After=network.target postgresql.service
[Service]
User=deploy
WorkingDirectory=/path/to/site
ExecStart=/usr/bin/php glueful queue:work --queue=default,webhooks,import-export,tenancy-maintenance --sleep=3 --tries=3 --max-runtime=3600
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target A worker runs only the queues it is given. Thallo's own jobs go to default; imports and exports
(Settings › Import / Export) go to import-export, so an import stays "queued" on a worker that
was not given that one; Run now under Utilities › Scheduled Tasks puts a task on its own queue
(maintenance for most). With workspaces on, tenancy adds tenancy-purge and
tenancy-maintenance. The scheduler and the queue lists
every queue and what arrives on it. Add a queue name to --queue if an extension you enable
documents its own. Sizing presets (*_QUEUE_MEMORY, *_QUEUE_TIMEOUT,
*_QUEUE_MAX_JOBS) live in .env. A site that cannot keep a worker under a supervisor can
drain the queue from a second cron line instead (queue:work --stop-when-empty); the same page
gives the line and its limits. There is no inline connection: only the database and redis
drivers exist. Redis (QUEUE_CONNECTION=redis plus REDIS_*) is optional and only worth it
under real load.
PHP-served asset paths (web server)
Only public/admin/* (the admin bundle, published there by thallo:provision from
core/resources/admin) is real files; uploads live outside the document root at
storage/uploads/ and are served at /v1/blobs/{uuid}. Three URL
prefixes are generated by PHP on every request and never exist on disk:
| Prefix | What it serves |
|---|---|
/theme-assets/* |
The active theme's stylesheets and fonts (site.css, blocks.css, fonts/…), switchable from Settings without a restart |
/_thallo/* |
The theme runtime (/_thallo/runtime/runtime.js), the site's custom stylesheet (/_thallo/custom.css, saved from the theme editor) and the designer's canvas support assets (/_thallo/preview.css, /_thallo/preview-bridge.js) |
/v1/*, /api-docs/* |
The API and its reference. Some API paths end like files — /v1/admin/render/templates/custom.css is the site's custom stylesheet as a template — and a static rule answers them 404 on GET and 405 on PUT |
Most nginx templates (CloudPanel's included) carry a "static files" location that matches any URL
ending in .css, .js, .woff2, … and serves it straight from the document root. nginx picks the
first regex location that matches, in file order, so that rule takes these URLs before
location / ever sees them and answers 404: the rendered site loads unstyled and the designer
loses its stylesheet and bridge.
Add a location for these prefixes above the static-file location, with the same body as
location /. CloudPanel (nginx in front of Varnish in front of PHP-FPM):
location / {
{{varnish_proxy_pass}}
...
}
# Thallo: /theme-assets/*, /_thallo/*, /v1/* and /api-docs/* are generated by PHP, not files on
# disk. Must precede the static-extension location below (first matching regex wins).
location ~ ^/(theme-assets|_thallo|v1|api-docs)/ {
{{varnish_proxy_pass}}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_hide_header X-Varnish;
proxy_redirect off;
proxy_max_temp_file_size 0;
proxy_connect_timeout 720;
proxy_send_timeout 720;
proxy_read_timeout 720;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
proxy_temp_file_write_size 256k;
}
location ~* ^.+\.(css|js|jpg|jpeg|gif|png|ico|gz|svg|svgz|ttf|otf|woff|woff2|eot|mp4|ogg|ogv|webm|webp|zip|swf|map|mjs)$ {
expires max;
...
} Plain nginx + PHP-FPM (no Varnish): the same location, handing the request to the front controller:
location ~ ^/(theme-assets|_thallo)/ {
try_files /index.php?$args =404;
} Apache with mod_rewrite needs nothing extra as long as the standard front-controller rewrite
(RewriteCond %{REQUEST_FILENAME} !-f → index.php) is in place; only a rule that short-circuits
by extension has to exclude the two prefixes.
Verify from the server (a CDN or Varnish may hold an earlier 404 — purge if the browser disagrees):
curl -sI https://example.com/theme-assets/site.css?t=default | head -1 # HTTP/2 200
curl -sI https://example.com/_thallo/preview.css | head -1 # HTTP/2 200 php glueful thallo:provision and php glueful thallo:doctor run this probe on a public
BASE_URL and report asset-routing as a warning when the answer is 404.
Email (any capability that sends mail)
| Obligation | Required? | Detail |
|---|---|---|
| SMTP / mail transport configured | Required for mail features | Forgot-password, notifications, payment-request email. |
| Rich notification channel present | Required for payment-request email | Ships via the bundled EmailNotification extension; the "send payment link" email refuses cleanly (never crashes) without it. |
Commerce (after extensions:enable Commerce)
| Obligation | Required? | Detail |
|---|---|---|
| Orders expiry sweep | Required | Cron php glueful commerce:orders:expire (e.g. every 15 minutes). Cancels stale storefront orders and stale drafts, and hard-deletes canceled draft artifacts older than commerce.orders.draft_purge_days (default 30, clamp 1–365 — no disable value; raise the window if you have retention requirements). |
| Stock / catalog reindex jobs | Automatic | Ride the queue when workers run. |
Payments (after extensions:enable Payvia)
| Obligation | Required? | Detail |
|---|---|---|
| Gateway credentials | Required to charge | Settings → Payments (runtime-editable, encrypted, write-only) or .env. Keyless installs degrade to manual collection — nothing breaks. |
| Stale-intent sweep | Required | Cron php glueful payvia:intents:sweep-stale (daily is fine; on multi-workspace installs loop --tenant or drive it via your tenancy scheduler). Frees abandoned payment attempts after payvia.intents.stale_after_days (default 30). |
| Webhook endpoint reachable over HTTPS | Required | Provider webhooks settle payment-link orders; without them, orders await manual confirmation. |
Paystack: integration payment_session_timeout = 0 |
Required if using Paystack | A non-zero value silently dead-ends resumed checkouts. Set it in your Paystack dashboard. |
| Payment-link minting origin | — | Requires BASE_URL to be canonical HTTPS with no non-default port (see Core). |
Workspaces / multi-tenancy (after enabling via Settings → Workspaces)
| Obligation | Required? | Detail |
|---|---|---|
| Enforcement lifecycle via Settings → Workspaces only | Required | Never toggle the tenancy enforcement provider by hand — the enablement flow owns it (begin → widen → confirm → finalize). |
| Tenant-scoped sweeps | Required | The payvia sweep (above) needs the per-tenant loop; core sweeps handle tenancy automatically. |
Monitoring worth having (any install taking money)
Alert on: failed/undelivered provider webhooks, payment_late_rejected audit entries
(Admin → Audit, category security — a refused late or duplicate payment may need a refund),
mail failures, queue backlog, and 5xx rates.