seedproject-web/api/.memory/foundation.md

210 lines
11 KiB
Markdown
Raw Normal View History

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