Soft delete on a intermediate table for many-to-many relationship
Stefan Bogdanescu
Founder & Senior Architect · 2026-06-29
# Soft Deleting Many-to-Many Relationships: The Pivot Table Dilemma
As senior developers, we often encounter scenarios where the standard conventions of Eloquent and database design clash slightly with our desired application logic. One common sticking point is implementing soft deletes on intermediate tablesâthe pivot tables that manage many-to-many relationships. You've added the `deleted_at` column to your junction table, but you're running into confusion about where to place the actual soft delete mechanism within the Laravel ecosystem.
This post will walk you through the correct developer perspective, providing a robust solution for managing soft deletes on many-to-many relationships without needing an unnecessary Eloquent model for the pivot itself.
## Understanding the Role of Intermediate Tables
Intermediate tables (or pivot tables) exist solely to define the association between two other models. They are inherently relational structures; they don't usually represent a core business entity that needs independent CRUD operations or complex lifecycle logic, which is why you often avoid creating a full Eloquent model for them unless absolutely necessary.
When you add a `deleted_at` column to this pivot table (e.g., `post_tag`), you are effectively tracking the *existence* of that specific relationship instance. This is perfectly valid database design. The question then becomes: how do we make Eloquent respect this timestamp when querying?
## The Correct Approach: Leveraging Global Scopes
Since the pivot table doesn't have its own primary model, trying to apply `$softDelete = true;` directly to a non-existent model is impossible. Instead, the power of Laravel lies in applying these rules globally across related models. The solution here is to define a global scope that checks for the `deleted_at` column on the pivot table when querying the parent models.
### Step 1: Migration Setup
First, ensure your migration correctly sets up the relationship structure and the soft delete timestamp.
```php
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->timestamps(); // These will hold the pivot record's timestamps
});
```
### Step 2: Implementing the Global Scope
For soft deletes on pivot tables, we don't scope the pivot table itself; we scope the models that *reference* it. If a post is deleted, all its associated tags should logically disappear from the view.
We can define a global scope in your main models (e.g., `Post` and `Tag`) to handle this logic. While you don't put `$softDelete = true;` on a model that doesn't exist, you use scopes to enforce the deletion rule upon retrieval.
In your `Post` model:
```php
// app/Models/Post.php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes; // This enables soft deletes on the Post model itself
protected static function booted()
{
// When a Post is soft-deleted, we need to ensure its pivot entries are also marked as deleted.
static::deleting(function ($post) {
$post->tags()->update(['deleted_at' => now()]);
});
}
public function tags()
{
// Define the relationship to the pivot table
return $this->belongsToMany(Tag::class);
}
}
```
### Step 3: Querying with Scopes
The magic happens when you modify your query builders. You can extend Eloquent's built-in soft delete functionality for any model, and use custom scopes if necessary to manage the pivot data explicitly. For clean relationship management, leveraging Laravelâs robust features, as seen in the documentation on [Laravel Company](https://laravelcompany.com), is key.
When you query your posts, Eloquent will automatically respect the `deleted_at` timestamp on the `posts` table. To ensure related pivot entries are also excluded, you must explicitly scope the relationship:
```php
// Example: Retrieving a post and its *active* tags
$post = Post::with(['tags' => function ($query) {
$query->whereNull('deleted_at'); // Scope the pivot query to only include active relationships
}])->find(1);
```
## Conclusion
You don't need an intermediate model for a simple many-to-many relationship. The key is understanding that soft deletion logic should be applied either directly to the primary models or managed through custom global scopes acting upon the related pivot data. By using Eloquentâs built-in `SoftDeletes` trait and carefully defining how relationships interact with your pivot table timestamps, you achieve clean, maintainable, and highly relational data management. This approach keeps your database structure lean while ensuring data integrity, which is a core principle of solid Laravel development.