There are reading terminals in the field. Each one asks on start-up: “am I allowed to run?” — is the licence valid, what permissions does it carry, until when.
A server was needed to answer that. Plus a panel showing who uses which terminal and what each device did when.
Why I did not use a framework
My first reflex was to install one. Then I looked at the requirement list:
- Five pages: login, dashboard, terminals, logs, users
- Two tables (both already existed)
- One API endpoint
- One role: administrator
Downloading 40 MB of dependencies, learning a configuration ecosystem and handling a major upgrade every year felt out of proportion.
I wrote it myself. Six classes:
App application shell, container
Router path → controller
Request input, headers, body
Response output, headers, redirects
Db PDO wrapper (prepared statements)
View template inclusion, escaping
Auth session, password verification
Under 900 lines in total. Exactly as much as I needed — no more, no less.
A framework’s real value is not saved code, it is a shared vocabulary. With five people on a team, everyone looking at the same pattern beats a bespoke six-class shell by a mile. But here the team is one person and the scope is fixed. In that case a framework is not a convenience, it is a dependency.
Everything secret lives outside the web root
The folder layout has one rule and everything follows from it:
service.bzt/
private/ ← the web server CANNOT reach this
config/ configuration, database credentials
core/ the six classes
controllers/
views/
storage/ users, logs
www/ ← this is the web root
index.php single entry point
.htaccess route everything to index.php
assets/
The value of that split: if one day .htaccess stops working, or a configuration error slips in, an attacker still cannot reach the configuration file — because it is not even inside the tree the web server can see.
The most common leak I see on shared hosting is precisely this: a config.php served because of a misconfiguration.
The old contract kept byte for byte
This service replaces an existing API. Terminals in the field expect that API and cannot be updated — some are in vehicles, some in basements.
So the rule was clear: not a single field of the response may change.
GET /terminal/api/?op=get&p=license&key={hardware}&ver={version}
{
"status": …,
"v2": { "securekey": … }, ← key derived per month
"time": { "license_expire": … },
"perm": { "read": …, "edit": … },
"admin": { … },
"security": { "manuplationlock": … }
}
The inside was rewritten completely. The outside — field names, typos included — stands as it was.
Cost: some field names make no sense and stay that way. A new developer will look at them and ask why.
Return: not one device in the field stopped during the transition. The new server went up, the old address was pointed at it, nobody noticed.
When replacing a system that is already running in the field, an invisible transition is worth more than a clean API.
And the console theme
The panel’s look is a deliberate choice: dark ground, monospaced type, terminal feel.
Not for aesthetics. The person using this panel is technical and is diagnosing a field problem. Instead of cards and coloured tiles they want a dense, readable list. A terminal aesthetic gives that naturally: many rows per screen, hierarchy carried by typography.
What I learned
“Which framework?” is usually asked too early. The first question should be: how many screens will this system have in five years?
If the answer is five, the scaffolding ends up larger than the project. If the answer is fifty, starting without one becomes a regret within a year.
Here the answer was five. Three months later it is still five.