Architecture overview
How the application is put together, for a developer who has to extend or audit it.
No framework, on purpose. The product runs on shared hosting where a framework's console
tooling, queue workers and build steps are not available. What it uses instead is a small
router, plain PHP templates, and PDO. There is nothing to compile and nothing to install: the
dependencies ship in vendor/.
A request, end to end
public/index.phpis the only entry point. Everything else is rewritten to it.bootstrap/app.phploads the autoloader and.env, starts the session with its security attributes, applies the security headers, checks the host againstALLOWED_HOSTS, and dispatches.routes/web.phpmaps the path to a controller method and states the middleware it requires:auth, a role, or a permission.- The controller validates input, calls a repository or a service, and renders a view with a layout.
- Repositories own the SQL. Services own the work that is not a database read or write.
The layout of the code
| Path | What lives there |
|---|---|
routes/web.php | Every address, with its middleware. The one file to read to see the whole surface. |
app/Controllers/ | HTTP actions, grouped by area: public, auth, admin, verifier, profile, install. |
app/Repositories/ | PDO access. Prepared statements only, and the place authorisation-sensitive record selection happens. |
app/Services/ | Import, export, email, audit, QR, PDF rendering, branding, translation, SMTP settings, requirements checking, installation. |
app/Security/ | Sessions and authentication, CSRF, role and permission checks, TOTP, password policy, tokens, rate limiting, headers. |
app/Validation/ | The validator and its rules. |
resources/views/ | Plain PHP templates and layouts. Output is escaped with e(). |
lang/ | One file per locale, holding every visible string as a flat key. |
public/assets/ | Vanilla CSS and JavaScript, and the bundled fonts. No build step, no bundler. |
database/ | schema.sql and the role and permission seed. |
bin/ | migrate.php, seed.php, create-admin.php. |
cron/ | The scheduled jobs. |
Classes are autoloaded as App\ from app/, so a new class needs no
registration. Composer's autoloader is used when vendor/ is present, with a small
fallback for the rest.
Where the data model turns
- A course owns dated occurrences and named templates. A certificate references one of each, and both are validated against its course.
- A certificate holds its own numbers and dates, its verifier assignment, and a 24-byte random public token. Its status moves from assigned to verified, returned or rejected, and a verified one can be revoked. Every transition is written to history.
- A document is created lazily, on the first authorised access, and stores the file hash, approved business values and the presentation used for its latest render. Business values stay frozen, apart from an explicit name correction that versions the prior render. Appearance follows the current template, and verification follows the current certificate. Changed appearance or verification, or a missing file, causes regeneration on the next authorised access. The recorded presentation is a fallback when the current template is unavailable, provided its background assets remain renderable. See stored PDF guidance.
- Edits write the previous state into a version table inside the same transaction, before the update.
Extending it
Adding a screen
- Add the route in
routes/web.phpwith the middleware it needs. Never rely on a hidden link for protection. - Add the controller method, validate input with the validator, and keep SQL in a repository.
- Add the view under
resources/views/and render it with the matching layout:layouts/admin,layouts/publicorlayouts/system. - Add every new visible string as a key in both locale files. The two files must hold the same keys.
- Write an audit entry for anything that changes data, with the changed field names and safe before and after values.
Adding a permission
Add it to database/seeds/001_roles_permissions.sql, grant it to the roles that
should hold it, and require it on the route. Re-run php bin/seed.php. Per-account
grants are deliberately unsupported: the seed empties that table.
Adding a language
Copy lang/en.php to lang/<code>.php, translate the values,
add the code to available_locales in config/app.php, and check that
the bundled fonts cover the script. The two shipped locales are complete, so a missing key is
a mistake rather than a fallback.
Changing the certificate rendering
Layouts are stored as normalised coordinates plus per-field typography, and rendered to PDF with the bundled renderer. Because each document stores its own render snapshot, a change to the renderer affects new documents and regenerations, not documents already stored. Test with a regeneration before assuming otherwise.
Two things to leave alone. The document snapshot mechanism preserves approved business values and name-correction history. The public route exposes certificate details and PDFs only for verified, non-revoked records; an unarchived revoked record has only a status notice. Keep both boundaries intact when extending the product.
Licensing and dependencies
THIRD-PARTY-LICENSES.md in the package root lists every bundled component and
its licence, and the full licence texts ship with it. Some are covered by the LGPL, which is
why the licence you bought explicitly permits modifying the product for your own use and
reverse engineering those modifications. Do not strip, minify or patch anything inside
vendor/ in a copy you redistribute internally: it must stay complete and
unmodified.