# The application model: System as the core

2026-08-07. Written on the PC, picking up the handoff.

The job: make Convoro a core called **System** with everything else — forums,
downloads, pages — as applications feeding off it. Nothing works without System;
everything else can be turned off, and any of them can own the front page.

---

## What is actually true today

The handoff says "`forum` became the accidental core". Measured against the
manifests, that is half right in a way that changes the work.

```
member:    []                        <- no dependencies at all
extension: []
forum:     ['member']
...and 12 of 17 modules declare 'forum'
```

**`member` is already the root of the dependency graph.** It depends on nothing
and everything reaches it, directly or through `forum`. System is not a module
that has to be invented from nothing — it is `member`, already sitting in the
right position, under a name that undersells it.

The genuine defect is the second half: `forum` is declared by 12 modules, and
most of them do not use it. Counting real references to forum code:

| Module | Declares `forum` | Real references |
|---|---|---|
| moderation | yes | 17 |
| api | yes | 11 |
| widget | yes | 9 |
| page | yes | 8 |
| realtime | yes | 4 |
| search | yes | 4 |
| **attachment** | yes | **0** |
| **download** | yes | **0** |
| **notification** | yes | **0** |
| **reaction** | yes | **0** |
| **webhook** | yes | **0** |

**Five modules declare a dependency they never use.** Those five lines are the
whole of the "Downloads cannot exist without Forums" problem, and deleting them
is a five-line change that no code depends on.

The remaining six are real coupling, and each is a different question — whether
the module needs *forums* or needs *content*, which is the interesting one and
the reason this note exists rather than a commit.

## The shape

Three layers, and the middle one is new only as a name:

- **System** — identity, groups, permissions, settings. Cannot be disabled.
  Depends on nothing.
- **Applications** — forum, download, page, and later events, gallery, calendar.
  Each depends on System. Each may be disabled. Each may own `/`.
- **Services** — search, notification, reaction, attachment, webhook, realtime,
  api, moderation, widget, menu, appearance, messenger, extension. These work
  *across* applications and must not name any one of them.

The service layer is where the real design is. A service that hardcodes `forum`
is a service that breaks the moment Downloads wants the same behaviour — and
five of them already declare the dependency without using it, which is the
cargo-cult version of the same mistake.

## Content types, which is what the six real dependencies are about

`moderation`, `search`, `realtime`, `widget`, `api` and `page` do not want
forums. They want **something to moderate, index, broadcast, list and expose**.
Today that something is spelled `Topic`, so they import `Modules\Forum`.

The answer is a **content-type registry in System**: an application registers
the types it owns, and declares for each what can be moderated, what is
indexed, what may be reacted to, what appears in an activity stream. Services
consume the registry and never name an application.

This is what makes Downloads feel like part of one product rather than a
bolted-on feature: a file gets reports, search hits, reactions and webhooks
because it registered for them, not because somebody wired Downloads into five
services by hand.

🚨 **This is the load-bearing decision in this note.** Stripping five unused
dependency lines is cleanup. The registry is the thing that decides whether
adding Gallery later is a week or a month, and it is much cheaper to design at
17 modules than at 30.

### The bug that proves the point

`modules/member/Services/AccountErasure.php:97` deletes from a fixed list of
tables when a member is erased. One entry is `topic_stars` — **a table that
belongs to the example extension in `examples/topic_stars/`, not to core.**

On a fresh install without that example installed, erasing an account throws.
That is not hypothetical: it is why **20 tests fail on this machine and 447
passed on the Mac**, where the example happened to be installed. Every failure
is `AccountErasure` or something that erases an account on the way past.

Core is carrying a hardcoded list of other people's tables. The registry
replaces it: anything holding per-member rows registers itself, erasure asks
the registry, and an extension that is not installed contributes nothing
instead of throwing. Data protection is a legal obligation, so a list that
silently rots is worse here than almost anywhere else in the codebase.

## Turning applications off

Nearly free, as the handoff says. `ModuleManager` already does enable/disable
and already drops a module whose dependency is missing. One condition confines
it to extensions:

```php
if ($source === self::SOURCE_EXTENSION && !in_array($key, $enabled, true))
```

Lifting the `SOURCE_EXTENSION` clause makes core modules disablable. What has
to come with it:

- System, and the services an administrator cannot sensibly lose, are not
  offered. A checkbox that bricks the site is not a feature.
- Disabling an application hides its routes, menu entries, widgets and content
  types. The registry gives this for free; without it, every service needs its
  own conditional.
- **Data is never touched.** Disabling hides; uninstalling removes. Conflating
  them is how somebody loses a forum by unticking a box.

## Who owns the front page

Does not exist today; there is no setting. Any enabled application may claim
`/`, chosen in Admin → Settings, defaulting to Forums. Pages already needed
fallback routes for root-level slugs, so the router can express this already.

The rule that keeps it sane: claiming `/` moves an application's index there,
it does not move everything else. Forums stays at `/forums` and keeps working.

## Order

1. **Delete the five unused `forum` dependencies.** No code change, immediately
   truthful manifests, and Downloads stops lying about needing Forums.
2. **Rename `member` → `system`,** widening it to own settings and permissions
   explicitly. Mechanical, touches every manifest, worth doing before the
   registry so the registry lands in the right place.
3. **The content-type registry**, and move `AccountErasure` onto it. This is the
   real work and it fixes the 20 failing tests as a side effect rather than by
   installing an example extension to make them go green.
4. **Move the six genuine `forum` dependencies onto the registry**, one service
   at a time, verifying the failing case each time.
5. **Enable/disable for applications**, which by then is the one-line change it
   should have been.
6. **Front-page ownership.**

Payments plug into step 5's seam: Downloads already has `is_paid`,
`price_cents` and a webhook-safe `grant()` with nothing calling them.

## What "member" keeps

The module is System. **A member is still a member**, and the names that
describe people rather than modules do not move:

- Routes — `/member/{id}`, `member.profile`, `member.avatar`
- Events — `member.registered`, `member.signed_in`
- Template hooks — `member.profile.top`, `member.register.fields`
- Classes — `MemberController` still administers members

These name their *subject*. `system.registered` would be worse English and a
worse API, and renaming them would break every extension and every webhook
subscription in exchange for nothing. What changed is which module owns them,
which is not something a URL should have to say.

Templates are the exception, because `member::front/register` genuinely names
the module: those became `system::`. `FreshInstallTest` caught the ones missed,
which is the argument for the test existing.

## Done 2026-08-07

Steps 1–3 are in. The suite is **447 passing on an empty database** — the same
number the Mac reported, but there it passed on accumulated fixtures.

- ✅ Five unused `forum` dependencies deleted.
- ✅ `member` → `system`: 36 files of namespace, 42 template references, every
  manifest, the example extension and the docs. 🚨 Migrations are recorded
  against the owning module, so the rename would have made all seventeen look
  unrun and replayed them onto existing tables — mid-upgrade, on a live site.
  `engine/Migrations/2026_08_07_000001_rename_member_module_to_system.php`
  carries an install across; engine migrations are prepended in
  `MigrateCommand`, so it lands before anything asks.
- ✅ `PersonalData` registry, and `AccountErasure` moved onto it. Each module
  registers the tables it owns in `boot()`; the example extension registers
  `topic_stars` itself. That deleted the hardcoded list and the bug in it.
- ✅ Two tests were reading member id 1 rather than making one, so they passed
  only on a database that happened to have members. Both now build their own
  fixtures — which matters more than it looks, because **a fresh install is
  precisely what the web installer will produce.**

Steps 4–6 followed the same day. **467 passing** — the 447 above plus twenty
covering the new core behaviour.

- ✅ **4.** Content-type registry, in
  `docs/design/2026-08-07-content-types.md`. Quick search now asks the
  registry, and Pages and Downloads became findable — content that was
  invisible before, so a feature rather than a refactor. `search` keeps its
  `forum` dependency until the full results page follows, and the manifest
  therefore still says something true.
- ✅ **5.** Applications can be turned off. A module opts in with
  `"application": true`; forum, download and page do, System and the services
  do not. 🚨 `pruneUnsatisfiable()` **threw** for core modules — a missing core
  dependency used to be impossible — so the first disable took the whole site
  down rather than dropping what depended on it. Now a dependency that is
  *switched off* drops its dependents with a reason, while one that is simply
  absent is still a fatal broken install. Verified: turning off Pages drops
  Widgets and leaves fifteen modules serving.
- ✅ **6.** Front-page ownership. The engine's welcome page became a
  **fallback**, because engine routes load before module routes and as an
  ordinary route it would always win. An application registers `/` when it owns
  it. Verified across all four states: unset shows the welcome page, Forums and
  Downloads each serve the root when chosen, `/forums` and `/downloads` keep
  working throughout, and disabling the owner falls back to the welcome page
  rather than a 404.

Next: the web installer.

## What this note does not decide

- **Whether `member` keeps a separate identity from `system`.** Taken here as
  one module, because that is how it was described: a core that holds the
  member information. If profiles later want to be an application in their own
  right, that is a later note.
- **The permission model itself.** The registry needs per-type permissions and
  that is the part most likely to be got wrong. It gets its own note before
  step 3 goes near it.
- **Migration for existing installs.** Only dev runs Convoro today, so a rename
  is cheap now and expensive the day after the first real site exists. Doing it
  now is most of the argument for doing it at all.
