seedproject-web/api/.memory/foundation.md
Carlos Arias 1559ce017d chore: scaffold SeedProject base (Phase 1)
Clean-room copy of the reusable engines from comiida, with all
instance data, secrets, dependencies, and build output excluded:
- app/         Astro theme skeleton (no comiida blog posts; hero image -> placeholder)
- api/         SeedProject PHP framework (no vendor/.env/config.php)
- content-pipeline/  engine only (scripts/admin/prompts; empty runtime state)
- astroagent.config.json + app/.astroagent/skills

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SYHWLHihq3v9nxNwoPCKSn
2026-07-04 22:53:10 +00:00

11 KiB

Foundation — SeedProject as a per-project Astro backend

Status: Design (approved, not yet implemented) · Date: 2026-07-04 Related: documentation.md (framework internals) · llm.md (LLM layer) · 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 CLIinstall/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 authPublicController + 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.phpphp 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.phpphp 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 OriginALLOWED_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/health200, db:"connected".
  4. From a disallowed Origin403; flood → 429.
  5. curl /api/admin/ping no token → 401; with ADMIN_TOKEN200.
  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).
  • 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.