Architecture

TL;DR

The Soccorso framework, its entry points and routing, and where each kind of thing lives.

CCC runs on Soccorso, a deliberately minimal in-house plain-PHP framework. No Composer, no autoloader, no namespaces in application code, and no dependency beyond a hand-vendored template engine. That is a decision, not an accident: the constitution treats simplicity as a feature and overengineering as a defect.

AGENTS.md is the authority on all of this. This page is the orientation; it does not restate the rules.

Where things live

PathHolds
public/Entry points and static assets. The docroot
app/models/Every class. One lowercase class per file, filename matching the class
app/http/Page controllers, one directory per module, one file per page
app/data/JSON endpoints feeding the tables on those pages
app/api/Token-guarded external endpoints
app/public/Token-guarded embeddable dashboards
app/jobs/Batch jobs, grouped per source or per step
config/Reference data, seeds, credentials and the cron inventory
templates/Templates, mirroring app/http/

Entry points

Each is a separate front door with its own guard. There is no shared bootstrap; each defines the constants it needs.

The main application is session, database, templating, authorisation, router. The JSON endpoint applies the same per-page permission. The API and the public dashboards use tokens instead. The job runner and the CSV exporter each have their own guard.

A route is always resolved against a set derived from disk, then required. Building a path from the request and testing whether it exists is not a guard, and this rule applies to any new entry point.

Routing

The first URI segment matching a directory is the module; the next segment is the page. Remaining segments stay available to the page for sub-views.

The consequence that catches people: a folder inside a module is not a URL segment. /dictionary/classifications/transactions works because classifications is a page file and transactions is a sub-view, not because classifications is a folder.

Page files run in the router's scope, so the database, templating, user and router variables are ambient. That is the pattern; keep it.

Models

Most data models take the database in their constructor, expose a public property per result set, and fill it through getters named for the thing they fetch. Filters accumulate into a list and are joined; paging is a cast limit. Disk-only docs, Markdown and settings models need no database; upstream clients have their own construction patterns. Copy the relevant model rather than imposing a database constructor on every class.

SQL is written inline as strings. There are no prepared statements and no transactions. Two rules make that safe and both are in AGENTS.md: escaping protects a value only inside quotes, and an identifier is checked against the real set rather than escaped.

Jobs and queues

Most jobs consume the batches queue. See the pipeline for what that means operationally.

A recurring job exists in four places and all four are easy to forget: the file, its schedule line, its entry in the cron inventory, and a healthcheck where its output matters. Nothing detects a missing one at runtime. AGENTS.md flow step 7 is the checklist.

Deployment

Merging to the acceptance branch deploys acceptance; merging to the main branch deploys production. A merge is a release. The deploy reinstalls the schedule and regenerates the permission matrix, so neither is a manual step on those environments.

Evidence

Framework constraints from .specify/memory/constitution.md, principles II and III. Layout, entry points, routing, model shape and the four-places job rule from AGENTS.md. Basis: code-checked at 30f5313. AGENTS.md remains the authority; where this page and it disagree, it wins.

About this document
  • Type
    development
  • Status
    active
  • Updated
    2026-09-14
  • Created
    2026-09-13