Building a frontend dashboard where users can manage their own content is a common requirement for modern web applications. While Craft CMS makes it incredibly easy to upload files via frontend forms, allowing users to delete assets from a frontend form requires a more nuanced approach to ensure security and data integrity.

In this guide, we will explore two primary methods for handling asset deletion: using Craft's native controller actions and building a custom module for granular permission control. Whether you are building a client portal or a community-driven project manager, these techniques will help you provide a seamless experience for your users.

Understanding the Challenge

When a user uploads a file through a frontend form, that file becomes a part of the Craft Asset library. Deleting that file is not just about removing a reference in a field; it involves removing the record from the database and deleting the physical file from the storage volume.

By default, Craft CMS protects these actions behind administrative permissions. If you simply point a frontend button to a delete action without the proper setup, you will likely encounter 403 Forbidden errors or, worse, open up security vulnerabilities where one user could potentially delete another user's files.

Method 1: Using Native Controller Actions

Craft CMS provides built-in controller actions to handle asset deletion. This is the fastest way to get a prototype working. You can trigger these actions via a POST request, typically using AJAX for a better user experience.

Craft 2 Implementation

In legacy Craft 2 installations, the action is assets/deleteFile.

$('#delete-file').click(function() {
    var data = {
        fileId: 100 // The ID of the asset you want to delete
    };

    $.post('/actions/assets/deleteFile', data, function(response) {
        if(response.success) {
            alert('File deleted successfully.');
        }
    });
});

Craft 3 and Craft 4/5 Implementation

In modern versions of Craft, the action has been updated to assets/delete-asset.

$('#delete-file').click(function() {
    var data = {
        assetId: 100,
        // Ensure you include the CSRF token for modern Craft installs
        [window.csrfTokenName]: window.csrfTokenValue 
    };

    $.post('/actions/assets/delete-asset', data, function(response) {
        console.log('Delete response:', response);
    });
});

The Major Limitation

While this method is straightforward, it comes with a significant security caveat. For this action to work, the logged-in user must have a role with the "Remove files" permission for the specific Asset Volume.

Warning: Granting this permission allows that user to delete any file in that volume, not just the ones they uploaded. For most public-facing sites, this is a major security risk. This leads us to the recommended, secure approach.

Method 2: The Secure Custom Module Approach

To ensure users can only delete their own files, you should write a small custom module. This allows you to perform an "ownership check" before the deletion occurs. This is the gold standard for frontend asset management in Craft CMS.

Creating the Controller Logic

In your custom module, you will create a controller action that validates the request. In this example, we assume the user's files are stored in a folder named after their username.

Craft 3/4/5 Controller Example

use Craft;
use craft\web\Controller;
use yii\web\BadRequestHttpException;
use yii\web\ForbiddenHttpException;
use yii\web\Response;

class MyController extends Controller
{
    // Allow this action to be accessed by frontend users
    protected array|int|bool $allowAnonymous = false;

    public function actionDeleteFile(): Response
    {    
        // Ensure the request is AJAX/JSON
        $this->requireAcceptsJson();

        $assetId = Craft::$app->request->getRequiredBodyParam('fileId');
        $asset = Craft::$app->assets->getAssetById($assetId);

        if (!$asset) {
            throw new BadRequestHttpException('Invalid asset ID: ' . $assetId);
        }

        // SECURITY CHECK: 
        // Verify the asset is in a folder matching the current user's username
        $currentUser = Craft::$app->user->getIdentity();
        if ($asset->getFolder()->name != $currentUser->username) {
            throw new ForbiddenHttpException('You do not have permission to delete this asset.');
        }

        // If check passes, delete the element
        $success = Craft::$app->elements->deleteElement($asset);

        return $this->asJson([
            'success' => $success,
        ]);
    }
}

Why This Works Better

  1. Ownership Validation: The code checks if the asset belongs to the user before calling the delete function.
  2. Abstraction: The user doesn't need global "Remove files" permissions. The controller runs the deletion with system-level authority after your custom logic approves it.
  3. Error Handling: It provides specific HTTP response codes (400 for bad IDs, 403 for unauthorized attempts) which you can handle gracefully in your JavaScript.

Frontend Implementation with CSRF Protection

When sending POST requests to Craft CMS from the frontend, you must include a CSRF (Cross-Site Request Forgery) token if it is enabled in your general.php config.

Here is how you would structure your Twig template and JavaScript to use your new custom controller:

{# In your Twig Template #}
<button class="delete-btn" data-id="{{ myAsset.id }}">Delete Image</button>

<script>
    $('.delete-btn').on('click', function() {
        const assetId = $(this).data('id');
        const container = $(this).closest('.image-wrapper');

        if (confirm('Are you sure you want to delete this file?')) {
            $.ajax({
                type: 'POST',
                url: '/actions/my-module/my-controller/delete-file',
                data: {
                    fileId: assetId,
                    {{ craft.app.config.general.csrfTokenName|json_encode|raw }}: {{ craft.app.request.csrfToken|json_encode|raw }}
                },
                dataType: 'json',
                success: function(response) {
                    if (response.success) {
                        container.remove();
                    }
                },
                error: function(jqXHR) {
                    alert('Error: ' + jqXHR.statusText);
                }
            });
        }
    });
</script>

Best Practices and Common Mistakes

1. Don't Rely Solely on Folder Names

In the example above, we checked ownership by comparing the folder name to the username. While simple, a more robust method is to save the userId in a custom field on the Asset itself during the initial upload. This prevents issues if a user changes their username.

2. Deleting vs. Unlinking

Remember that deleteElement removes the file from the server entirely. If you only want to remove the image from a specific entry but keep the file in the library, you should instead update the Entry and remove that Asset ID from the Assets field.

3. User Feedback

Always include a confirmation dialog (confirm() in JS or a custom modal). Deleting an asset is a destructive action that cannot be easily undone without backups.

Frequently Asked Questions

How do I handle CSRF tokens in Craft CMS frontend forms?

You can output the CSRF token name and value directly in your Twig template using {{ craft.app.request.csrfToken }} and {{ craft.app.config.general.csrfTokenName }}. This must be sent with every POST, PUT, or DELETE request.

Can I delete multiple assets at once?

Yes. Your custom controller can accept an array of IDs instead of a single ID. You would then loop through the IDs, perform ownership checks for each, and use Craft::$app->elements->deleteElement($asset) within the loop.

Does deleting an asset remove it from all entries?

Yes. When an asset is deleted via deleteElement, all relationships in Assets fields across your entire site are automatically cleared, and the physical file is removed from storage.

Wrapping Up

Allowing frontend asset deletion provides a powerful UX for your users but requires careful attention to security. While Craft's native controller actions are available, building a custom module is the recommended path for any production site. By implementing ownership checks and proper AJAX handling, you can create a secure and professional management interface for your Craft CMS projects.

Always remember to verify your implementation against the specific version of Craft you are using, as controller names and service methods can evolve between major releases.