# 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.

**Known violation found in the real `auth.php`:** `auth_env_value()`
hardcodes the app name `register` in its `.env` file list
(`.../apps/register/.env`) instead of reading the *current* app via
`APP_ROOT` (which `paths.php` defines) — meaning `auth_login_required()`
silently reads the wrong app's `.env` for every app except `register`.
Needs fixing to use `APP_ROOT . '/.env'` dynamically. Flagging this since
it's the exact class of bug Rule #1 exists to prevent, found in the file
that's supposed to enforce it.

## 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
