A framework's defaults optimize for the first ten minutes: install it, log in, see something work. Those defaults are chosen for a laptop on a private network with one trusted user — you.

Production has different requirements, and the gap between the two is not a flaw in the framework. It is a step in the process, and the only real failure is not knowing the step exists. This is that step, made explicit.

The five that actually matter

If you do nothing else on this page, do these.

1. Set (or confirm) the admin password

If you installed with install.sh, you were already prompted for an admin password (or given a random one in --auto mode) — so this is a confirmation step, not a fix. The well-known admin123 only exists if you ran database/init.php by hand and skipped the installer; on that path, change it before the host is reachable. Either way, verify you cannot log in with admin/admin123 before you go live.

2. Turn off build mode

[app]
build_mode = false

Build mode auto-creates a permission row for any route that lacks one — and grants access. On a public host that is a system that permits whatever it has not yet been told to refuse. It is a development accelerator; it is not a deployment setting.

3. Audit the permission table

This is the one people skip, and it is the most important. A controller::method pair with no authcontrol row defaults to public access. A missing row is not a locked door.

-- What can an anonymous visitor reach?
SELECT control, method, level FROM authcontrol WHERE level >= 101 ORDER BY control;

-- Which controllers have no wildcard fallback at all?
SELECT DISTINCT control FROM authcontrol WHERE method = '*';

Cross-reference the second query against your controls/ directory. Every controller should have an explicit wildcard, so that a method someone adds later inherits a sane default instead of falling through to public. Then re-check after every deploy that adds routes.

4. Session cookie security comes from environment

[app]
environment = "production"

This is the same switch as item 5, and it does double duty. Tiknix hardens the session cookie in code at startup — HttpOnly and SameSite=Lax are always on — and marks the cookie Secure automatically when environment = production. There is no separate secure flag to flip. Get environment right and the HTTPS-only cookie follows; leave it non-production behind HTTPS and your session cookie will happily travel over any plain-HTTP request. Consider SameSite=Strict if you have no cross-site flows.

5. Turn off debug output

[app]
environment = "production"
debug       = false

[logging]
level = "INFO"      ; DEBUG in production is a disk-space and privacy problem

Debug mode renders stack traces to the browser. Stack traces contain file paths, class names, query fragments, and occasionally values — a free architecture diagram for anyone probing your app.

Then these

Protect the config file

conf/config.ini holds your database credentials, your app_key, your mail credentials, and your sidecar SSO secrets. It must be out of version control, unreadable by the web server as a static file, and backed up somewhere you can find in an emergency. Losing app_key means every encrypted connection token is permanently unrecoverable.

Freeze the schema

RedBeanPHP's fluid mode is a development feature. Frozen, a typo throws instead of silently creating a staus column at 2am. Freeze in production and apply schema changes deliberately as part of a deploy.

Decide your 2FA policy

[security]
two_factor_enabled = true
two_factor_enforce = true      ; or false for optional during rollout

If you have admin accounts on a public host, enforce it. If you need a gentler rollout, run optional for a couple of weeks — users are prompted and may skip — then enforce. Write down your account-recovery procedure before you enforce, because lost-authenticator tickets start the same day.

File permissions and ownership

chmod -R 755 .
chmod -R 775 log/ cache/ uploads/     # prefer group-writable over 777
chown -R www-data:www-data .

The install docs suggest 777 on writable directories to get people running quickly. On a shared or public host, prefer 775 with correct group ownership — world-writable directories are a local privilege-escalation gift.

Separately: uploads/ must never execute. If a user can upload a .php file and then request it, you have handed over the server. Serve uploads from a location with script execution disabled, and validate types on the way in.

Only public/ is the web root

The document root points at public/. If it points at the project root instead, your configuration, logs, and database file are all fetchable over HTTP. Verify by requesting /conf/config.ini and /database/tiknix.db and confirming both 404.

Rate limiting at the edge

The built-in limiter is session-backed: it stops an ordinary attacker in a browser and a naive script that keeps cookies. It does not stop a distributed attempt where each client starts fresh. Add limits at the reverse proxy, a WAF, or fail2ban against the access log.

Remove or lock down development surfaces

The Test and Demo controllers exist for development. Give them explicit authcontrol rows at ROOT level or delete them. Same for any scaffold or install route that should not exist post-setup.

Backups you have actually restored

With SQLite this is unusually easy — the database is one file — which is precisely why it is easy to skip. Use sqlite3 .backup or the backup API for a consistent copy while writes are in flight, store copies off the machine, and restore one into a scratch environment at least once. An untested backup is a belief, not a backup.

Know where the logs are and read them

Monolog writes to log/ with daily rotation and 30-day retention. The dispatcher logs permission denials, failed logins, controller errors, and cache behavior. That is a genuinely useful stream — but only if something surfaces it. Ship logs somewhere with alerting, or at minimum set a recurring reminder to read them.

A deployment discipline, not a deployment

PHP's "copy the files" model is a strength right up until two people copy different files. Deploy from Git, tag what you ship, and keep the production checkout clean so git status tells you the truth. The ability to hotfix a file on the server is real and occasionally necessary — treat every use of it as an incident to reconcile, not a workflow.

Honest notes
  • This list is not exhaustive and is not a security audit. It is the framework-specific set. TLS configuration, OS patching, SSH hardening, firewall rules, and dependency updates are still yours, and no framework checklist covers them.
  • The permission-table audit is the highest-value item here. Public-by-default for unknown routes is the single behavior most likely to expose something you did not intend. Automate the check if you can — a test that fails when a controller has no wildcard row is thirty lines and pays for itself.
  • Defaults drift back. A fresh clone, a new workspace, or a restored config can reintroduce development settings. Verify the config on the running host, not in your repository.
  • Some hardening costs usability. Enforced 2FA generates support load; strict SameSite breaks some cross-site flows; short session lifetimes annoy people. Make those trade-offs deliberately rather than discovering them in production.
  • Do this before launch, not after. Every item is minutes of work on an unlaunched app and a scramble on a live one.

This is the last article in the series. If you started at Start From Primitives, the arc was: choose a substrate that already works, inherit the parts everybody needs, keep the conventions tight enough that humans and agents write the same code — and then do the unglamorous work of turning development defaults into production ones. That last part has never been automatable, and it is where a system earns the right to be trusted.