full.md

Shared App Bootstrap System — Documentation
Two-tier config, self-locating bootstrap, and shared assets/session store for every app under apps/.

1. Overview
One system, many apps. Every app shares:

Config — system/.env (system-wide, read-only from apps) merged with each app's own .env (overrides).
Session store — shared at the system level so identity survives a handoff between apps.
Login/role gate — opt-in per app via AUTH_REQUIRE_LOGIN.
Front-end assets — Bootstrap 5, FontAwesome, DataTables, jQuery, AdminLTE — versioned once, included selectively per app.
Guiding rule: no hardcoded paths anywhere, except the one unavoidable exception documented in §3.2.

2. Directory Layout
websites/ (ROOT)
├── apps/ (APP_BASE_ROOT / PROJECT_ROOT)
│ └── sampleapp/ (APP_ROOT = APP_BASE_ROOT + APP_NAME)
│ ├── .env app-specific overrides (optional)
│ └── sampleapp.index.php entry file — starts with the walk-up shim
└── system/ (SYSTEM_ROOT / SYSTEM_BASE_ROOT)
├── .env golden rule — single source of truth, READONLY from apps
├── data/
│ └── sessions/ shared session store (SESSION_SAVE_DIR)
└── php/
├── paths.php two-tier .env loader, path/URL constants, helpers
├── bootstrap.php session start + conditional auth dispatch
├── assets.php CDN include blocks (CSS/JS), per-item options
└── auth.php login/role/handoff logic (not yet built — see §6)
DEPLOY_ROOT is a separate absolute path (e.g. /Users/rbutare/prod) used by deploy tooling — it is not necessarily where the app runs from at request time.

3. Architecture
3.1 Request flow
sequenceDiagram
participant App as apps/{app}/{app}.index.php
participant Boot as system/php/bootstrap.php
participant Paths as system/php/paths.php
participant Auth as system/php/auth.php

App->>App: define('APP_NAME', ...)
App->>App: walk up directories to find system/php/bootstrap.php
App->>Boot: require_once (found path)
Boot->>Paths: require_once paths.php
Paths->>Paths: load system/.env (required)
Paths->>Paths: define SYSTEM_ROOT, APP_BASE_ROOT, etc. from .env
Paths->>Paths: define APP_ROOT = APP_BASE_ROOT + APP_NAME
Paths->>Paths: load apps/{app}/.env (optional, overrides system)
Paths->>Paths: define remaining constants (BASE_URL, APP_URL, versions...)
Boot->>Boot: session_save_path(SESSION_SAVE_DIR); session_start()
alt AUTH_REQUIRE_LOGIN is truthy
Boot->>Auth: require_once auth.php; auth_enforce()
Auth-->>App: redirect to login, or continue with identity
else not required
Boot-->>App: return immediately — auth.php never loaded
end
App->>App: render page (assets_css_block/js_block, app.css/app.js)
3.2 The one hardcoded-path exception
An app cannot know where system/ lives relative to itself in advance — it could be one level up, three levels up, or differently nested per deployment. So each app's entry file starts with a small walk-up shim: starting at its own directory, it checks {dir}/system/php/bootstrap.php, then walks up one parent at a time until it finds that file (or hits the filesystem root and throws).

This is the only place any file path is written literally. It exists purely to locate and require_once bootstrap.php — nothing else. Every constant used from that point on (APP_ROOT, APP_URL, SESSION_SAVE_DIR, CDN versions, etc.) comes from system/.env via paths.php, never from a second directory computation.

This block is copy-pasted verbatim into every app's entry file — only the APP_NAME line changes. It can't be extracted into a shared file, because a shared file would itself need to be located first (the same chicken-and-egg problem).

3.3 Physical discovery vs. logical constants
paths.php draws a hard line between two kinds of path knowledge:

Physical discovery (dirname(__DIR__) from paths.php's own location): used only to open system/.env off disk. This is reliable because paths.php genuinely lives inside system/php/ — there's no ambiguity about where it physically sits.
Logical constants (SYSTEM_ROOT, APP_BASE_ROOT, PROJECT_ROOT, DEPLOY_ROOT, ROOT, APP_ROOT, SESSION_SAVE_DIR, ...): defined from the values stated in system/.env, with the physically-discovered path only used as a fallback if a key is missing.
Why this matters: system/.env is the thing that differs between your Mac and production — same code, different .env. If constants were computed from dirname(__DIR__) instead of read from .env, that per-environment flexibility would silently break the moment the physical layout didn't match what the code assumed (symlinks, a differently-nested deploy, etc.).

3.4 Two-tier .env merge
system/.env loads first (required — a missing system .env is a hard failure). The calling app's own .env loads second (optional) and is merged on top, so app values override system values for any key both define. Root/location keys (SYSTEM_ROOT, APP_BASE_ROOT, etc.) are the one exception — those are read from system/.env only, never from an app's .env, since they're structural and shouldn't vary per app.

3.5 Shared session store
SESSION_SAVE_DIR resolves against APP_BASE_ROOT, not the individual app's own directory — e.g. ../system/data/sessions relative to APP_BASE_ROOT lands at the system-level system/data/sessions/, shared by every app. This is required for cross-app SSO: if each app wrote sessions to its own directory, a handoff from App A to App B would find no session to resume.

3.6 Asset includes
assets_css_block() / assets_js_block() each take an options array (bootstrap5, adminlte, fontawesome, datatables, jquery) so an app only pulls in what it uses. Load order is fixed internally regardless of the order options are passed:

CSS: bootstrap5 → adminlte → fontawesome → datatables
JS: jquery → bootstrap5 → adminlte → datatables
App-specific app.css / app.js are included after each shared block — CSS after, so it can override shared styles; JS after, so it can rely on the shared libraries already being initialized.

4. Environment Variable Reference
Key Tier Purpose
SYSTEM_ROOT, SYSTEM_BASE_ROOT system Absolute path to system/
APP_BASE_ROOT, PROJECT_ROOT system Absolute path to apps/
DEPLOY_ROOT system Deploy-tooling target path (not necessarily the runtime path)
ROOT system Legacy-compat: common parent of system/ and apps/
APP_NAME system (default) Fallback app name for CLI diagnostics; each app overrides via define('APP_NAME', ...)
DB_HOST, DB_NAME, DB_USER, DB_PASS, DB_CHARSET system Database connection — masked in CLI diagnostics/JSON output
SYSTEM_ENV_READONLY system Informational flag — apps should not attempt to write system/.env
APP_ENV_WRITABLE system Informational flag — whether app-level tooling may write app .env files
DEFAULT_APP_VERSION system (default) Fallback for APP_VERSION
BASE_URL_PREFIX system Base URL every app's APP_URL is built from
CACHE system/app ON/OFF → derived boolean APP_CACHE_ENABLED
AUTH_REQUIRE_LOGIN system/app 1/0 → derived boolean APP_AUTH_REQUIRED; gates whether auth.php loads at all
UPLOADS_DIR system/app Relative uploads directory name
SESSION_SAVE_DIR system Session store path — resolved against APP_BASE_ROOT if relative
BS5_VERSION, FA_VERSION, DATATABLES_VERSION, JQUERY_VERSION, ADMINLTE_VERSION system/app CDN versions consumed by assets.php
REMEMBER_DAYS system How long a "remember me" login persists before re-auth is required (consumed by auth.php, not yet built)
5. Requirements (recap)
Functional

One PHP entry point (bootstrap.php) every app includes for config, session continuity, and per-app security enforcement.
No hardcoded paths outside the walk-up shim (§3.2).
Apps opt into login/role enforcement individually via AUTH_REQUIRE_LOGIN; a no-security app never loads auth.php.
Cross-app calls pass an approved user id/role so a user isn't asked to log in again unless they're unauthorized for that specific app.
Session login persists on the same computer/browser for REMEMBER_DAYS; logout forces a full re-login.
Single point of definition for BS5 / AdminLTE / DataTables / FontAwesome / jQuery CDN includes, with per-app opt-in and correct load order.
Non-functional

System-level .env is read-only from app code (policy, not currently enforced at runtime).
Secrets (DB_PASS, etc.) are masked in any diagnostic output.
The system must work identically on local Mac dev and the shared cPanel production host, with only system/.env differing between them.
6. Open Tasks / Backlog
Build auth.php. Needs: auth_enforce() (called from bootstrap.php when AUTH_REQUIRE_LOGIN is true), identity check against session, redirect-to-login when absent, authorization check against the user_groups multi-group schema (users / groups / user_groups / login_logs).
Cross-app SSO handoff tokens. Short-lived signed token carrying user_id + role from App A to App B; App B still runs its own auth_enforce() against the token rather than trusting App A's decision.
Role/ID invalidation. If a user's role or id changes, any live session or remember-me token holding the old value must stop being trusted. Proposed: a groups_version counter checked on every request; mismatch forces a silent re-check against user_groups (not a full re-login).
Remember-me tokens. Long-lived signed cookie, separate from the PHP session, valid for REMEMBER_DAYS, tied to user_id + device fingerprint; silently re-establishes a session on a fresh visit. Every establishment (fresh login or silent resume) logs a row in login_logs.
Logout. Destroy the session and revoke/clear the remember-me token so it can't silently resume.
SYSTEM_ENV_READONLY enforcement. Currently informational only — decide whether to add a runtime guard (e.g. refuse to fopen system/.env for writing from app-context code).
DataTables CDN paths for 3.0.4. Not yet verified against cdn.datatables.net — the file layout has changed across DataTables' major versions; confirm before relying on it in production.
7. FAQ
Q: Why walk up directories instead of a fixed ../../system/... path? A fixed relative path assumes every app sits at exactly the same depth under apps/. The walk-up works regardless of nesting depth or how a given deployment is laid out, and it's the only way to locate system/.env before anything has been read from it.

Q: Why does the session store live at the system level instead of per-app? Cross-app SSO requires every app's session_start() to read/write the same store. A per-app session directory would make a handoff from App A to App B invisible to App B — it would just look like a brand-new, unauthenticated session.

Q: Why is auth.php only loaded conditionally instead of always? So a no-security app (a public status page, say) never touches the auth system at all — no session-identity check, no DB query against user_groups, nothing. AUTH_REQUIRE_LOGIN in that app's .env is the single switch.

Q: What happens if system/.env is missing? load_env() is called with required: true for the system tier, so a missing file logs a warning via paths_log(). (It currently does not hard-fail the request — worth revisiting if a missing system .env should be treated as fatal rather than a warning, since nearly every downstream constant depends on it.)

Q: Can an app override SYSTEM_ROOT or APP_BASE_ROOT in its own .env? No — those are read from system/.env only. They're structural to the whole system, not something an individual app should be able to redirect.

Q: How do I add a new app? Create apps/{name}/{name}.index.php starting with the walk-up shim (§3.2, only the APP_NAME line changes), optionally add apps/{name}/.env for anything that overrides system defaults (e.g. AUTH_REQUIRE_LOGIN=1, a different CACHE setting, a pinned asset version just for that app).

Q: How do I pin a different library version for just one app? Set e.g. BS5_VERSION=5.2.0 in that app's own .env — app values override system values for any key both define (root/location keys are the only exception).

Q: Why is DB_PASS a defined PHP constant instead of something more guarded? App code needs the real value to connect to the database — the constant holds the actual credential. What's masked is display/diagnostic output only (display_settings(), the optional JSON diagnostics file) — paths_mask_sensitive_settings() replaces anything matching PASS/SECRET/TOKEN/API_KEY/etc. with [hidden] before it's ever printed or written to disk.

Q: What's DEPLOY_ROOT actually used for, if not the runtime path? It's the target path deploy/rsync tooling pushes files to — distinct from SYSTEM_ROOT, which is where the code is actually running from at request time. They may be the same path or different ones depending on your deploy process.