GOLDEN_RULES.md
Standard App Structure — v2 (validated against real folder tree, piloted on `defapp`)
`defapp` was the old standard. This version corrects it against what's
actually in production (`register`, `aiwizard`, `aicode`), and was tested by
converting `defapp` itself before rolling out further. This is a full
rewrite consolidating every decision made since v1 — treat this as the
current source of truth, not the earlier drafts.
Rule #1 — No hardcoded paths, ever
Every path and every auth check is set off the `.env` files (`system/.env`
+ `{appname}.env`) and enforced by including `system/php/paths.sh`,
`system/php/paths.php`, and `system/php/auth.php`. No file — app or system
tier — hardcodes a path or reimplements auth logic; it includes these three
and uses the constants/functions they expose. This is the rule every other
rule in this doc exists to support, and it's non-negotiable regardless of
how small or "temporary" a file feels.
Enforcement, three layers:
1. Structural — `defapp`'s own entry-point files already contain the
required includes, so every scaffolded app inherits them automatically.
Doesn't catch files added later.
2. Runtime guard — `paths.php` should `define('BOOTSTRAP_LOADED', true)`
after it runs; `auth.php` (and anything else security-sensitive) should
refuse to execute (`exit` with a 500) if that constant isn't set. Turns
"forgot to include paths.php" into an immediate loud failure instead of
a silent wrong-app bug.
3. Static audit — `system/audit/audit.bootstrap.sh` scans every app's
entry-point files (`public/`, `pages/`, `admin/`, `api/`) for missing
`paths.php`/`auth.php` includes and for hardcoded absolute-path or
inline-credential patterns. `-s`/`--strict` makes it usable as a
pre-deploy gate.
Fixed: `auth.php`'s `auth_env_value()` previously hardcoded the app name
`register` in its `.env` file list, meaning `auth_login_required()` silently
read the wrong app's `.env` for every app except `register`. Now reads
`APP_ROOT . '/.env'` dynamically (`APP_ROOT` is defined by `paths.php`,
guaranteed present by the `BOOTSTRAP_LOADED` guard below). Verified with a
two-app test: `APP_NAME`/`AUTH_REQUIRE_LOGIN` correctly resolved per-app,
and `auth.php` included without `paths.php` first correctly fails loudly
instead of running with wrong/missing constants.
Still open — `paths.php` itself has a hardcoded fallback:
`$BASE_ROOT_FS = '/Users/rbutare/websites'` is a literal path used before
`.env` is loaded (a genuine chicken-and-egg problem — you need a path to
find `.env` before `.env` can tell you the path). Options: derive it from
`__DIR__` instead (`paths.php` lives at `system/php/paths.php`, so
`dirname(__DIR__, 2)` = the `websites/` root, no hardcoding needed), or
accept it as the one deliberately-hardcoded bootstrap seed and document it
as an explicit, intentional exception to Rule #1 rather than an oversight.
Not yet fixed — tracked in `tasks.md`.
Mandatory per-app skeleton (confirmed against the converted `defapp`)
```
{appname}/
├── {appname}.index.php # entry point, dot-segment naming
├── {appname}.env # app-level config, WRITABLE (see .env section below)
├── {appname}.manifest # authoritative per-app manifest (see Manifest section)
├── public/ # web-servable root ONLY — everything else stays outside webroot
├── admin/
├── pages/
├── templates/
├── config/ # FLAT — no config/templates/ subfolder
├── css/
│ └── themes/ # integrates with system-tier theme-manager
├── js/
├── img/
├── php/ # app-specific PHP includes/logic (procedural, default style)
├── data/
│ ├── config/ # live/mutable JSON config values (app-wide scope)
│ ├── fonts/
│ └── tmp/
│ └── logs/ # NOT a top-level logs/, and NOT data/logs/ — data/tmp/logs/
├── docs/ # FLAT — no adr/ or runbooks/ subfolders; grouped via a JSON index instead
├── ai/
│ ├── prompts/
│ └── output/
├── audit/ # app-specific audit checks
├── tests/
├── scripts/ # one-off/maintenance scripts for THIS app
├── tools/ # maintained reusable CLI for THIS app
├── sql/ # TOP-LEVEL — not nested under data/
└── tmp/ # scratch/ephemeral, gitignored
```
No `var/` and no `sessions/` at app tier — sessions are shared/SSO scope
and live at `system/data/tmp/sessions/` instead (see System tier below).
Optional: `src/{Controllers,Helpers,Parser}/` (or an app-specific
namespace set, e.g. `aicode`'s `Database/Diagnostics/Logging/Models`) — the
deliberate exception to procedural style, used only where OOP is genuinely
the better fit (e.g. native PDO usage). Not every app needs this; `php/`
(procedural) is the default.
Optional declared extension zones (per app, audited only if declared)
```
{appname}/
├── api/ # REST API layer (family-tree)
├── python/ # non-PHP runtime — venv itself is gitignored, never committed
├── assets/ # uploads, photos, exports (family-tree)
└── themes/ # only if beyond the standard css/themes/ override
```
System tier — same mandatory-skeleton logic, system-wide scope
```
system/
├── .env # READONLY from apps. Owns DB credentials and every
│ # *_ROOT / DEPLOY_ROOT / BASE_URL_PREFIX path.
├── config/
├── data/
│ ├── config/ # system-wide live config
│ ├── sql/
│ │ └── user_preferences.sql
│ └── tmp/
│ └── sessions/ # moved from app tier — shared/SSO scope
├── audit/ # cross-app compliance checks — mandatory at this tier
├── tests/ # tests the shared tooling itself — mandatory at this tier
├── tools/
│ └── theme-manager/ # shared across apps — moved here, not a peer app
├── scripts/
├── docs/
└── php/
├── paths.php # loads system/.env AND app .env; defines all path constants
├── paths.sh # bash equivalent, same job
├── auth.php # auth logic — included alongside paths.php by app entry points
└── aicode/ # shared library code — moved here, not a peer app
```
`system/php/paths.php`/`paths.sh` and `system/php/auth.php` are the only
files that read `.env` or implement auth — nothing else does. The path
files load BOTH `system/.env` (readonly) and the calling app's own `.env`
(writable, can override), then expose the result as PHP constants / shell
exports so every other file just consumes those instead of ever parsing an
`.env` itself:
- Path constants: `BASE_ROOT`, `SYSTEM_ROOT`, `APPS_ROOT`, `APP_ROOT`,
`PROJECT_ROOT`, `APP_HOME`, `APP_NAME`, `DEFAULT_APP_VERSION`
- DB constants: `DB_HOST`, `DB_NAME`, `DB_USER`, `DB_PASS` (+ `DB_CHARSET` in
the shell version)
- URL constants: `BASE_URL`, `SYSTEM_URL`, `APPS_URL`, `APP_URL`,
`ADMIN_URL`, `PAGES_URL`
- Helper functions (PHP only): `sys_include($path)` — resolves a path against
app root, then `templates/`, then system root, in that order; `app_url()`
and `app_path()` — build URLs/paths relative to the current app
- `APP_NAME` is auto-detected from the calling script's path
(`.../apps/{name}/...`), not hardcoded, so the same `paths.php`/`paths.sh`
works for every app without modification
- Optional diagnostics: if `PATHS_WRITE_JSON=true` is set, `paths.php` writes
the resolved values to `system/php/paths.json` for debugging
This mirrors the READONLY/WRITABLE split at a code level too: `.env` is data,
`paths.php`/`paths.sh` is the one accessor everything else goes through.
The single placement rule (the test for every file)
> If a folder's contents apply to more than one app, or govern the tooling
> system itself, it belongs under `system/`, declared as a system extension.
> If a folder's contents are specific to one app's own behavior or
> requirements, it belongs under that app, declared as an app extension.
`.env` — two tiers, override by design (not restriction)
`system/.env` is READONLY in the sense that apps must never edit that
file — it holds shared defaults (base `*_ROOT` paths, `DEPLOY_ROOT`,
`BASE_URL_PREFIX`) that apply when an app doesn't specify its own. It is
NOT the only place DB credentials or other config can live.
Each app's own `{appname}.env` is WRITABLE and, per
`paths.php`/`paths.sh`, is loaded after `system/.env` — so it can freely
override DB credentials (a different database per app is expected and
normal), add API keys or third-party service credentials specific to that
app, and set any other app-specific config. This is the actual point of
the two-tier load order, not an exception to it. The only real constraint
is Rule #1: no hardcoded paths — DB creds, API keys, and app-specific
values in `{appname}.env` are fine and expected; hardcoding a filesystem
path instead of going through `paths.php`/`paths.sh` is what's not allowed.
Config scope, by cardinality (not by who edits it)
The right storage mechanism is decided by how many instances of a setting
can exist — not by who makes the change. An admin editing something doesn't
make it "admin data"; it's still scoped by who the setting affects.
| Scope | Cardinality | Storage | Example |
|---|---|---|---|
| System-wide | One instance, applies to every app | `system/data/config/` | Shared auth settings, global feature flags |
| App-wide | One instance, applies to every user of that app | `{appname}/data/config/` | `forms.json` — admin edits it once, every user sees the change from then on |
| Per-user | One instance per user (N grows with user base) | Database (`user_preferences` table), never the filesystem | A user's saved preferences, dashboard layout |
| Per-session | One instance per active session, ephemeral | `system/data/tmp/sessions/` | Login state, CSRF token, in-progress form draft |
Rule: file location reflects who's affected by a setting, never who
made the edit.
`user_preferences` table
```sql
CREATE TABLE user_preferences (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
appname VARCHAR(64) NULL, -- NULL = system-wide for this user; else scoped to one app
pref_key VARCHAR(128) NOT NULL,
pref_value JSON NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uq_user_app_key (user_id, appname, pref_key),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```
Lives at `system/data/sql/user_preferences.sql`, extending the existing
`users`/`groups`/`user_groups`/`login_logs` auth schema.
Manifest — authoritative per app; registry is generated, not edited
`{appname}.manifest` (JSON, at the app root) is the single source of truth
for that app's declared shape: `appname`, `path`, `role`, `skeleton_version`,
`extensions`, `notes`, `date_created`/`date_updated`, and a `files` array
listing every expected file for drift-checking. `project.registry.json` is
auto-generated by aggregating every app's manifest plus the system
manifest — never hand-edited directly.
Tooling:
- `manifest.sh` — creates/updates one app's manifest, or regenerates the
registry from all manifests (`-g`/`--generate-registry`)
- `manifest.diff.sh` / `manifest.diff.php` — compare a manifest's `files`
list against what's actually on disk; `-b` (both, default), `-a`
(additional only), `-m` (missing only)
All three tools belong in `system/tools/`, since they operate across apps.
Pilot: `defapp` conversion — status
`defapp` has been converted and its directory structure confirmed correct
against this standard (verified via `tree -d`). Remaining to actually
finish the pilot:
- [ ] Populate `defapp.manifest`'s `files` array (currently empty/manual —
no automated "snapshot current state as correct" mechanism yet)
- [ ] Confirm `defapp.env` contains only `APP_NAME` and `AUTH_REQUIRE_LOGIN`
(no invented path/DB/session keys — those were mistakenly added in an
earlier draft and have been removed)
- [ ] Decide whether `docs/`'s JSON grouping index has a defined schema yet,
or is still just a stated intent