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.
- 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.