13 best practices for database development in Visual Studio 2026 

https://www.devart.com/blog/wp-content/uploads/2026/08/1068x580_Database-Development-in-VS-2026.png

Database development rarely breaks all at once. It happens through small changes that seem harmless at the time. Someone adds a column directly to the shared database. A stored procedure gets fixed in production but not in the project. An index is created in staging and never added anywhere else.

Everything looks fine until the next release. Then the differences start showing up, and nobody is quite sure which version of the schema is correct.

Reliable database development in Visual Studio avoids that problem by treating the schema like source code. Each database object lives in a .sql file, builds catch syntax errors and unresolved references, and the project produces a .dacpac that you can deploy across environments.

This guide covers 13 best practices for database development in Visual Studio, from choosing the right project format to checking for drift after deployment. We will also look at where dbForge Edge fits when your team works with database systems beyond SQL Server. 

Summary 

  • Install SSDT through the Data storage and processing workload.
  • Keep the SQL project (not the shared database) as the single source of truth.
  • Match the target platform to the SQL Server version running in production.
  • Build before every commit to catch errors early.
  • Conduct a review with Schema Compare or a DeployReport before publishing.
  • Register DAC during deployment so schema drift is caught before the next release. 

Table of contents

What database development in Visual Studio looks like in 2026

In a typical Visual Studio 2026 database development workflow, the schema lives in a SQL project. You build the project to catch errors, then package it as a .dacpac for deployment. Because the project is stored in source control with the application code, database changes can go through the same review and testing process as everything else. 

The landscape of SQL Server development tools is where things get confusing. Visual Studio, VS Code, SSDT, SSMS, and specialized database IDEs have some overlapping features, but they are not meant for exactly the same work.

Visual Studio 2026 and SQL Server Data Tools 

SQL Server Data Tools, or SSDT, adds database project support to Visual Studio. It gives you SQL projects, T-SQL editing with IntelliSense for SQL autocompletion, object navigation, table design, schema comparison, build validation, debugging, and publishing. SQL Server Object Explorer also lets you connect to SQL Server and Azure SQL without leaving the IDE.

SSDT is mainly built for the Microsoft SQL ecosystem. It supports SQL Server 2016 through SQL Server 2025, Azure SQL, Azure Synapse Analytics, and SQL services in Microsoft Fabric. It works well for SQL Server development, but it is not a general-purpose database administration tool and does not cover MySQL, Oracle, or PostgreSQL. 

Original SQL projects vs SDK-style SQL projects 

There are two SQL project formats in use. The original format is the MSBuild-based .sqlproj that SSDT has used for years. It supports the full Visual Studio workflow, including graphical design, schema comparison, builds, debugging, and publishing. 

The newer SDK-style format is based on Microsoft.Build.Sql. It follows the same general conventions as other .NET SDK projects, builds with dotnet build, and supports NuGet package references. 

Here is the important part: Visual Studio 2026 only supports the original format. For SDK-style projects, you will need Visual Studio Code, command-line tools, or the optional preview component available in Visual Studio 2022. 

Visual Studio vs Visual Studio Code vs dedicated database IDEs 

VS Code and Visual Studio are not the same thing here. The complete SSDT workflow for original SQL projects is provided by Visual Studio. For connections and SDK-style project work, VS Code uses the MSSQL and SQL Database Projects extensions. 

You might still need SSMS or a dedicated database IDE. These tools are more appropriate for administration, data editing, query profiling, backup and permission management, and multi-database work. 

Your workflow  Best fit 
Application code and a SQL Server project in one solution  Visual Studio 2026 with SSDT 
SDK-style projects and command-line builds  VS Code with SQL Database Projects 
SQL Server administration, backups, and permissions  SSMS 
Work across SQL Server, MySQL, MariaDB, Oracle, PostgreSQL, and related cloud services   dbForge Edge 

13 best practices for database development in Visual Studio 

The mechanics of how to develop a database in Visual Studio are not difficult. The harder part is keeping the project, environments, and deployment process aligned as more people start making changes. The database development tips below follow that workflow from initial setup to post-deployment monitoring.

1. Install and standardize the required Visual Studio database tools 

Open the Visual Studio Installer to install SSDT. Select Modify, and then check the SQL Server Data Tools option in the Data storage and processing workload. 

Be sure everyone is working on the same workload and components. Otherwise, a project may build on one developer’s machine but fail on another. Analysis Services, Integration Services, and Reporting Services each require separate extensions. Microsoft also supports offline installation layouts for secured networks. 

2. Choose the right SQL project format 

Choose the format of the project and design the workflow around it. Visual Studio 2026 supports only original SQL projects. If your team requires Microsoft.Build.Sql, NuGet package references, or dotnet build in Linux-based pipelines, use an SDK-style project with VS Code or command-line tools. 

Avoid mixing both formats for the same database unless there is a clear migration plan. The formats use different build tooling and handle dependencies differently. 

3. Make the database project the single source of truth 

The approved schema should live in the SQL project, not in a shared development database. When developers change the database directly, the project quickly becomes an outdated copy that nobody fully trusts. 

For an existing database, right-click the empty project and select Import > Database. SSDT will script the objects into the project. After that, make schema changes in the project first and publish them to each environment.

4. Select the correct target database platform 

The target platform controls which T-SQL syntax and SQL Server features the build accepts. If the project targets SQL Server 2025 while production still runs SQL Server 2019, the build may approve code that production cannot run. 

Set the target to the oldest platform the project must support. This is especially important when the same codebase runs across different SQL Server versions or both Azure SQL and on-premises SQL Server. 

5. Organize database objects consistently 

Select a folder structure and stay with it for the project. You can group objects by schema, object type, or both. SSDT’s default import structure (schema, then object type) makes sense as a starting point. 

A reviewer should be able to find a file without having to search the whole project. For larger databases, splitting the schema into separate projects joined by explicit references can also make ownership and dependencies easier to manage. 

6. Define database dependencies explicitly 

If an object depends on something outside the current project, add a database reference. Without one, the build will report unresolved references. 

Original Visual Studio SQL projects can reference another SQL project, a compiled .dacpac, or a system database configured through SSDT. NuGet package references are only available in SDK-style projects. 

Make sure the build agent can resolve the same dependency. A local .dacpac reference, for example, must exist at the expected relative path in CI. 

7. Store every database change in Git 

Treat database objects the same as application code when committing. Schema history, branches, pull requests, and code review should apply to tables and stored procedures too. 

Application and database changes that depend on each other should also be versioned and released together. They do not have to live in the same repository, but the release process must prevent one from being deployed without the other. 

See also: Our comparison of the best Git GUI clients for Windows

8. Keep database changes small and reviewable 

Keep branches short-lived and pull requests small. A small change is easier to review, test and roll back than a pull request that touches several unrelated objects. 

The reviewer should be able to answer a practical question for each change: what will happen to the existing data when this is deployed? What matters more is if the SQL diff looks clean. 

9. Build the database project before every commit 

A build checks T-SQL syntax, object relationships, database references, and compatibility with the target platform. It also creates the .dacpac used for deployment. 

This catches problems such as a procedure referencing a deleted column or a view using a renamed table. Build locally before committing, then run the same check again in CI. A database project with build errors should not move into deployment. 

10. Review schema differences before deployment 

Schema Compare helps you inspect differences between a project, .dacpac, or live database. In Visual Studio, open Tools > SQL Server > New Schema Comparison, select the source and target, and review each proposed change. 

For a release, generate a script or DeployReport from the same .dacpac and publish profile the pipeline will use. This gives you a more accurate view of the planned deployment. 

Common mistake: Do not treat a successful comparison as approval to deploy. Keep BlockOnPossibleDataLoss enabled, but still review the generated script. It is a safeguard, not a replacement for checking the deployment plan. 

For a closer look at the differences between these approaches, see dbForge Compare Bundle vs. Visual Studio: Which one compares databases better? 

11. Test database changes in an isolated environment 

Publish to an isolated database before staging. This could be LocalDB, a local SQL Server instance, or a SQL Server container. 

Do not test only against an empty database. Use representative data, constraints, and table sizes where possible. An ALTER TABLE that finishes immediately on an empty table may take much longer or cause blocking against production-scale data. 

12. Automate deployment through CI/CD 

Once the build produces a .dacpac, use SqlPackage to deploy it through Azure Pipelines, GitHub Actions, or another CI/CD platform.

Generate a Script or DeployReport first, store it as a pipeline artifact, and require approval before production deployment. Promote the same .dacpac through development, testing, staging, and production rather than rebuilding it for each environment. 

13. Detect schema drift continuously 

Schema drift happens when someone changes a deployed database outside the normal release process. To track it, publish with RegisterDataTierApplication enabled. This records the deployed schema as a baseline. 

You can then run the DriftReport action to find changes made since registration. Use BlockWhenDriftDetected to stop the next deployment until the differences have been reviewed and either added to the project or removed from the database.

How dbForge Edge supports database development in Visual Studio 

dbForge Edge works alongside Visual Studio rather than replacing it. Visual Studio and SSDT can remain the main environment for application code, SQL projects, and .dacpac builds. dbForge Edge becomes useful when the work extends beyond that workflow or beyond Microsoft databases. 

That is where a universal database tool earns its place. dbForge Edge brings together four database IDEs for SQL Server, MySQL and MariaDB, Oracle, and PostgreSQL. The suite supports SQL development, data editing, administration, query profiling, debugging, source control, and schema and data comparison. It also connects to more than 30 databases and cloud services, including Azure SQL, Amazon RDS, Aurora, Redshift, Supabase, and Neon. 

This gives teams a practical way to keep SQL Server database project development in Visual Studio while using one toolset for work across other database systems. Schema and data comparison can run against databases, snapshots, scripts folders, and source control revisions. Many of these tasks can also be automated from the command line for deployment checks and scheduled jobs. 

The optional dbForge AI Assistant is a context-aware SQL AI tool that can generate, explain, optimize, and troubleshoot SQL using database metadata as context. It requires a separate license from dbForge Edge. 

Alt text: dbForge schema comparison showing object differences, a deployment script, and report export options. 

Alt text: dbForge schema comparison showing object differences, a deployment script, and report export options. 

Reviewing schema differences and generating a comparison report in dbForge. 

Need to work beyond the databases covered by SSDT? Download dbForge Edge for free or compare the features for different RDBMSs. 

Conclusion 

Reliable database development in Visual Studio 2026 comes down to keeping the schema controlled from the first change to the final deployment. The SQL project stays in source control, builds catch problems early, and the same .dacpac moves through testing and production. 

These best practices come back to the same goal: make every schema change visible, reviewable, and repeatable. When the process works, the team spends less time untangling differences between environments and finds problems before the release starts. 

Download dbForge Edge for free to bring schema comparison, database development, testing, and deployment tools into the same workflow. 

FAQ 

Is Visual Studio 2026 suitable for database development? 

Yes, if you work with Microsoft SQL platforms. Database development in Visual Studio 2026 supports SQL Server 2016 through SQL Server 2025, Azure SQL Database, Azure SQL Managed Instance, Azure Synapse, and SQL services in Microsoft Fabric. It does not support MySQL, Oracle, or PostgreSQL. 

Does Visual Studio 2026 support SDK-style SQL projects? 

No. Visual Studio 2026 supports only the original SQL project format through SSDT. SDK-style projects based on Microsoft.Build.Sql work with VS Code and command-line tools. They are also available through an optional preview component in Visual Studio 2022. 

How do you import an existing database into Visual Studio? 

Create an empty SQL Server Database Project, right-click the project, and select Import > Database. Connect to the source, and SSDT will generate files for its database objects. If the project already contains schema objects, use Schema Compare instead. 

Can Visual Studio database projects be deployed through CI/CD? 

Yes. The build produces a .dacpac that SqlPackage can publish through Azure Pipelines, GitHub Actions, or another CI/CD platform. Generate a script or DeployReport first so the team can review the deployment plan before publishing. 

How do you prevent schema drift between environments? 

Avoid direct database changes and deploy through the same controlled pipeline. Enable RegisterDataTierApplication to record the deployed schema, then use SqlPackage DriftReport to find out-of-band changes. BlockWhenDriftDetected can block the next deployment until those differences are resolved. 

Can dbForge Edge replace Visual Studio? 

Not for app development or Microsoft Visual Studio SQL projects. dbForge Edge is a visual GUI tool for SQL development, comparison, administration and query profiling for SQL Server, MySQL, MariaDB, Oracle, PostgreSQL, and quite a few related cloud services. 

Planet for the MySQL Community

My Journey from Traditional Monolithic Architecture to Distributed SQL

https://static.pingcap.com/files/2022/01/21141638/Blog-Twitter-Banner.png

Editor’s note: Bill Kendall wrote this post during his time at PingCAP. It was originally published in January 2022 and updated in August 2026 to reflect TiDB’s agent state stack.

Key Takeaways

  • Monolithic databases hit a hard ceiling: one machine, one failure domain, and a cost curve that steepens as you scale up.
  • Distributed database architecture scales out instead of up, spreading data across nodes that tolerate failure and rebalance automatically.
  • Distributed SQL keeps transactions, joins, and relational modeling on top of that architecture, so you don’t trade correctness for scale.
  • AI agents face the same gap now. Durable memory, state, and files need the same distributed foundation, not a fourth system bolted on.

Like many data management engineers, I started my career on Oracle. It was a pragmatic choice. Oracle was mature, reliable, and feature-rich, and I could build fast, very large, vertically scalable systems with it. When a database needed more resources, I scaled up: a bigger machine, all the way to Oracle’s flagship engineered system, Exadata.

Then the cloud arrived and the assumption underneath all of that work stopped holding. Distributed database architecture stopped being a topic I read about and became the job. I spent years moving large on-premises applications and their databases onto cloud infrastructure, and the database was always the hard part. That work convinced me the shift is not a stylistic preference. For any workload that has to scale out, span regions, and stay online through failure, distribution is the design that fits.

Four years after I first wrote this, the same shift is happening again in a different domain. AI agents are hitting the wall my applications hit in 2014: they have nowhere durable to keep what they know. This is the story of how I got from Oracle to distributed SQL, and where that road leads next.

Why Monolithic Database Architecture Worked for Its Era

Monolithic database architecture places compute and storage on a single server and scales vertically. One machine holds the data, runs the optimizer, executes transactions, and enforces consistency. When it runs out of headroom, you buy a larger machine. For decades that was the correct answer, and it is worth saying why before explaining why it stopped being enough.

Traditional relational databases were built for workloads that were largely predictable. A general ledger, an ERP install, or an order-entry system had a known user population, a known peak, and a growth curve a DBA could budget a year ahead. Centralizing compute and storage on one node made those workloads simpler to reason about. Transactions had exactly one place to commit. Consistency was a property of the machine rather than a property of a protocol. Tuning, backups, and capacity planning all pointed at a single target.

Oracle-style enterprise environments took that model close to its limit. Exadata paired the database with purpose-built storage and networking so one system could carry workloads that would have needed a room full of hardware a decade earlier. I built plenty of those, and they worked. When the workload fits inside one machine and the machine is engineered well, scale-up remains the simplest and often the cheapest architecture available.

Monolithic databases were not badly built. The shape of the workload changed underneath them.

Where Monolithic Systems Start to Break Down

Monolithic systems break down when the workload stops fitting on one machine, and no amount of tuning fixes that. Vertical scaling has a hard ceiling set by the largest server you can buy, and the cost curve toward that ceiling is not linear. The last increment of capacity is always the most expensive one.

Moving the applications to the cloud was relatively trivial. I could distribute them geographically, and most of my clients had already moved to microservices designed to be cloud-native and horizontally distributed. The Oracle database was far harder. It was designed from the beginning to scale vertically. I could scale up, but not out to where the cloud required me to be. Traditional relational systems were becoming the bottleneck for mission-critical applications, so I pivoted in a big way.

The failure modes are consistent across the migrations I have worked on:

  • Hardware ceilings. Once you are on the largest available instance, the only lever left is rewriting the application.
  • The cost of vertical scale. Each doubling of capacity costs more than the last, and you pay for peak around the clock.
  • Availability risk. A single primary is a single point of failure. Failover means a window where writes stop.
  • Operational complexity. Teams that outgrow one node shard by hand, pushing routing, rebalancing, and cross-shard joins into application code.
  • Global application demands. Users on three continents reading from one region pay for that geography in every request.
  • Mixed workloads. Analytics against a transactional primary degrades both, so most teams add a warehouse and accept a lag measured in tens of minutes.

None of these stay technical for long. Manual sharding slows releases, peak-provisioned hardware inflates cost, and failover windows become customer-facing incidents. The architecture sets the ceiling on what the product can do.

What Distributed Database Architecture Changes

Distributed database architecture spreads data and query processing across many nodes that behave as one logical database. Instead of replacing a server to grow, you add nodes and the system rebalances data across them. That single change, scale out rather than scale up, cascades into how availability, sharding, and consistency all work.

In early 2014 I was sent a newly released Google Research paper, Spanner: Google’s Globally-Distributed Database. It described a clustered distributed database that could span one data center or several. Data was sharded automatically. Copies were distributed and balanced across the cluster. Applications could connect to any node and reach any data. Nodes could be added or removed to scale in either direction. It was the first design I had read that treated the database the way my applications were already being treated.

The mechanics matter more than the label:

  • Horizontal scaling and data distribution. The data set is divided into ranges spread across nodes, and the cluster moves ranges to keep load even as nodes join or leave.
  • Automatic sharding. Splitting and placement happen inside the database, not in application code.
  • Replication and fault tolerance. Each range keeps multiple replicas. A consensus protocol, Raft in TiDB’s case, elects a leader per range so the cluster keeps serving when a node dies.
  • Coordination of compute and storage. Separating the SQL layer from the storage layer lets each scale on its own axis, which makes elastic capacity practical.
  • Consistency models. Eventual consistency is cheaper to build and harder to reason about. Strong consistency costs coordination and returns correctness.
  • Operational resilience. Rolling upgrades, node replacement, and region failure become routine operations rather than change-control events.

Monolithic vs. Distributed Architecture

Set side by side, the two architectures differ on nearly every axis that matters in production:

Dimension Monolithic architecture Distributed architecture
Scaling model Scale up. Replace the server with a larger one. Scale out. Add nodes and let the cluster rebalance.
Data placement One copy on one machine, plus backups and standbys. Data sharded and replicated across many nodes.
Failure domain The server is the failure domain. Losing it means failover or downtime. A node is the failure domain. The cluster keeps serving on surviving replicas.
Capacity ceiling The largest machine you can buy and afford. Practically bounded by cluster size, not by any single machine.
Mixed workloads Analytics competes with transactions for the same resources, or moves to a separate warehouse. Row and column stores can serve transactions and analytics from the same cluster.
Operational model Tune one instance. Shard manually at the application layer if you outgrow it. Manage a cluster. Sharding, rebalancing, and failover happen inside the database.

The Rise of Distributed SQL

Distributed SQL is the branch of distributed database architecture that keeps SQL, relational modeling, and transactional consistency while distributing data across nodes. It sits one level below the broader category: every distributed SQL database is a distributed database, but not every distributed database gives you transactions and joins.

That distinction exists because the first wave of distributed systems dropped the relational model to get scale. NoSQL stores solved real problems, and for append-heavy or schema-loose workloads they are still the right tool. What many teams discovered is that they had traded away things they could not do without. Multi-row transactions moved into application code, where they became a source of subtle bugs. Ad hoc queries required a second system. Referential integrity became a convention rather than a guarantee. For a payments ledger or an inventory system, “the write probably landed” is not an acceptable answer.

SQL survived because the relational model is a good fit for how businesses describe their data, and because thirty years of tooling, drivers, ORMs, and hiring pools sit on top of it. Distributed SQL is the attempt to keep that surface while changing what runs underneath: familiar relational semantics and ACID transactions on an architecture that scales horizontally and survives node loss.

At PingCAP we took the multi-node auto-sharding architecture at the core of distributed SQL and extended it for flexibility beyond what Spanner and other distributed SQL databases offered. That database is TiDB: an open source, MySQL-compatible distributed SQL database. The MySQL wire protocol compatibility is deliberate. It means an existing application, driver, or DBA workflow arrives already knowing how to talk to the database.

TiDB Design Tenets for Distributed Database Architecture

TiDB operationalizes the properties described above rather than simply claiming the distributed label. In 2015 our founders built TiDB from scratch alongside a large and active open source community on GitHub, working from a set of design tenets meant to produce a cloud-native distributed SQL architecture. These are the core tenets:

  • Scale without complexity. The database scales horizontally without operator intervention. Nodes join the cluster and TiDB rebalances data onto them automatically. Scaling in works the same way.
  • Resiliency always on. TiDB survives node failures while applications continue processing transactions on the surviving nodes, which drives time to recovery toward zero.
  • Consistent ACID transactions. For transactional systems you must always read the most recent correct copy of the data. That matters most in financial services and inventory. TiDB guarantees high isolation levels at the transaction level to preserve consistency.
  • Support for both OLTP and OLAP workloads. Many distributed SQL databases handle only transactional traffic. TiDB serves transactional, analytical, and mixed workloads by adding a columnar store, TiFlash, alongside the row store to handle large aggregations and analytical functions. This is hybrid transactional/analytical processing, or HTAP, and the cost-based optimizer decides which store answers each query.
  • A common API interface. TiDB was built by developers for developers, so access happens through a widely understood SQL interface. We chose wire compatibility with MySQL, which lets application developers and DBAs use one of the most established SQL protocols in production today.
  • Install and run anywhere. The database deploys on premises, in a single cloud, or across multiple clouds, on VMs or containerized with Kubernetes. Deployment architecture can change as the organization’s requirements change.

Operational Features That Make the Tenets Usable

Design tenets produce a capable database. Operational features make it something a team can actually run. TiDB exposes rich metrics and monitoring through Prometheus and Grafana plus a built-in TiDB dashboard. Role-based access control and TLS encryption protect data in flight and at rest. Built-in asynchronous change data capture moves data into and out of the cluster, which matters for migrations and for feeding downstream systems. TiDB Cloud takes the same architecture and runs it as a managed service, which is how most teams start today.

Why Distributed Database Architecture Matters for AI Agents

AI agents are stateless by default, the same architectural gap monolithic databases left behind. A model call takes input and returns output. Nothing about it remembers the last call. The context window looks like memory, but it is a buffer: bounded, scoped to one session, and gone when the process ends. An agent that ran for six hours yesterday starts today knowing nothing about it.

Production agents need more than a transcript. The state that must survive falls into categories:

  • Facts and preferences learned in earlier sessions, which must persist across machines and users.
  • Session and task history, so the agent can answer what it did, when, and why.
  • Tool outputs and intermediate results, expensive to regenerate and needed several steps later.
  • Files and artifacts: repositories, generated code, logs, and documents produced during a run.
  • Retrieval metadata tying embeddings to their source, version, and access rules.

Two access patterns run against that state, and both have to work. Agents need exact lookups with filters, joins, and transactions: which user owns this record, has this step already run. They also need semantic retrieval when nobody knows the exact key. A single agent turn frequently needs both.

The common workaround is to stitch systems together: Postgres for records, a vector database for embeddings, object storage for files, and Redis for session state. Every boundary adds a consistency problem. When memory is revised, superseded, or deleted, the change has to land everywhere or the agent acts on stale facts. This is why we treat agent memory and state as a database problem rather than a retrieval add-on. A distributed SQL database with native vector search can hold structured records, embeddings, and searchable text in one place, run hybrid retrieval in a single query, and give mutable memory transactional guarantees.

The TiDB Agent State Stack: Zero, mem9, and drive9

The TiDB agent state stack is three layers that together give an agent durable state, all powered by TiDB Cloud. TiDB Cloud Zero provides instant SQL state, mem9 provides persistent memory, and drive9 provides a persistent workspace for files. Because they share one backend, memory, operational records, retrieval metadata, files, and execution history stay connected as a deployment grows.

TiDB Cloud Zero for Instant SQL State

TiDB Cloud Zero is a zero sign-up, zero configuration way to provision a real MySQL-compatible TiDB Cloud database in seconds. There is no registration step and no cluster to size. The instance is TLS-secured and supports joins, transactions, and standard SQL, along with native vector search and full-text search, so an agent can store embeddings next to relational data instead of splitting them across systems.

Zero instances are disposable by design, with a 30-day lifespan. That fits agent sessions, CI runs, demos, and prototypes. When a prototype turns into something you want to keep, a three-click claim converts the instance into a persistent TiDB Cloud Starter database and carries the data and schema over. Zero also works well as the database behind an MCP server or custom agent tooling; it is not itself an MCP server, but it gives MCP tools a SQL backend they can provision on demand. TiDB Cloud Zero is currently in public preview.

mem9 for Persistent Agent Memory

mem9 is a persistent memory layer for AI agents. It gives coding agents, custom tools, and multi-agent systems a shared memory space that survives across sessions, machines, and users, so what one agent learns is available to the next one. It supports hybrid retrieval, combining vector similarity and keyword search in the same query, and generates embeddings server-side so there is no separate embedding pipeline to run.

Two design decisions matter more than the API surface. First, mem9 treats memory as a system rather than a store: ingestion, deduplication, ranking, retrieval, and evaluation are all part of it, because cognitive memory gets revised, superseded, expired, and purged. Mutable memory needs strong consistency, which is why it runs on TiDB Cloud. Second, memory is inspectable. The mem9 interface lets you visualize, manage, import, and export what an agent remembers, which is what makes it possible to trust and correct. mem9 integrates with agent harnesses including OpenClaw, OpenCode, Claude Code, Codex, and Dify apps, and any HTTP client can read and write to the same memory space.

drive9 for Persistent Agent Files

drive9 is a durable filesystem for agents that do real work on disk. Coding agents clone repositories, install dependencies, edit files, run tests, produce logs, and try several approaches in parallel. The sandbox they run in may last minutes; the work usually needs to last longer. drive9 keeps that workspace state outside the sandbox, so it survives resets and can be mounted from a different environment entirely.

You mount a drive9 workspace as a local filesystem and use normal tools against it. Workspaces can be forked for parallel attempts, checkpointed, rolled back, and committed with server-side conflict detection. Files are embedded and indexed automatically on write, so an agent can search its own workspace by meaning instead of guessing at paths. Local disks run the process and Git stores the final result; drive9 holds the working state in between.

Framed against the rest of this article, none of the three is a departure. They are the same architectural shift applied to a new workload: state moves out of a fragile, session-bound, single-machine home and into a distributed system that expects to outlive any one process.

From Monolithic Databases to Agent-Ready Architecture

After more than twenty years around Oracle, I can say plainly that distributed SQL changed what I am able to build. I have watched teams solve problems that were not solvable under a scale-up architecture, and I have watched that unlock a level of creativity in the community that keeps surprising me.

The lesson that has held up is that architectural decisions compound. Choosing a scale-up database in 2010 was defensible, and it still set the ceiling on what those systems could do in 2020. The same compounding is underway right now in AI infrastructure. Teams building agents on ephemeral state are making a decision whose cost arrives later, when the agent has to answer what it knew last Tuesday, or when memory has to move across machines, or when a sandbox reset takes an afternoon of work with it. The architecture that let databases scale for the cloud era is what makes durable agent state practical now, and for the same reasons: distribution, replication, consistency, and retrieval in one place instead of four.

If you want to see what that feels like in practice, spin up a TiDB Cloud Zero instance. It takes seconds, requires no sign-up, and gives you a MySQL-compatible database with vector search that you can point an agent at immediately.

FAQ

What Is Distributed Database Architecture?

Distributed database architecture spreads data and processing across multiple nodes instead of concentrating them on a single centralized server. This architecture improves scalability, availability, fault tolerance, and performance for applications that need to serve users or workloads at scale.

How Is Distributed Database Architecture Different from Monolithic Database Architecture?

Monolithic database architecture typically relies on a centralized system that scales vertically, while distributed database architecture scales horizontally across multiple nodes. Distributed systems handle larger workloads, tolerate failures, and support more flexible deployment models.

Why Did Companies Move from Monolithic Databases to Distributed Databases?

Companies moved toward distributed databases because modern applications require higher availability, global scale, real-time data access, and resilience than traditional scale-up architectures can easily provide. Distributed databases reduce single points of failure and make it easier to scale as workloads grow.

Where Does Distributed SQL Fit into Distributed Database Architecture?

Distributed SQL is a type of distributed database architecture that preserves SQL, relational data modeling, and transactional consistency while distributing data across multiple nodes. It serves teams that need horizontal scale without giving up familiar relational database capabilities.

Why Do AI Agents Need a Distributed Database for Memory?

AI agents are stateless between calls, and a context window is a temporary buffer rather than durable memory. Agents need persistent state that outlives a session: facts learned earlier, task history, tool outputs, files, and retrieval metadata. A distributed database can hold structured records and embeddings together, serve exact lookups and semantic search from one query, and apply transactional guarantees to memory that gets updated, superseded, or deleted.

Planet for the MySQL Community

Curated MySQL Data Sets for Realistic Testing

https://ronaldbradford.com/images/blog/mysql-curated-datasets.pngSynthetic benchmarks have their place, but I have always preferred working with real data. Not client production data — that stays private — but publicly available datasets that reflect the messy shapes, skewed distributions, and indexing challenges you encounter in the wild.Planet MySQL

Laravel NaturalQuery: ask your database questions in English, by voice or text

https://opengraph.githubassets.com/3f074d12b607463f214c915379044d7a1ad86276c55e5b80c10393bd76739318/jay123anta/laravel-natural-query

Let people ask your database questions in English – by voice or by typing –
without your data ever leaving your server.

Tests
License: MIT

Packagist
Downloads
PHP

The AI is sent your schema structure only – table names, column names,
types, and the words your users use for them. It returns SQL. Your server
validates that SQL, runs it locally, and formats the rows. Not one row is
ever sent upstream
, and that is enforced by tests, not by intent.

Any model, hosted or your own. Gemini, Claude, OpenAI, DeepSeek, Mistral,
Groq, OpenRouter – or a model you run yourself on Ollama, vLLM, LM Studio or
llama.cpp. One config block, no code changes. Self-hosting goes further still:
nothing leaves your network at all, not even the schema.

$result = NaturalQuery::query("top 5 customers by revenue");

Or drop the whole UI – chat thread, microphone, charts – into any Blade view:

<x-naturalquery::widget />

Requires PHP 8.2+ and Laravel 12 or 13. Works on PostgreSQL, MySQL,
MariaDB and SQLite.

composer require jayanta/laravel-natural-query
php artisan naturalquery:install
php artisan migrate

Choose a model in .env. A model you run yourself is a first-class choice, not
a fallback:

# Local, no API key, nothing leaves your machine
NATURALQUERY_LLM_DRIVER=ollama
OLLAMA_MODEL=llama3.3

# Or a hosted API
NATURALQUERY_LLM_DRIVER=gemini
GEMINI_API_KEY=your-key-here

Built-in drivers: ollama, gemini, openai, claude. Any other
OpenAI-compatible service – DeepSeek, Groq, Mistral, OpenRouter, vLLM,
LM Studio, LocalAI – plugs in with a base_url and a model; see
docs/PROVIDERS.md.

php artisan naturalquery:discover --ai

This is the whole adaptation step. The package knows nothing about your
application: this reads your database and writes one plain PHP file per table
into config/naturalquery-schemas/. Those files are the only thing that makes
it understand your domain – no code changes, no subclassing.

--ai also fills in the human layer that cannot be read from a database:
descriptions, the words your users actually say, business rules, and computed
metrics like averages. Worth doing – without it, a question like "average
amount" costs an extra API call to answer.

Then check it:

php artisan naturalquery:doctor

It names the real cause of any problem and prints the exact fix. Run it first
whenever something is wrong.

use Jayanta\NaturalQuery\Facades\NaturalQuery;

$result = NaturalQuery::query('total revenue by region last month');

Show parsed_query to your users. It states which measure, breakdown,
filters and dates were actually used – the difference between someone catching
a misreading and believing a number that answers a different question.

Or over HTTP, which is what the widget uses:

POST /naturalquery/text          {"text": "top 5 customers by revenue"}
POST /naturalquery/conversation  {"session_id": "abc", "text": "only in West"}

The browser listens. Your server only ever receives text.

<x-naturalquery::widget />   

There is nothing to configure and no audio endpoint. The widget uses the
browser’s SpeechRecognition to turn speech into English text on the device,
then posts that text exactly as if it had been typed. Three things follow from
that one decision:

  • It works with every model – Gemini, Claude, Ollama, anything – because by
    the time the model is involved it is reading a sentence, not hearing a
    recording.
  • No audio leaves the device. Not to your server, not to a provider. There
    is no upload path in the package at all.
  • Nothing extra to set up or pay for – no transcription service, no second
    API key, no added latency.

Answers carry a speech_text field phrased for reading aloud, and the widget
speaks it. Chrome, Edge and Safari support recognition; Firefox does not, so
the microphone is hidden there and people type – which is why text input is
never optional.

language picks which English accent to listen for – en-IN recognises
Indian English far more accurately than en-US does:

<x-naturalquery::widget language="en-IN" />

English only, on purpose. Multilingual belongs to a separate package with a
speech pipeline of its own; this one stays an English natural-language-to-SQL
assistant. → docs/WIDGET.md

These endpoints spend your API key, so they are not open by default.

Who gets in
A viewNaturalQuery gate you define Whatever the gate says
No gate, local or testing Everyone – so it works the moment you install it
No gate, anywhere else Signed-in users only
// AppServiceProvider::boot()
Gate::define('viewNaturalQuery', fn ($user) => $user->isAdmin());

Define the gate as soon as this is more than you: an ungated endpoint in
production is an LLM proxy for the internet.


What it is good at, and what it is not

It works well on datasets you have described. Told that revenue is a
measure to total, that users say "client" for customer_name, and that
cancelled orders do not count, it is reliable for the questions those datasets
are meant to answer.

It is not magic. Pointed at an undescribed database and asked something
vague, any text-to-SQL system will sometimes produce a confident, wrong answer.

Measured against the Spider benchmark – real questions, unfamiliar databases,
no curation – it answers 29 of 36 (81%). Read that as: roughly one question
in five is wrong on an uncurated schema. On a described one it is far better,
which is why the schema files matter more than anything else you will do.

The honest framing is a fast analyst for datasets you have curated, not an
oracle for arbitrary databases. Every mitigation here follows from that: SQL is
SELECT-only and restricted to your tables, doctor catches schema drift, and
every answer shows the query it understood.

Seventeen cases whose answers are arithmetic on three seeded rows – totals,
filters, averages, periods, a decomposed comparison, and a conversation that
narrows, drills down and rewinds:

Model Result
Gemini 2.5 Flash 17/17
Claude Sonnet 5 17/17
DeepSeek v4 Flash 17/17
Mistral Large 17/17
Llama 3.3 70B (open weights) 17/17
Llama 3.1 8B (open weights) 12/17

Model size matters more than vendor. The 70B open-weight model scores the
same as the four frontier hosted ones, and runs on a single good GPU. The 8B
drops filters and ignores date periods – asked for July it returns the whole
table, confidently – so use a 70B-class model or better wherever a wrong
number matters.

Conversation state is the exception worth noting: narrowing, drill-down and
rewind pass even on the 8B, because they are resolved in PHP rather than left
to the model.

NATURALQUERY_CONFORMANCE=1 NATURALQUERY_LLM_DRIVER=claude \
NATURALQUERY_CONFORMANCE_KEY=sk-... vendor/bin/phpunit --testsuite Conformance

Run any battery more than once before believing it. On a free tier the first
pass often measures the rate limit rather than the model – add
NATURALQUERY_CONFORMANCE_DELAY=15 to space the calls out.


docs/SCHEMA.md Schema files in full – metrics, aliases, joins, many tables
docs/API.md Every endpoint, field and error code – plus events and token cost
docs/CONVERSATIONS.md Follow-ups, drill-downs, rewind, multi-step answers
docs/PROVIDERS.md Every LLM driver, and adding your own
docs/WIDGET.md The bundled UI and browser voice input
docs/SECURITY.md The privacy wall, SQL validation, prompt-injection guard
docs/TROUBLESHOOTING.md What each error means and how to fix it
php artisan naturalquery:doctor      # diagnose setup problems, print the fix
php artisan naturalquery:discover    # generate schema files from your database
php artisan naturalquery:install     # publish config and migrations
php artisan naturalquery:debug ""   # show the exact prompt sent to the AI
php artisan naturalquery:cache-cleanup

vendor/bin/phpunit must pass and the widget must pass node --check. New
behaviour gets a test; every failure a real user hits becomes a regression
test.

MIT. See LICENSE.

Laravel News Links

Enforcing enterprise conventions with Spatie guidelines skills and Laravel Boost

https://origin-main.com/wp-content/uploads/2026/08/feat-8813-1.png

🕒  6 Minute Read    📅  Date Published: August 9, 2026

AI coding agents write functional PHP fast, but speed isn’t the same as adherence. Left alone, an agent will happily produce verbose control flow, stale docblocks, and framework anti-patterns that any senior reviewer would bounce in code review. Spatie tackled this by packaging their production coding standards as spatie guidelines skills for Laravel Boost, a machine-readable format that agents like Claude Code, Cursor, and GitHub Copilot can load on demand instead of guessing at your team’s conventions.

This article is part of our wider AI Architecture Module coverage of agent tooling, where Laravel Boost and MCP infrastructure provide coding agents the context they need to ensure AI-generated code follows vendor-specified conventions, not just that it compiles.

If you haven’t wired up the transport layer yet, our guide to rolling out Boost across a Laravel 13 project covers the installation and agent registration steps this article assumes.

The skill activation pipeline

Why machine-readable skills beat static prompt files

A .cursorrules file or a bloated CLAUDE.md gets loaded into every single turn whether it’s relevant or not. That’s wasted context on a JavaScript formatting rule while you’re editing a migration. Skills packages solve this differently.

  • Context on demand. Agents activate spatie-laravel-php or spatie-security only when the file being touched matches, not on every prompt.
  • Team-wide consistency. Installing through Composer means every engineer, and every CI runner, pulls the identical ruleset. No copy-pasted markdown drifting between machines.
  • One-command updates. Spatie ships changes to the package; your team pulls them with a single sync command instead of manually re-pasting guideline files.

[Architect’s Note] The distinction matters more than it sounds. A prompt file is advisory, the agent reads it and might follow it. A skill is conditional, it fires deterministically on file type and task. That’s the difference between a suggestion and a guardrail.

Installing Spatie’s guidelines skills via Laravel Boost

Laravel Boost is the primary transport for registering skill packages inside a Laravel 13 project. Install both dev dependencies through Composer:

composer require laravel/boost --dev
composer require spatie/guidelines-skills --dev

Link the repository immediately, before you run anything else: spatie/guidelines-skills on GitHub. That’s the source of truth for the skill definitions you’re about to install, and it’s the first place to check when a rule looks off.

Running the installer

php artisan boost:install

Select Spatie guidelines from the vendor list, then choose your active coding agent. Boost writes the agent binding files, one skill per convention area: spatie-laravel-php, spatie-javascript, spatie-version-control, and spatie-security.

[Production Pitfall] Don’t skip .boost/skills/ in your .gitignore by habit. Teams that treat generated agent config as disposable end up with every developer running a different skill version locally, which defeats the entire point of pulling this through Composer in the first place.

Running outside Laravel Boost: skills.sh and global Claude Code

Not every project runs Boost. A standalone JavaScript service, a non-Laravel PHP microservice, or a developer who just wants Spatie’s PHP conventions available everywhere still needs a path in.

Global Claude Code integration

To apply the conventions across every local repository without touching individual projects, register them in your user home directory:

mkdir -p ~/.claude && touch ~/.claude/CLAUDE.md
curl -o ~/.claude/laravel-php-guidelines.md https://spatie.be/laravel-php-ai-guidelines.md
echo -e '\n## Coding standards\nWhen working with Laravel/PHP projects, first read the coding guidelines at @~/.claude/laravel-php-guidelines.md' >> ~/.claude/CLAUDE.md

Runtime-agnostic install via skills.sh

For non-Laravel services, install through the cross-runtime skills.sh registry directly:

npx skills add spatie/guidelines-skills

This gets you the same four skills without a Composer dependency, and it’s the path that works across Claude Code, Cursor, Codex, and Copilot uniformly.

[Word to the Wise] If your team runs a mixed stack, decide up front which install method is canonical for which repo type. Having half your services pull skills through Boost and the other half through skills.sh isn’t wrong, but it does mean two update commands to remember, and someone will forget the second one.

Keeping guidelines current: Context7 and CI sync

Enforcing syntax conventions only solves half the problem. When an agent generates code against a specific Spatie package, spatie/laravel-permission, spatie/laravel-medialibrary, it needs accurate, current API signatures, not whatever the model’s training data happened to capture. That’s a separate failure mode from style drift, and it’s the one that produces confidently wrong method calls.

To close that gap, wire in the Context7 MCP server. For background on building or extending your own protocol gateways, see our guide on production MCP server infrastructure in Laravel.

{
  "mcpServers": {
    "context7": {
      "command": "npx",
      "args": ["-y", "@upstash/context7-mcp@latest"]
    }
  }
}

With this configured, an agent implementing a Spatie package feature queries Context7 for live documentation instead of relying on stale training data.

Automating the sync

Guidelines evolve. Keeping every developer’s local skill definitions current, and catching drift in CI before it ships, means treating the sync as a scripted step rather than a reminder in Slack:

{
    "scripts": {
        "update-ai-skills": [
            "composer update spatie/guidelines-skills",
            "php artisan boost:update"
        ]
    }
}

Run composer run update-ai-skills during local setup and again as a pull-request check. This is the same discipline we cover in our CI pipeline guide for AI applications: the goal isn’t just passing tests, it’s guaranteeing the agent instructions in front of every contributor match the security and architectural rules currently in force.

[Efficiency Gain] Bundling boost:update into the same CI step you already run for schema validation costs you nothing extra in pipeline time and removes an entire class of “works on my machine, why is the agent doing something different” tickets.

Operational guardrails for teams at scale

A few practices separate a clean rollout from one that quietly rots over six months.

  • Prune unused skill drivers. If your application is a pure API backend, disable spatie-javascript. Every active skill costs agent context; loading rules for code you don’t write is pure waste.
  • Audit generated output, don’t trust it blindly. Skills reduce style violations, they don’t eliminate the need for review. Check that agent-produced code actually respects PSR-12 and Spatie’s return-type conventions rather than assuming the skill fired correctly.
  • Keep automated formatting as your final gate. AI guidelines catch structural and naming issues; a vendor/bin/pint pre-commit hook is still your deterministic backstop for whitespace and formatting drift that skills won’t always reach.
  • Match your local environment to what CI enforces. If your team’s dev containers differ from what agents run against locally, guideline enforcement gets inconsistent fast. Our breakdown of a 2026 Laravel AI development stack covers keeping that parity intact.

[Edge Case Alert] Watch for skill collisions when a project pulls in guidelines from more than one vendor package. If a second guidelines package also defines a spatie-laravel-php-equivalent skill, Boost doesn’t merge them, it’s whichever one installs last. Audit .boost/skills/ after any multi-package install, not just after a single-package one.

Where this fits in your AI governance stack

Guidelines skills solve a narrower problem than most of what we cover in Production-Grade AI Architecture in Laravel: they’re about what the agent writes, not what it decides or how much it costs to run. But narrow doesn’t mean small. Style and convention drift is one of the quieter failure modes in agentic development, it doesn’t throw an exception, it just accumulates until a codebase written by five different agent sessions looks like it was written by five different developers who never talked to each other.

The pattern here generalizes past Spatie’s specific rules. Any team with an opinionated internal style guide can package it the same way: distilled into skills, distributed through Boost or skills.sh, kept current through Context7 and a CI sync step. The specific package changes. The architecture, deterministic, versioned, and enforced at the point of generation rather than the point of review, doesn’t.

Additional external references

– Laravel Boost documentation (https://spatie.be/guidelines/ai)
– Context7 MCP server on GitHub (https://github.com/upstash/context7)


Frequently Asked Questions

Does spatie/guidelines-skills replace spatie/boost-spatie-guidelines?

Yes. The older package was Boost-only; the new one works with both Boost and skills.sh, and covers everything the old package did.

Do I need Laravel Boost to use Spatie’s guidelines?

No. The skills.sh install path (npx skills add spatie/guidelines-skills) works independently of Boost, which matters for non-Laravel services in a polyglot stack.

Will these skills conflict with my own custom AI guideline files?

Not automatically, but they can collide if another package defines a similarly named skill. Audit .boost/skills/ after installing, especially in multi-package setups.

Does this replace code review?

No. Skills reduce the volume of style and convention violations an agent introduces; they don’t replace human review or your automated formatting gate (Pint).

Dewald Hugo

A software architect with 15+ years of experience in the PHP and Laravel ecosystem. Dewald created Origin Main to provide the engineering rigour required to integrate AI into professional, high-concurrency production systems. He writes for developers who care less about "getting it to work" and more about "getting it to last".

Laravel News Links

Laravel Truss 1.8: feed your real schema to a coding agent (MCP + context export)

https://albertoarena.it/images/posts/my-coding-agent-kept-inventing-columns/cover.jpg

Ask a coding agent to write a query against a table it hasn’t seen this session, and it will guess. Confidently, plausibly, and wrong: a foreign key named author_id when the column is actually created_by, a status treated as a free-text string when it’s a tinyint enum, a table it’s sure exists because a table like it usually does. It isn’t lying, it just doesn’t know, and nothing forces it to say so.

The usual fix is pasting a schema dump into the chat at the start of a session. That works until the next migration, at which point it’s just a different kind of wrong: confidently out of date instead of confidently invented.

There’s an obvious third option: let the agent run queries itself, most coding agents can already execute SQL. But that means handing over real database credentials, and a connection that can query can also see rows, not just structure. That’s a bigger grant than the problem needs.

Truss has always been a live, zoomable ER diagram of your Laravel app’s real database schema, structure only, never a row of data. Version 1.8 points that same live structure at a coding agent instead of a browser tab.

Give it meaning a type can’t carry

A column tells an agent its name and type, not what it means. status = 1 doesn’t say “paid” on its own. Annotate it once and every export carries it:

// config/truss.php
'annotations' => [
 'source' => ['config', 'database'],
 'tables' => [
 'orders' => [
 'note' => 'One row per checkout attempt, not per completed order.',
 'columns' => [
 'status' => 'tinyint: 0 pending, 1 paid, 2 refunded',
 ],
 ],
 ],
],

If your database already carries COMMENT strings on tables and columns, leave 'database' in annotations.source and Truss reads those directly, no duplicate config to keep in sync. Either way, a comment is part of the CREATE TABLE definition, not a row: still structure only. Strip them from any single export with --no-annotations when you just want the bare shape.

Trim it to what the question needs

A forty-table schema is a lot of tokens to spend on a question about one table. --compact drops column defaults and non-unique indexes without dropping a single table, column, or foreign key. --focus=orders --depth=1 narrows the export to one table and its foreign-key neighbourhood, the same idea as the dashboard’s focus mode, now available from the command line. And there’s a new llm format alongside the existing five (DBML, JSON, CSV, Markdown, Mermaid), a dense plaintext export tuned for a token budget rather than for a human reading a data dictionary:

php artisan truss:export --format=llm --focus=orders --depth=1 --compact

That’s what the export produces. Calling it is just as direct, in code or over HTTP.

Building the same thing in code goes through a new fluent, immutable Truss facade instead of the command:

Truss::snapshot()->focus('orders', depth: 1)->compact()->toDbml();

And a gated GET {prefix}/export/{format} route serves the identical output to any HTTP client, behind the same viewTruss gate as the dashboard. Command, facade, route, dashboard download: one pipeline underneath all four, so they can never quietly disagree with each other.

Ask it live, instead of pasting a snapshot

The part I actually wanted, though, wasn’t a better export. It was not exporting anything at all.

Truss 1.8 adds an optional server for MCP, the Model Context Protocol that Claude Code, Claude Desktop, and Cursor use to reach outside tools. Built on laravel/mcp, it talks to a coding agent directly over local stdio:

composer require laravel/mcp
php artisan mcp:start truss

Point one at it and the agent gets five tools, list_tables, describe_table, get_schema, focus_table, and get_structural_review, plus a truss://schema resource, all reading the live schema on demand. Every tool advertises MCP’s readOnlyHint, so a client can present them as read-only instead of prompting for write approval on a call that was never going to write anything. No row data, ever, and the same exclusion and managed-connection safeguards as the rest of Truss apply here too, opt-in and off by default behind truss.mcp.enabled.

I pointed it at a real project I’ve worked on for a while, in Claude Desktop, and the difference was immediate: instead of me pasting a schema dump at the start of the conversation, or the agent asking me to run a query to check a column name, it just called describe_table before it wrote anything, the same check that would have caught the guessed author_id from the start of this post. No staleness, because there’s nothing to go stale, it’s reading the same live introspection the diagram uses.

Try it

Update with composer update albertoarena/laravel-truss, and if you want the MCP server too, composer require laravel/mcp on top.

What’s next

More truss:doctor rules and CI-native output formats are next on the roadmap, followed by reading Eloquent relationships for semantic edge labels instead of raw foreign keys, and navigation aids for schemas with a hundred tables or more. If a tool the agent needs isn’t there yet, or an annotation source you’d want isn’t supported, open a discussion.

Laravel News Links

Laravel Discount: Coupon Codes, Usage Limits, and Stacking

https://laravelnews.s3.amazonaws.com/featured-images/Laravel-Discount-LN.png

Some Laravel applications that sell products or services need more than a flat percentage off. An e-commerce store might schedule a seasonal sale, or a SaaS product might hand out a launch coupon, and a discount that starts at 10% off can be capped at a maximum, tied to a minimum order total, a per-customer limit, and an expiry date. Laravel Discount by Milwad Khosravi stores all of that on Eloquent models and evaluates it through a single facade.

Here’s what the package covers:

  • Two discount types: DiscountType::Percentage and DiscountType::Fixed, with an optional max_discount_amount cap.
  • Coupon codes: Discounts with a code act as coupons, while codeless discounts apply automatically.
  • Time windows: starts_at and expires_at columns, paired with a valid() query scope.
  • Usage limits: Total usage_limit and per-customer usage_limit_per_user enforcement.
  • Guest support: Pass a session ID to enforce per-user limits for unauthenticated users.
  • Stacking rules: Mark discounts is_stackable and the package works out which combination saves the customer the most.
  • Discountable models: The HasDiscounts trait attaches discounts to any Eloquent model.
  • Cart integration: A CartDiscount service that applies codes to a Laravel Cart total or to a single cart item.

Percentage and Fixed Discounts

A discount is a standard Eloquent model, so you create one like any other record:

use Binafy\LaravelDiscount\Enums\DiscountType;

use Binafy\LaravelDiscount\Models\Discount;

 

$discount = Discount::query()->create([

'name' => 'Summer Sale',

'type' => DiscountType::Percentage,

'value' => 20,

]);

Applying it goes through the LaravelDiscount facade, which validates the discount before calculating anything and hands back a DiscountResult:

use Binafy\LaravelDiscount\Facades\LaravelDiscount;

 

$result = LaravelDiscount::apply($discount, 200);

 

$result->originalAmount; // 200.0

$result->discountAmount; // 40.0

$result->payableAmount(); // 160.0

The DiscountResult object holds the applied discounts, the original total, and the calculated discount value. payableAmount() subtracts the discount from the original total, clamping the result to a minimum of zero so that fixed discounts never yield negative totals.

A discount of either type can carry a ceiling through max_discount_amount, which is what covers the "20% off, up to $100" case that otherwise ends up hardcoded in a controller:

$discount = Discount::query()->create([

'code' => 'SAVE20',

'type' => DiscountType::Percentage,

'value' => 20,

'max_discount_amount' => 100,

]);

 

LaravelDiscount::apply($discount, 300)->discountAmount; // 60.0

LaravelDiscount::apply($discount, 1000)->discountAmount; // 100.0

Discount Codes, Expiry, and Usage Limits

Setting a code column turns a discount into a promotional coupon. applyCode() validates the code and throws DiscountNotFoundException if no match exists:

$result = LaravelDiscount::applyCode('WELCOME10', 200, $user);

For campaigns where every customer needs their own code, the package builds them with random_int() and leaves out ambiguous characters like 0/O and 1/I, so nobody misreads a code off a printed card:

LaravelDiscount::generateCode(); // "8FJ2K9QW"

LaravelDiscount::generateCodes(100, 'VIP'); // Collection of 100 unique codes

Length, alphabet, prefix, and separator all live under the codes key in config/laravel-discount.php.

Time-limited offers use starts_at and expires_at. Applying before the window throws DiscountNotStartedException, applying after it throws DiscountExpiredException and fires a DiscountExpired event. Querying for what is live right now is a scope:

Discount::query()->valid()->get();

usage_limit caps total redemptions, and usage_limit_per_user caps them per customer, but neither is settled at the time of application alone. You call redeem() when the order is actually finalised:

LaravelDiscount::redeem($discount, $user, $result->discountAmount);

That runs within a transaction and increments used_count by the limit in the where clause, so the database decides whether the increment occurs at all. Update zero rows and the limit was already reached, which surfaces as DiscountUsageLimitReachedException. Two customers claiming the hundredth spot at the same moment cannot both win it.

Guests get the same treatment through a session ID instead of a user model, tracked in the session_id column of discount_usages alongside the nullable user_id:

$result = LaravelDiscount::applyCode('GUEST10', $total, sessionId: session()->getId());

 

LaravelDiscount::redeem($discount, amount: $result->discountAmount, sessionId: session()->getId());

Conditional and Stackable Discounts

A min_order_value column gates a discount behind a spending threshold and throws MinimumOrderValueException when the total falls short. There’s also a conditions JSON column for storing your own condition data, which is where you’d hang anything the package doesn’t model natively. If you want conditions expressed as composable PHP objects instead, the Discountify package takes that approach.

Discounts can also attach to models. Add the HasDiscounts trait, and you get a polymorphic relationship backed by the discountables table:

use Binafy\LaravelDiscount\Traits\HasDiscounts;

 

class Product extends Model

{

use HasDiscounts;

}

$product->discounts()->attach($discount);

$product->validDiscounts();

$product->hasDiscount('TECH10');

 

$result = $product->applyDiscounts($product->price);

That last call routes through applyMany(), which is the stacking resolver. It drops any discount that fails validation, splits the rest into stackable and non-stackable, sums the stackable ones (capped at the order total), finds the single best non-stackable one, and returns whichever side saves more:

$result = LaravelDiscount::applyMany([$tenPercent, $tenFixed, $bigSolo], 100);

 

$result->discounts; // the ones that actually applied

$result->discountAmount; // the winning total

The discounts collection on the result matters here. After a stacking decision, you often need to show the customer which codes made the cut, and that collection is the answer.

Laravel Cart Integration

Install binafy/laravel-cart, the cart package from the same author that we covered previously, and a CartDiscount service becomes available for cart-level and item-level discounts:

use Binafy\LaravelDiscount\Integrations\LaravelCart\CartDiscount;

 

$cartDiscount = app(CartDiscount::class);

 

$result = $cartDiscount->applyToCart($cart, 'SUMMER-8FJ2K9QW');

 

$result = $cartDiscount->applyToItem($cartItem, $discount);

 

$result = $cartDiscount->applyItemDiscounts($cart);

applyToCart() checks the cart total against min_order_value and pulls the cart’s user for the per-user limit, so you don’t pass either one yourself. applyToItem() works against price multiplied by quantity for a single line. applyItemDiscounts() walks the cart and applies whatever each item’s underlying model has attached through HasDiscounts, which is how you run a product-level sale across a whole basket without touching the cart total.

Validation, Exceptions, and Events

Checkout forms need to reject invalid code before anything else happens, and the package includes a ValidDiscountCode rule for that. Its message names the actual reason rather than failing generically:

use Binafy\LaravelDiscount\Rules\ValidDiscountCode;

 

public function rules(): array

{

return [

'code' => ['required', new ValidDiscountCode(

orderAmount: $this->cartTotal(),

user: $this->user(),

)],

];

}

It’s a standard rule object, so it composes with the rest of a form request the same way any custom validation rule does.

Outside validation, every failure case has its own exception extending DiscountException: DiscountNotFoundException, DiscountNotActiveException, DiscountNotStartedException, DiscountExpiredException, DiscountUsageLimitReachedException, and MinimumOrderValueException. Each one exposes the discount that failed through getDiscount(), so you can catch the specific case you want to message differently and let the base class handle the rest:

try {

$result = LaravelDiscount::applyCode($code, $total, $user);

} catch (DiscountExpiredException $e) {

return back()->withErrors("Code {$e->getDiscount()->code} has expired.");

} catch (DiscountException $e) {

return back()->withErrors($e->getMessage());

}

Three events cover the lifecycle: DiscountApplied when discounts are applied to an amount, DiscountRedeemed after a redemption transaction commits, and DiscountExpired when validation hits an expired discount. The redeemed event carries both the discount and the usage row, which is enough to drive analytics or a notification without querying again.

Installation

Laravel Discount requires PHP 8.1+ and Laravel 9-13. Install the package via Composer and run the migrations:

composer require binafy/laravel-discount

php artisan migrate

The service provider registers itself, and the migrations create discounts, discount_usages, and discountables. Publishing the config is optional, and only worth doing if you need to change table names, point at a different user model, or adjust code generation defaults:

php artisan vendor:publish --tag="laravel-discount-config"

Two Artisan commands come along with it. discount:generate produces codes from the terminal, and discount:prune deletes expired discounts along with their usage records, which is a reasonable thing to put on the scheduler:

Schedule::command('discount:prune --days=30')->daily();

The full documentation, including the config reference, is on the Laravel Discount GitHub repository.

Laravel News

Slashdot Reader Builds a Photo-Verification App for iPhones

Long-time Slashdot reader BrianFagioli is announcing that he’s released a new iPhone app that creates a cryptographic witness for photos without uploading the original image.
The app hashes the file, signs the hash using a dedicated identity stored in Apple Keychain, and publishes the witness to Nostr relays while keeping the photo inside the app unless the user chooses to export it. Rather than trying to determine whether a scene was genuine, the app — named Veridenz — answers a narrower question, by verifying whether a photo file matches the one that was originally witnessed.
Users do not need a Nostr account because the app creates its own signing identity, keeping private documentation separate from a public Nostr profile. "The developer does not collect any data from this app," says its page in Apple’s iPhone store. The app’s tagline is "Capture. Prove. Verify."


Read more of this story at Slashdot.

Slashdot