flowchart LR
publicIndex["public/index.php"] --> bootstrap["core/bootstrap.php"]
bootstrap --> extensions["content/extensions/{slug}/"]
extensions --> init["do_action init"]
init --> routesFile["core/routes.php"]
routesFile --> routesHook["do_action routes"]
routesHook --> apiGroup["group /api"]
apiGroup --> apiFile["core/routes/api.php"]
apiFile --> apiHook["do_action api_routes"]
apiHook --> kernel["Kernel::handle"]
kernel --> reqFilter["apply_filters request"]
reqFilter --> middleware["middleware pipeline"]
middleware --> controller["Controller"]
controller --> view["View Handlebars"]
view --> resFilter["apply_filters response"]
- Web server serves
public/index.php. - Defines
DLTN_ROOT,DLTN_CORE,DLTN_CONTENT. - Bootstrap loads Composer autoload,
.env+config/*.php, builds the DIContainer. ExtensionLoaderdiscovers eachcontent/extensions/{slug}/folder with a valid header; enabled ones gettemplates/roots, autoload, and bootstraprequire. Disabled slugs (Settingsextensions.disabled) are skipped.- Fires
init. - Registers core routes, then fires
routeswith theRouter. - Registers the
/apigroup (apimiddleware), loadscore/routes/api.php, then firesapi_routes. - Returns
Kernel.handle()captures the request, applies filters, matches routes, runs middleware, invokes the controller.
| Path | Role |
|---|---|
public/ |
Document root only |
core/src/ |
PHP (Dltn\ PSR-4) |
core/templates/ |
Handlebars templates |
core/assets/ |
CSS/JS (URL /assets/... via PHP) |
core/assets/icons/lucide/ |
Lucide SVG icon set |
core/database/migrations/{mysql,sqlite}/ |
Core/framework SQL per driver (+ optional companion .php) |
content/extensions/{slug}/ |
Self-contained extensions (bootstrap + templates + assets + migrations) |
content/extensions/{slug}/database/migrations/{mysql,sqlite}/ |
Feature schema owned by that extension |
content/database/ |
Default location for SQLite database files |
content/templates/ |
Optional global template overrides |
content/uploads/, avatars/, cache/, backups/ |
Runtime data |
config/ |
Committed PHP config (env() defaults) |
.env |
Secrets + environment overrides (not in git) |
docs/ |
This handbook |
WordPress-style folders. Preferred bootstrap: content/extensions/{slug}/{slug}.php with an Extension Name header. See EXTENDING.md.
Loaded (enabled) extensions are available from the container as Dltn\Core\ExtensionLoader (all()). All on-disk extensions including disabled: discovered(). Toggle in admin at /admin/extensions (manage_extensions); state stored in Settings key extensions.disabled.
Registered services include:
Config,Database,Encryptor,Session,SessionStore,Csrf,RateLimiterSettings,View,Hooks,ExtensionLoader,Logger,PhpErrorHandlerTotpService,AdminAuth,TeamRepositoryMailer→SmtpMailer(readsconfig['smtp'])Queue(DB-backed jobs; drained bybin/cron.php/bin/queue.php)
| Key | Purpose |
|---|---|
db.default |
Active connection name (mysql or sqlite) |
db.connections.* |
Named connections (driver, credentials / SQLite path, optional prefix) |
app_key |
Encryption at rest |
app_secret |
CSRF / signing |
app_url |
Absolute base URL |
app_name |
Product display name (default DLTN Framework) |
app_env |
production | development |
force_https |
Redirect + secure cookies |
smtp.* |
Outbound mail |
session.driver |
database (default; uses sessions table) or file |
session.lifetime |
Seconds for DB sessions (default 7200) |
log.file |
Optional absolute app log path (default content/logs/app.log) |
log.php_file |
Optional absolute PHP error log path (default content/logs/php.log) |
installed |
Locks installer when true |
Supported drivers: MySQL (default) and SQLite. Config selects one via db.default + db.connections. Legacy flat db arrays (no connections) still resolve as MySQL.
Database exposes driver(), table(), quoteIdent(), and upsert() for portable helpers. Raw SQL that names tables should use $db->table('name') when a non-empty prefix is set.
Migrations are per driver with matching basenames:
core/database/migrations/mysql/0001_….sql
core/database/migrations/sqlite/0001_….sql
Migration 0001_core_schema.sql (core only):
team_members— admin users (roles:super_admin,admin,editor,member)team_invites— invite tokensteam_email_changes— email change tokenssettings— optional key/value JSONrate_limits— login / reset throttlingschema_migrations— created by Migrator
Migration 0002_password_resets_jobs_sessions.sql:
team_password_resets— forgot-password tokensjobs/job_failures— background queuesessions— DB session store (session.driver = database)
Migration 0003_session_member_meta.sql:
- Adds
team_member_id,ip_address,user_agentonsessionsso Account/Team can list devices and revoke sessions remotely
Feature tables belong in extension migrations (content/extensions/{slug}/database/migrations/{driver}/). MigrationRunner applies core first, then each extension (versions stored as ext:{slug}/{file}). Run with php bin/migrate.php (also runs during install).
Routes under /admin with auth middleware. Capabilities via can:view_dashboard, can:edit_content, can:manage_team, can:manage_settings, can:manage_extensions.
Auth extras: /admin/forgot-password, /admin/reset-password/{token}.
Core mounts a dedicated /api group (api middleware) from core/routes/api.php. Extensions register under the same prefix via api_routes (router is already scoped to /api).
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/api/health |
Public | Liveness { "status": "ok" } — works during maintenance and pre-install |
GET |
/api/ready |
Public | Readiness (installed + DB); 200 / 503 |
GET |
/api/version |
Public | App name, DLTN_VERSION, PHP version (api.version filter) |
GET |
/api/csrf |
Public (session) | CSRF token for X-CSRF-Token |
GET |
/api/me |
auth.api |
Current admin member + capabilities |
Middleware aliases for API routes: api, auth.api (JSON 401; session cookie or Authorization: Bearer), csrf (JSON 419 under /api; skipped when a Bearer header is present; also covers PUT/PATCH/DELETE), can:* (JSON 403 under /api). Kernel returns JSON for /api 404/405/500 and maintenance (/api/health exempt).
Super Admins mint and revoke personal API tokens under System Settings → API tokens (can:manage_settings). Tokens are hashed at rest (dltn_…), are owned by a team member (identity for /api/me, revoke-on-deactivate), and carry an explicit capability list chosen at create time — they do not inherit the member’s full role. Capabilities drive can:* for Bearer requests.
Account settings (/admin/account): profile, email, password, avatar, 2FA, and Sessions (list/revoke own devices when using the database session driver). Password change signs out other devices and revokes that member’s API tokens. Team (manage_team) can Sign out everywhere for another member; deactivate/remove also clears their sessions and API tokens. RequireAuth rejects deactivated or missing members even if a cookie remains.
Base nav (then filtered by admin_menu): Dashboard under Menu; Extensions and Team settings under Admin; System Settings under System (manage_settings, Super Admin only). Account settings live in the sidebar user menu. Menu icon values are Lucide names (or raw SVG). Optional link/group section key chooses the sidebar heading (default Menu). Optional order (int, default 50) sorts items within a section — lower first; core items use spaced values (10, 20, …) so extensions can insert with e.g. order => 5.
System Settings (/admin/system, manage_settings): config readout (app_name, app_url, app_env, …), PHP/runtime info, frontend maintenance toggle (creates/removes content/cache/.maintenance; public routes get HTTP 503 + maintenance template for anonymous visitors, fully authenticated admins bypass and can preview the live frontend, /admin, /install, /assets, /ext, /content/avatars, and /content/uploads stay up; admin header shows a Maintenance pill while active), API tokens (create/list/revoke Bearer tokens for any active member), clear template cache, recompile assets (Tailwind → core/assets/css/app.css + Alpine sync; also php bin/build-assets.php), clear logs, and a log viewer with a channel dropdown (Application → content/logs/app.log, PHP errors → content/logs/php.log). When smtp.host is set, Application shows Send test email (POST /admin/system/test-email) which delivers emails/smtp-test to the signed-in admin. PHP errors/fatals/uncaught exceptions go to the PHP log via PhpErrorHandler. Old /admin/logs redirects there.
Extension assets: /ext/{slug}/... → content/extensions/{slug}/assets/... (enabled extensions only). Core assets: /assets/... → core/assets/.... Operator files: /content/avatars/... and /content/uploads/... only (never cache/logs/extensions).
/install/* while installed === false. Steps: welcome → requirements → database → admin account → migrate + write .env → finish. Fires install.complete after success. GET /install/finish remains reachable once so the post-install success page can render after APP_INSTALLED is written; other /install/* paths return 404.
- Apache:
public/.htaccess(document root =public/) - Nginx: see nginx.conf.example