You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
I want to build a separate PHP package that manages Laravel Forge infrastructure declaratively (infrastructure-as-code: a manifest of desired servers, databases, users, sites, deploys, etc. that's planned and applied/reconciled). When I reach for phpdevkits/forge-sdk as the engine, it covers the provision + site + deploy layer (servers incl. database/cache types, SSH keys, sites, deployments, daemons) — but the moment a manifest says "create database app with user app_user", or "issue an SSL cert", or "resolve my Hetzner credential by name", I hit a wall: those Forge resources exist in the API but aren't wrapped by the SDK. Today an IaC tool can stand up machines and deploy code, but can't manage databases, database users, firewall, scheduled jobs, env, certs, or look up credentials/VPCs by name.
Solution
Round out the SDK's resource coverage so every Forge resource an IaC reconciler needs is available as an imperative primitive with the established list / get / create / update / delete shape (plus filters for lookup-by-name). The SDK stays imperative; it does not gain plan/apply/state/reconcile logic — that lives in the downstream IaC package. This PRD is the dependency map for that package, delivered in tiers.
Tier 1 — IaC-blocking
Databases (schemas) — GET/POST /database/schemas, GET/DELETE /database/schemas/{database}, POST /database/schemas/synchronizations.
Server Credentials + VPCs — GET /server-credentials, GET /server-credentials/{credential}, GET/POST /server-credentials/{credential}/regions/{region}/vpcs, GET .../vpcs/{vpcId} — so the IaC layer resolves credential_id / network_id by name instead of hardcoded numbers (closes the gap noted in docs/FINDINGS.md for server create).
As an IaC package author, I want to create/list/get/delete firewall rules, so that a firewall: block is reconciled.
As an IaC package author, I want to create/list/get/delete scheduled jobs at server and site scope, so that cron is declarative.
As an IaC package author, I want to read a scheduled job's output, so that I can surface run results.
As an IaC package author, I want to get and update a site's environment file, so that .env is managed as desired state.
As an IaC package author, I want to create/list/get/delete monitors, so that health checks are declarative.
As an IaC package author, I want to get/update a site's nginx config and manage server nginx templates, so that custom server config is reproducible.
As an IaC package author, I want to manage redirect rules on a site, so that redirects are declarative.
As an IaC package author, I want to restart nginx (service action), so that config changes take effect within a reconcile.
As an IaC package author, I want to manage database backup configurations, list/get instances, and trigger restores, so that backup policy is declarative.
As an IaC package author, I want to read site and server logs, so that the IaC tool can report on apply failures.
As an IaC package author, I want every managed resource to expose list + get + create/update/delete with filters, so that my reconciler can converge desired state with minimal custom code.
As an IaC package author, I want list endpoints to support filtering by name/identifier, so that lookups don't require paging the whole collection.
As an IaC package author, I want typed, hydrated DTOs for every resource, so that planning/diffing works against real fields, not arrays.
As an IaC package author, I want consistent typed exceptions across all new resources, so that apply errors are catchable and actionable.
As an SDK maintainer, I want each new resource to follow the existing Resource + Request + DTO + Enum + factory shape, so that the SDK stays internally consistent and learnable.
As an SDK maintainer, I want spec-vs-runtime divergences for the new resources recorded in docs/FINDINGS.md, so that the next person isn't surprised.
Implementation Decisions
The SDK stays imperative. No plan/apply/diff/state/reconcile logic lands here — that's the downstream IaC package. This PRD only adds resource primitives.
Resource shape (uniform): every new resource gets a collection resource (all/iterate/create) and/or an item resource (get/update/delete/actions), matching the existing servers/sites/deployments/daemons resources, so a reconciler can rely on a consistent CRUD surface.
DTOs:final readonly, JsonSerializable, ::from(array) hydration with the require/optional helper pattern; input DTOs (Create*Data, Update*Data) with toArray() that strips nulls; list-options DTOs with toQuery(). Lenient on spec-vs-runtime nullability per existing precedent.
Enums only for spec-closed sets (e.g. firewall rule type, scheduled-job frequency, backup provider — TBD per spec).
Pagination via the existing Page<T> + ParsesPage trait for all new collections.
Exceptions reuse the existing typed hierarchy; no new exception types unless a new client-side guard is needed.
Credentials/VPC ergonomics: expose enough read surface that the IaC layer can map human names → credential_id/network_id, removing the magic-number requirement currently documented in FINDINGS.
Testing Decisions
Same standard as the SDK to date. A good test asserts external behavior — DTO hydration from a recorded response, request URL/method/body composition, and resource method return types/contracts — never internal implementation details.
Per resource: unit tests for DTOs (::from() happy path + each validation throw), enums, list-options toQuery(), and request resolveEndpoint()/method/body; plus a live-recorded lifecycle test (create → list → get → update → delete/actions) with fixtures captured against the real Forge API and scrubbed via per-domain Fixture subclasses.
Coverage: maintain the 100% pest --exactly=100 gate; new resources must not drop it.
Prior art:tests/Unit/Resources/*ResourceTest.php, the *WriteLifecycleTest files, the *Fixture redaction classes, and the recording workflow in the saloon-integration skill / docs/FINDINGS.md.
Fixtures: recorded only (no hand-crafted JSON, no MockResponse::make()), per the established convention; redact all PII and account-identifying ids/hostnames (the centralized URL-id redaction in ForgeFixture already covers new nested segments — extend the alternation when adding a new URL segment).
Out of Scope
The IaC package itself — manifest parsing, plan/diff, apply, dependency ordering, state tracking. This PRD only delivers SDK primitives it will consume.
Problem Statement
I want to build a separate PHP package that manages Laravel Forge infrastructure declaratively (infrastructure-as-code: a manifest of desired servers, databases, users, sites, deploys, etc. that's planned and applied/reconciled). When I reach for
phpdevkits/forge-sdkas the engine, it covers the provision + site + deploy layer (servers incl. database/cache types, SSH keys, sites, deployments, daemons) — but the moment a manifest says "create databaseappwith userapp_user", or "issue an SSL cert", or "resolve my Hetzner credential by name", I hit a wall: those Forge resources exist in the API but aren't wrapped by the SDK. Today an IaC tool can stand up machines and deploy code, but can't manage databases, database users, firewall, scheduled jobs, env, certs, or look up credentials/VPCs by name.Solution
Round out the SDK's resource coverage so every Forge resource an IaC reconciler needs is available as an imperative primitive with the established
list/get/create/update/deleteshape (plus filters for lookup-by-name). The SDK stays imperative; it does not gain plan/apply/state/reconcile logic — that lives in the downstream IaC package. This PRD is the dependency map for that package, delivered in tiers.Tier 1 — IaC-blocking
GET/POST /database/schemas,GET/DELETE /database/schemas/{database},POST /database/schemas/synchronizations.GET/POST /database/users,GET/PUT/DELETE /database/users/{databaseUser}.PUT /database/password.GET /server-credentials,GET /server-credentials/{credential},GET/POST /server-credentials/{credential}/regions/{region}/vpcs,GET .../vpcs/{vpcId}— so the IaC layer resolvescredential_id/network_idby name instead of hardcoded numbers (closes the gap noted indocs/FINDINGS.mdfor server create).Tier 2 — common infra
GET/POST /firewall-rules,GET/DELETE /firewall-rules/{rule}./scheduled-jobs(+/output) and site-level/sites/{site}/scheduled-jobs(+/output): list/get/create/delete.GET/PUT /sites/{site}/environment.GET/POST /monitors,GET/DELETE /monitors/{monitor}.Tier 3 — config / ops
GET/PUT /sites/{site}/nginx, server templates/nginx/templatesCRUD,POST /services/nginx/actions.GET/POST /sites/{site}/redirect-rules,GET/DELETE .../{redirectRule}./database/backups/....application/nginx-access/nginx-error, server/logs/{key}.User Stories
database:block provisions it.users:block is provisioned.network_idby name (required for Hetzner creates).firewall:block is reconciled..envis managed as desired state.list+get+create/update/deletewith filters, so that my reconciler can converge desired state with minimal custom code.docs/FINDINGS.md, so that the next person isn't surprised.Implementation Decisions
all/iterate/create) and/or an item resource (get/update/delete/actions), matching the existing servers/sites/deployments/daemons resources, so a reconciler can rely on a consistent CRUD surface.server($id)->databases()/->database($name);server($id)->databaseUsers()/->databaseUser($id);server($id)->database()->updatePassword(...).server($id)->firewallRules(),->monitors(),->scheduledJobs(),->nginxTemplates().site($id)->environment()(get/update),->scheduledJobs(),->redirectRules(),->nginx()(get/update),->domain($id)->certificates()(SSL certificates resource (under site domains) #25),->logs().org()->serverCredentials()/->serverCredential($id)->region($r)->vpcs().final readonly,JsonSerializable,::from(array)hydration with the require/optional helper pattern; input DTOs (Create*Data,Update*Data) withtoArray()that strips nulls; list-options DTOs withtoQuery(). Lenient on spec-vs-runtime nullability per existing precedent.Page<T>+ParsesPagetrait for all new collections.$forge->me()end-to-end + credential factories #2–Daemons / Background Processes (under server) #9). Tier 1 may be split into per-resource sub-issues.credential_id/network_id, removing the magic-number requirement currently documented in FINDINGS.Testing Decisions
::from()happy path + each validation throw), enums, list-optionstoQuery(), and requestresolveEndpoint()/method/body; plus a live-recorded lifecycle test (create → list → get → update → delete/actions) with fixtures captured against the real Forge API and scrubbed via per-domainFixturesubclasses.pest --exactly=100gate; new resources must not drop it.tests/Unit/Resources/*ResourceTest.php, the*WriteLifecycleTestfiles, the*Fixtureredaction classes, and the recording workflow in thesaloon-integrationskill /docs/FINDINGS.md.MockResponse::make()), per the established convention; redact all PII and account-identifying ids/hostnames (the centralized URL-id redaction inForgeFixturealready covers new nested segments — extend the alternation when adding a new URL segment).Out of Scope
sudo_passwordsurfacing — tracked in Surface server create's one-time sudo_password to callers #30.Further Notes
$forge->me()end-to-end + credential factories #2–Tag v0.1.0 + Packagist publication (HITL) #10 MVP breakdown.