veeqtoh/prompt-deck
Manage AI prompts as versioned files in Laravel and PHP. Organise agent instructions, compare prompt performance, and activate the right version with variable interpolation, tracking, A/B testing, and AI SDK integration.
Verdict. Adopt as a lightweight prompt-convention layer inside a Laravel codebase, not as a prompt-management platform. It is worth the integration cost for teams that already accept "prompts ship with the deploy," and it is the wrong tool for teams expecting non-engineers to edit prompts in production without a release.
Architecture fit. Deck sits between application code and any chat-completion API as a thin resolution layer. Prompts live as directories under resources/prompts, each version is a folder of role files (system.md, user.md, assistant.md) plus a version metadata.json, and the prompt-level metadata.json names the active_version. Consumption is a Deck facade returning a prompt object that renders a role with interpolated variables or emits a provider-agnostic toMessages() array; a HasPromptTemplate trait covers Laravel AI SDK agents so instructions() and promptMessages() come for free. That is idiomatic Laravel throughout — service provider, publishable config, migrations, Artisan generators, facade — and adds no infrastructure of its own except an optional tracking table. The architectural caveat that shapes everything downstream: this is file-based, not a remote registry. What you get is in-repo versioning, draft/promote discipline, and a single resolution point. What you do not get is prompt changes decoupled from deploys.
Integration feasibility. Low. composer require, one vendor:publish, one migrate, then make:prompt. Interpolation is {{ $variable }} into markdown role files; toMessages() is usable without the AI SDK. The one friction point is that tracking tables are a separate opt-in publish step — historically a production trip-wire. Budget about a day for install plus first prompt, not a sprint.
Technical risk. Pre-1.0 (0.4.6) with one breaking rename already executed: veeqtoh/prompt-deck → promptphp/deck, namespace Veeqtoh\PromptDeck → PromptPHP\Deck, old namespace removed outright. 117 stars, zero dependents, single maintainer, sponsorship-funded. Treat semver breakage as expected rather than exceptional. The 0.4.6 changelog is the most informative document in the repo: it fixes a production-only crash (tracking defaulted on while its tables were unpublished), a silent no-op in Deck::activate() meaning runtime activation had never actually worked, a path-traversal hole where prompt names reached the filesystem unvalidated, and an unanchored version regex that read rev2, dev3, and archive-v9 as versions. Individually each is an ordinary bug; collectively they indicate the database-backed paths lacked end-to-end coverage until very recently — and those are precisely the paths you would lean on for activation and A/B. Mitigating factors: MIT license, small codebase, disciplined changelog, published security contact, and an upgrade guide. Assume you may have to fork.
Also calibrate the marketing: "A/B testing" here means flipping the active version and comparing tracked outcomes. It is whole-traffic, manually switched, with no weighted split. It is an experimentation workflow, not an experiment engine.
Key questions.
metadata.json to a bootstrap default?prompt:activate?Stack fit. Laravel, with Laravel 13 support added in 0.2.0 alongside earlier majors, and PHP per the package constraint. Templates are plain markdown with {{ $var }} interpolation, so the output fits anywhere you can assemble a messages array. Where the app already routes model calls through the Laravel AI SDK, the HasPromptTemplate trait is the cheapest possible integration and make:agent scaffolds the matching prompt directory automatically. Where it doesn't, use toMessages() as the seam and leave provider code untouched.
Migration path. Two shapes, with a warning on the second:
resources/prompts/<name>/v1/system.md, replace literals with Deck::get()->system(...), and assert output parity with a snapshot test before flipping traffic.veeqtoh/prompt-deck 0.3.x → complete the rename and namespace rewrite before broad adoption; the old namespace is gone in 0.4.0, and that is what UPGRADE.md exists for. Pin exact versions in composer.json and upgrade deliberately.Compatibility. Post-0.4.0 the public namespace is PromptPHP\Deck only, so partial upgrades break. The tracking schema changed in 0.4.6 (prompt_versions.user_prompt nullable) — existing installs must re-publish and re-migrate, not merely composer update. Behavior change in the same release: tracking now defaults to off; set DECK_TRACKING_ENABLED=true if you rely on it. Once a version has been activated with tracking enabled, the database takes precedence over metadata.json — that semantic is a compatibility concern for your deploy process, not just your code.
Sequencing.
make:prompt; parity-test it.prompt:list --all, prompt:activate, roll back to v1.DECK_TRACKING_ENABLED, confirm rows land and confirm the degraded path logs correctly when a table is absent.make:agent scaffolding, and write the activation runbook.Maintenance. Bus factor one. Low stars and zero dependents mean no downstream field validation of your dependency. Read the changelog before every bump, because breaking changes are cheap pre-1.0. Budget for the realistic option of vendoring the package — it is small and MIT, so forking works; the cost is losing upstream fixes. The consolidation under a PromptPHP org is a positive trajectory signal, but the package identity has already changed once.
Support. GitHub issues plus a maintainer email for security disclosures. No commercial SLA, no vendor escalation path. Internal ownership of the dependency must be assigned explicitly and in writing.
Scaling. Resolution is filesystem read plus JSON decode per call — enable the cache and verify invalidation on activation. Tracking writes one row per tracked call to a shared table; at meaningful AI volume that is a hot table and a write on the request path after a completed, paid-for call. Queue it and set a retention window. Prompt count and size are bounded by deploy size rather than runtime, which is fine.
Failure modes, ranked by blast radius:
active_version in metadata.json does nothing. A "revert the commit, redeploy, you're rolled back" playbook silently stops working. This is the single most important thing to document.Deck::get() name in the codebase resolves.system versus user roles.Ramp-up. The technical ramp is short: a Laravel developer is productive with the facade and Artisan workflow in half a day, and the Mintlify docs site is materially better than 117 stars implies. The real cost is process. Teams must agree on who may run prompt:activate against production, internalize that activation is environment state rather than a code change, and define how draft versions get reviewed. Without those agreements the package degrades into a filesystem with extra steps and a tracking table nobody trusts.
How can I help you explore Laravel packages today?