promptphp/deck
Deck (formerly Prompt Deck) brings versioned AI prompt management to Laravel and PHP: store prompts as files, interpolate variables, track performance, run A/B tests, and activate versions across your app with Laravel AI SDK integration.
Disposition: conditional adopt — pilot on a non-critical AI path, wrapped behind an internal adapter, with tracking enabled only after the tables are verified. The package is cheap to try and cheap to abandon, but it is pre-1.0, single-maintainer, and its own changelog shows production-affecting defects shipping in patch releases.
Architecture fit
resources/prompts/<name>/ as markdown plus metadata.json, versioned in the same repo as the code that calls them. This is the correct fit for teams that want prompt changes under code review, PR gates, and deploy-time reproducibility — no second CMS, no credential sprawl, no sync problem between prompt store and app.Deck::get() → $prompt->system($vars) / $prompt->toMessages($vars) returns a plain chat-completion messages array, so it works with the Laravel AI SDK or any other client. Vendor lock-in is genuinely low.Integration feasibility
composer require, publish config, php artisan migrate, four Artisan commands (make:prompt, prompt:list, prompt:activate, plus test/diff), one facade, one optional trait. A mid-level Laravel developer is productive inside a day.Deck::get() in an afternoon. This should be treated as the primary risk mitigation, and it argues for an internal PromptRepository adapter now rather than calling the facade directly in dozens of call sites.Technical risk
veeqtoh/prompt-deck → promptphp/deck) and the entire public namespace (Veeqtoh\PromptDeck → PromptPHP\Deck), with the old namespace removed. Any further rename is a find-and-replace across the codebase plus a lockfile change./v(\d+)$/, unanchored) that reported rev2/dev3/archive-v9 as legitimate versions, a make:prompt name kebab-caser that could scaffold unloadable prompts, Deck::activate() never actually inserting rows so runtime activation silently no-op'd, and tracking defaulting on whenever APP_DEBUG was false — which made the natural production install throw no such table: prompt_versions on the first Deck::get(). Each is individually fixable; collectively they show hardening arriving after release rather than before. Assume more of this and read every changelog before bumping.metadata.json, so editing active_version in a file and deploying no longer changes what is served. This is documented and arguably correct, but it breaks the "prod is git HEAD" guarantee that justified the file-based design in the first place.Key questions
prompt_versions an audit trail? Does activate() record who activated and why, or only timestamps? Without an actor, rollback forensics mean correlating deploys by hand.Deck::get() names ever be derived from user input? InvalidPromptNameException is thrown — where is it caught, and does it become a 500 on a paid inference path?{{ $variable }} escape or sanitise? If user-supplied text can be interpolated into a system.md role, that is a prompt-injection surface and a support-escalation path, not just a rendering detail.Stack fit
Laravel 12/13 with an optional, removable coupling to the Laravel AI SDK via the HasPromptTemplate trait (which supplies instructions() and promptMessages() so agents don't hand-define them). Because toMessages() emits a standard messages array, the package is usable with any chat-completion client and is not a decision that couples to your model vendor. Config includes a toggle for auto-scaffolding prompt directories on make:agent.
Migration path
DECK_TRACKING_ENABLED=false explicitly set. Confirm rendering, prompt:list, and activation work end-to-end before enabling tracking in a non-production environment.veeqtoh/prompt-deck 0.3.x: follow UPGRADE.md; the namespace rewrite is mandatory and the old namespace is gone, so there is no compat shim. If tracking was already on, tables must be re-published and migrated because prompt_versions.user_prompt became nullable in 0.4.6.Compatibility and coexistence
Deck::get('order-summary', $assignedVersion) accepts 'v2' or 2 — and let the flag system decide who gets which. This gives real splitting without waiting for the package to grow a capability it may never build.^0.4 range will pull minors that have historically included security fixes and behavioural changes in the same release.Sequencing
| Phase | Scope | Exit criteria |
|---|---|---|
| 1. Spike (1–2 days) | Install in a sandbox branch, tracking off, one low-risk prompt (e.g. summarisation), called through an internal PromptRepository adapter |
Rendering and activation work; adapter is the only place the facade is referenced |
| 2. Pilot (1 sprint) | One production feature, tracking on in staging only, prompt:list --all in the deploy checklist |
No tracking-related incidents; drift check between git and DB is documented |
| 3. Tracking in prod | Enable tracking, add monitoring on write count | A/B comparison is interpretable, or explicitly scoped down to "version history + rollback" |
| 4. Scale out | Migrate remaining ad-hoc prompt strings; add AI SDK trait to agents | Prompt changes are reviewable in PRs and rollback is a one-command runbook |
Hold at phase 2 if the pilot surfaces drift between file and DB state that the team cannot detect automatically, or if anyone outside engineering needs to edit prompts.
Maintenance
Support
Scaling
prompt:list --all becomes unreadable.Failure modes
| Failure | Symptom | Control |
|---|---|---|
| Tracking tables absent/not migrated | 0.4.6+ degrades to metadata.json with a single logged warning instead of throwing |
Alert on tracking write count dropping to zero; a silent degrade means empty experiment data, not a visible error |
| DB vs file drift on active version | Prod serves a version that isn't what metadata.json in git says |
Scheduled prompt:list --all diff against git; runbook entry that activation is environment state |
| Invalid prompt name | InvalidPromptNameException → 500 on the inference path |
Only ever pass literal, validated names; catch and fall back at the adapter boundary |
| Missing prompt or version at render time | Exception after the user has waited, on a paid call | Fallback prompt constant in the adapter; render-failure metric |
User text interpolated into system.md |
Prompt injection, tone/style escapes | Route untrusted text into a user role only; review every system.md that takes a $variable from request input |
| Package rename / breaking minor | Fatal on deploy, dependency audit noise | Exact version pins, adapter isolation, upgrade guide in the runbook |
make:prompt overwrite of a live version |
Prior content replaced in place | Prefer new-version workflow; git is the recovery path, so never run it on an uncommitted tree |
Ramp-up
prompt:list --all in the pre-deploy checklist, monitoring on tracking writes, and exception alerting on prompt-name and prompt-not-found failures.How can I help you explore Laravel packages today?