All Articles

Architecture of my open source Laravel monitoring application

· Written by Vincent Bean

In this article I'd like to share how I've setup the codebase for Vigilant, an open source web monitoring application. Vigilant is designed to be an all-in-one solution for monitoring a website, that means from uptime to certificates to CVE's. I wanted to keep the codebase clean and split each of these parts so that it does not become one big mess.

I've built this project using the Laravel Framework, I am familiar with the ecosystem and it contains everything I need to build a web monitoring application. Most importantly it has Horizon, a powerful queueing system.
For the interface I'm using Livewire and Eloquent's global scopes ensure that users do not see each other's resources.
It is also easy to dockerize for self-hosting utilizing Laravel Octane so that I don't have to deal with Nginx and FPM.

But a project with many different components also comes with a challenge, how to keep the codebase clean and organized? By default Laravel ships with an `app` folder in which your models, controllers, and other logic go. This default structure works initially, but quickly becomes overwhelming as the application grows. Each component has models, jobs, and actions so I have a lot of different classes.
We could create folders for each component but that will still be a lot in one place.
The solution for me is to utilize the power of Composer.

The Power of Composer

If you have worked on any modern PHP application you probably know Composer. It is the most popular package manager for PHP. Laravel also heavily utilizes Composer and there are a lot of Composer packages available for Laravel.

One of the key architectural decisions in Vigilant is treating each major feature as a standalone Composer package. To wire them all together, I use Composer’s path repository feature. This is what that looks like in the composer json of the Laravel application:

"repositories": [
    {
        "type": "path",
        "url": "./packages/*"
    },
]

Then I add all the packages using Composer's `@dev` alias:

    "require": {
        "vigilant/certificates": "@dev",
        "vigilant/core": "@dev",
        "vigilant/crawler": "@dev",
        "vigilant/cve": "@dev",
        "vigilant/dns": "@dev",
        "vigilant/frontend": "@dev",
        "vigilant/lighthouse": "@dev",
        "vigilant/notifications": "@dev",
        "vigilant/onboarding": "@dev",
        "vigilant/settings": "@dev",
        "vigilant/sites": "@dev",
        "vigilant/uptime": "@dev",
        "vigilant/users": "@dev"
    },

By structuring your code into independent packages, you naturally promote separation of concerns. Each feature becomes isolated. Models, migrations, jobs, and logic all live together, making your codebase easier maintain and you instantly know in what folder you need to look when you want to find a file.

It also makes it easier to write isolated tests and reduce accidental coupling as the dependencies of the package's composer json are required.

What's inside a package?

Each of the packages contains the following:

  • Migrations

  • Models

  • Commands

  • Jobs

  • Actions

  • Livewire components

  • HTTP controllers

The packages themselves are responsible for registering the things that they need such as migrations, routes, commands, and Livewire components. The main project does not force or expect any of these.

Let's take a look at the `certificates` package, this package contains all of the logic for Vigilant's certificate monitoring. The composer json located in `packages/certificates/composer.json` looks like this:

{
    "name": "vigilant/certificates",
    "description": "Vigilant Certificate Monitor",
    "type": "package",
    "license": "AGPL",
    "require": {
        "php": "^8.3",
        "guzzlehttp/guzzle": "^7.8",
        "laravel/framework": "^12.0",
        "livewire/livewire": "^3.4",
        "vigilant/core": "@dev",
        "vigilant/sites": "@dev",
        "vigilant/users": "@dev",
        "vigilant/frontend": "@dev",
        "vigilant/notifications": "@dev"
    },
    "require-dev": {
        "laravel/pint": "^1.6",
        "larastan/larastan": "^3.0",
        "orchestra/testbench": "^10.0",
        "phpstan/phpstan-mockery": "^2.0",
        "phpunit/phpunit": "^11.0"
    },
    "autoload": {
        "psr-4": {
            "Vigilant\\Certificates\\": "src"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Vigilant\\Certificates\\Tests\\": "tests"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Vigilant\\Certificates\\ServiceProvider"
            ]
        }
    },
    "scripts": {
        "test": "phpunit",
        "analyse": "phpstan",
        "style": "pint --test",
        "quality": [
            "@test",
            "@analyse"
        ]
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "repositories": [
        {
            "type": "path",
            "url": "../*"
        }
    ]
}

As you can see the package name is `vigilant/certificates` which is how the main composer json requires it. Then in the `require` list of this package are the other packages it depends on. For example, the certificate monitor needs to send notifications so it depends on `vigilant/notifications` to do that.

The service provider is set up just like any other Laravel Composer package, it registers routes, configuration, migrations, views, policies/gates, and Livewire components.

But it does not only register the things Laravel needs, it also registers things Vigilant needs. For example, it registers the menu in the file `packages/certificates/resources/navigation.php`:

<?php

use Vigilant\Core\Facades\Navigation;

Navigation::add(route('certificates'), 'Certificates')
    ->icon('phosphor-certificate')
    ->gate('use-certificates')
    ->routeIs('certificate*')
    ->sort(600);

And it registers notifications with their conditions for the notification system:

NotificationRegistry::registerNotification([
    CertificateExpiresInDaysNotification::class,
    CertificateExpiredNotification::class,
]);

NotificationRegistry::registerCondition(CertificateExpiresInDaysNotification::class, [
    SiteCondition::class,    
    DaysCondition::class,
]);

This way each package is responsible for adding to the main application.

Quality checks

I just quickly want to mention quality checks (tests, PHPStan, code style), these are done per package and if you have looked closely at the `composer.json` of the certificates package you have seen that it contains a composer command called `quality`.
This way each package is responsible for which quality checks run.

All these quality checks are run per package in a GitHub workflow using a matrix which makes them all run in parallel, this is a really cool feature that GitHub has. And the best thing is that it creates the matrix automatically using a `ls` command in the packages folder. Check out the GitHub action here.

How to handle common logic and views?

Common logic is put in a special package called the core. The core contains things like validation rules, global scopes, and policies but also middleware and some utility classes.

Frontend lives in two places, the layout is in the main project and the common components are in a package called `frontend`. Each package that has frontend requires `vigilant/frontend` and uses these components.

The only exception is the scheduler, packages themselves do not register their scheduler. Instead this is handled in the main application so that we can quickly see what commands are being scheduled without having to jump through multiple service providers.

But Vigilant is also a SaaS

Vigilant is an open-source project at heart but also has a SaaS component, this is the hosted version of the application. In this version I need things like billing, policies, and an admin panel.
I have chosen to not publish these as open-source software to make it harder for people to create their own SaaS with my code. I think the benefits of open-source software outweigh the risks but I must keep this part closed source to mitigate some of that risk.

These additional things I need for the SaaS are packaged in a separate SaaS repository. This repository is like the `packages` folder in the main project, it contains multiple packages that all register themselves.

During the build process of the SaaS version, it will do a composer require on the SaaS package which adds all the necessary packages.

But as you can imagine the SaaS version has some different requirements, I'm using Laravel's policies and gates to control resource limits but as you might know, if you check if someone may create a model and don't have a policy for that model it will always return false. Vigilant has a setting in the configuration called `edition`. This is `ce` (Community Edition) by default. There is a global helper function to check if it is running in `ce` mode, this is what the service providers use to add an allow all policy to the models. The SaaS version does not run in the `ce` version and the SaaS package brings it's own policies for all the models.

Final Thoughts

Building Vigilant as a modular Laravel application has been a rewarding architectural decision. By leveraging Composer’s path repository feature, each part of the system, from certificates to the SaaS packages, is neatly encapsulated into its own package. This has made the codebase more organized, maintainable, and scalable as the project continues to grow.

Laravel’s ecosystem, combined with tools like Livewire, Horizon, and Octane, provides everything needed to build a modern web application. But Laravel’s real power shines when you break away from the monolithic default structure and start thinking in terms of domains and components.

Modularity doesn’t just help with code clarity, it also enables faster testing, clearer ownership of features, and an easier path to open-sourcing or scaling into SaaS. While managing multiple packages adds some overhead, the long-term benefits in code quality and team collaboration far outweigh the costs.

If you found this helpful, please consider starring the repository 🙏

Start Monitoring within minutes.

Get started with Vigilant in a few minutes, sign up, enter your website and select your monitors.
Vigilant comes with sensible defaults and requires minimal configuration.

Start Monitoring