# 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; same token pattern as `/devconsole` & `/admin`)
  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`.
