# Foundation — SeedProject as a per-project Astro backend **Status:** Design (approved, not yet implemented) · **Date:** 2026-07-04 **Related:** [documentation.md](documentation.md) (framework internals) · [llm.md](llm.md) (LLM layer) · [changelog.md](changelog.md) --- ## Context & goal SeedProject (this `api/` framework) is mounted at `comiida.com/api/` and boots (see the 2026-07-04 changelog). The goal now: make it a **solid, reusable backend platform** that a static Astro frontend calls to reach a database, run agents, trigger CLI processes, and record metrics. That vision is four subsystems on a shared base. This doc specifies **only the Foundation** (sub-project #1) — the database + a secure, reproducible, reusable request round-trip. Metrics, agents, and CLI *business* commands are separate specs built on top of it. ### Locked decisions | Question | Decision | |---|---| | Reuse model | **Per-project copy** — each Astro site gets its own `api/` install + own DB. A drop-in starter, fully isolated. | | Auth | **Two-tier** — public (origin + rate-limit) for browser beacons; privileged (bearer token / API key) for reads/admin. | | Schema mgmt | **Versioned SQL migrations via CLI** — `install/dump.sql` baseline + `db/migrations/NNN_*.sql` applied by `php console db:migrate`. | | Install | **CLI-first** (`php console app:install`); the existing web wizard is fixed but hard-locked. | ### Deployment facts (this project) - Web server: **Apache** (`/www/server/apache`), DocumentRoot = `.../public` (static Astro build). `/api` is served via an `Alias` → `.../api` with a php-fpm 8.3 handler and front-controller rewrite. (nginx conf in the BT panel is inert.) - DB engine: **MariaDB 10.11** on `/tmp/mysql.sock` (BT-managed). Foundation provisions a dedicated `comiida` database + user. - Frontend calls are **browser-side, same-origin** `fetch("/api/…")` (comiida is static; there is no Astro SSR). This is why the public tier is origin/rate-limited rather than server-secret-authenticated. --- ## Architecture ``` Browser (static Astro page) │ fetch("/api/health") same-origin ▼ Apache :443 ──Alias /api──▶ api/index.php ──▶ core/Bootstrap ──▶ Router │ │ │ (php-fpm 8.3 via /tmp/php-cgi-83.sock) ┌────────────────┴───────────────┐ │ ▼ ▼ │ PublicController ApiController │ (origin + rate limit) (bearer / api_auth │ │ + plan limits + logging) │ ▼ ▼ │ App logic ───────▶ Db helper ───▶ MariaDB (comiida) ▼ JSON envelope { ok, data, error } ``` The Foundation adds **five units**, each independently understandable/testable: 1. **Installer** — provisions DB + config (CLI + locked web wizard share one service). 2. **Migrations runner** — versioned schema evolution. 3. **Two-tier auth** — `PublicController` + `ApiController` base classes. 4. **Health round-trip** — a real end-to-end proof endpoint. 5. **Security hardening** — Apache denies, install lock, crypto keys. --- ## 1. Install (CLI-first) **Problem with the shipped installer** (`install/controllers/index.php`): it reads `dump.sql`/writes `config.php` via `$_SERVER['DOCUMENT_ROOT']` (wrong under the `/api` Alias — that's comiida's `public/`); it generates a config with `SITE_BASE '/'`, `ASSETS '/public/assets/'`, `seedproject.com` (would clobber the correct `/api` config); it is web-reachable and re-runnable; and `getName()` uses non-crypto `rand()` over an alphabet containing `'`, `\`, `/` (a quote breaks the single-quoted `config.php` → PHP syntax error). **Design:** - **`app/Services/Installer.php`** — one service both entrypoints call (DRY). Uses `dirname(__DIR__, 2)` base paths, never `DOCUMENT_ROOT`. Responsibilities: 1. Validate + test DB connection (PDO). 2. Import `install/dump.sql` (the skeleton). 3. Generate keys with `bin2hex(random_bytes(32))` (crypto-safe; hex only — no quote bug). 4. Write `api/config.php` with the **correct `/api` constants** (`SITE_BASE=/api`, `ASSETS=/api/public/assets/`, `URL`, `PROJECT_NAME` from args/env; DB creds). 5. Record the baseline as applied, then run pending migrations. 6. Write install lock `api/system/.installed`. - **`commands/InstallCommand.php`** — `php console app:install` (Symfony Console, registered in `console` next to `GreetCommand`). Reads creds from options (`--db-host --db-name --db-user --db-pass --url --name`) or interactive prompt; refuses to run if `.installed` exists unless `--force`. - **Web wizard** (`install/`) — refactored to call `Installer` (fixes its path bugs); refuses to run when `.installed` exists; denied at the Apache layer by default. Config stays as `config.php` (framework-native, already gitignored) — no `.env` loader is introduced (YAGNI). The stray CreditPullEngine `.env` remains vestigial and should be deleted/rotated by the operator. ## 2. Schema & migrations - `install/dump.sql` = **baseline** (imported once). Tables already provided: `users*`, `roles`/`permissions`/`role_perm` (RBAC), `org*` (orgs/profiles), `api_auth`/`users_api` (API keys), `api_plans` (rate limits), `api_requests` (request log), `api_usage` (daily/monthly/yearly counters), `config` (key/value). - **`db/migrations/NNN_description.sql`** — ordered, forward-only SQL files. - **`migrations`** tracking table (`id, filename, applied_at`) — created by the installer; baseline recorded so it is never re-run. - **`commands/MigrateCommand.php`** — `php console db:migrate` applies unapplied files in filename order, each in a transaction where the DDL allows; records each. `php console db:migrate --status` lists applied/pending. ## 3. Two-tier auth + response contract Reuses the existing tables — no duplication. - **`ApiController`** (base, privileged) — verifies a bearer token: either the `ADMIN_TOKEN` (config constant) for first-party/admin calls, **or** an `api_auth.apikey` for programmatic consumers. On an API key: check `active`, enforce `api_plans.limit_minute` / `limit_monthly` against `api_usage`, and log to `api_requests`. Failures → `401` (missing/invalid) or `429` (over limit). - **`PublicController`** (base, public) — for browser beacons. Verifies `Origin`/`Referer` against a config **`ALLOWED_ORIGINS`** allowlist, applies **IP-based rate limiting**, requires no user key. Failures → `403` (bad origin) or `429` (flood). Emits same-origin CORS headers. - **Response envelope** — a shared `json($data, $status)` helper on the base `Controller` returns `{ "ok": bool, "data": …, "error": { "code", "message" } }` with the matching HTTP status. ## 4. Health round-trip (the demonstrable slice) - **`GET /api/health`** (public) → runs a **real** DB query (`SELECT 1` + read one `config` row) → `{ ok:true, data:{ db:"connected", app, version, time } }`. - **`GET /api/admin/ping`** (privileged) → echoes authenticated context. - A small client-side `fetch("/api/health")` on an Astro test page renders the result — proving **browser → Apache → php-fpm → MariaDB → JSON** end to end. ## 5. Security hardening - Apache `` denies extended to `install/` (once `.installed` exists), `db/`, and the `.installed` lock. Existing denies (`vendor|core|app|system|commands| .memory|.reference_files`, dotfiles, `config.php`, `composer.*`) stay. - `config.php` gitignored, written with crypto-safe keys. - Public endpoints origin-gated + rate-limited. - Operator TODO: rotate/delete the vestigial `api/.env` (live CreditPullEngine secrets + a GitHub PAT). --- ## Data flow — a public request 1. Browser `GET /api/health` (same-origin, no credentials). 2. Apache Alias → `api/index.php` → Bootstrap → Router → `Health` controller (extends `PublicController`). 3. `PublicController` checks `Origin` ∈ `ALLOWED_ORIGINS`, checks IP rate limit. 4. Controller queries `Db` (lazy PDO connect to MariaDB), builds payload. 5. `json()` emits `{ ok, data }` + `200` (or `403`/`429`/`500`). ## Error handling - All controller output goes through `json()`; no raw echo. HTTP status always set. - DB/PDO exceptions caught → `{ ok:false, error:{ code:"db_error", … } }` + `500` (detail hidden unless `DEBUG`). `system/ErrorHandler.php` remains the backstop. - Auth failures return typed codes: `unauthorized` (401), `forbidden_origin` (403), `rate_limited` (429). ## File inventory **New** - `app/Services/Installer.php` - `commands/InstallCommand.php`, `commands/MigrateCommand.php` - `app/Controllers/PublicController.php`, `app/Controllers/ApiController.php` - `public/controllers/health.php` (public), `public/controllers/admin.php` (privileged) - `db/migrations/` (+ a first example migration) - `api/system/.installed` (generated at install) **Modified** - `console` — register `InstallCommand`, `MigrateCommand` - `install/controllers/index.php` — delegate to `Installer` (path-bug fix, lock check) - `core/Controller.php` (or a base) — add `json()` helper - `config.php` — add `ADMIN_TOKEN`, `ALLOWED_ORIGINS` constants (generated by installer) - Apache vhost (`/www/server/panel/vhost/apache/comiida.com.conf`) — extend denies **Server (outside repo)** - MariaDB: create `comiida` database + dedicated user. ## Verification 1. `php console app:install --db-… --url=https://www.comiida.com --name=Comiida` → tables created, `config.php` written with crypto keys, `.installed` present. 2. `php console db:migrate --status` → baseline + migrations applied. 3. `curl https://www.comiida.com/api/health` → `200`, `db:"connected"`. 4. From a disallowed `Origin` → `403`; flood → `429`. 5. `curl /api/admin/ping` no token → `401`; with `ADMIN_TOKEN` → `200`. 6. `curl /api/install/` after lock → denied. 7. Main Astro site (`/`) unaffected. ## Out of scope (future sub-projects, each its own spec) - **Metrics** — event tables + `collect` beacon + admin dashboard. - **Runtime agents/skills** — LLM agent endpoints (builds on [llm.md](llm.md)). - **Business CLI commands** — cron/process runners. The Foundation delivers the CLI *infrastructure* (`app:install`, `db:migrate`) and the secure round-trip those three build on. ## Open items for the operator - DB name/user/password for the `comiida` database (BT panel or root creds). - Decision to rotate + delete `api/.env`.