PHPackages                             plin-code/laravel-forge-domain - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [DevOps &amp; Deployment](/categories/devops)
4. /
5. plin-code/laravel-forge-domain

ActiveLibrary[DevOps &amp; Deployment](/categories/devops)

plin-code/laravel-forge-domain
==============================

Onboard tenant and customer hostnames in Laravel with DNS verification and automated Laravel Forge SSL provisioning.

v0.1.0(today)00MITPHPPHP ^8.3CI passing

Since Jun 30Pushed todayCompare

[ Source](https://github.com/plin-code/laravel-forge-domain)[ Packagist](https://packagist.org/packages/plin-code/laravel-forge-domain)[ GitHub Sponsors](https://github.com/PlinCode)[ RSS](/packages/plin-code-laravel-forge-domain/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (18)Versions (2)Used By (0)

forge-domain
============

[](#forge-domain)

[![Latest Version on Packagist](https://camo.githubusercontent.com/37cc300b62e94aa9bc0e06be011037f782be4492baad9e04d900aac54ef9d5a7/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f706c696e2d636f64652f6c61726176656c2d666f7267652d646f6d61696e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/plin-code/laravel-forge-domain)[![Tests](https://camo.githubusercontent.com/477ff06a6c6d9712b6170f4e623b23a43b5edd6ebb28b23d7f23b4d5af3b04d9/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f706c696e2d636f64652f6c61726176656c2d666f7267652d646f6d61696e2f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/plin-code/laravel-forge-domain/actions/workflows/run-tests.yml)[![Code Style](https://camo.githubusercontent.com/e2c927d643ef0610525d0dfe7bc7934beeb9c6e06c5424205be0ca99952437b6/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f706c696e2d636f64652f6c61726176656c2d666f7267652d646f6d61696e2f6669782d7068702d636f64652d7374796c652d6973737565732e796d6c3f6272616e63683d6d61696e266c6162656c3d636f64652532307374796c65267374796c653d666c61742d737175617265)](https://github.com/plin-code/laravel-forge-domain/actions/workflows/fix-php-code-style-issues.yml)[![Total Downloads](https://camo.githubusercontent.com/6fc6c0e79d04df1209895b5d55ed16bbb17f09f164397c4451680e7932644746/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f706c696e2d636f64652f6c61726176656c2d666f7267652d646f6d61696e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/plin-code/laravel-forge-domain)

A Laravel package for onboarding tenant and customer hostnames with DNS verification and a Laravel Forge SSL provisioning flow.

What it solves
--------------

[](#what-it-solves)

Multi-tenant SaaS applications often allow customers to bring their own domains. This package automates the lifecycle from the moment a hostname is submitted until its SSL certificate is active and confirmed on Laravel Forge. It handles DNS verification (CNAME or TXT), Forge API calls, SSL polling, lifecycle events, reconciliation, and renewal, all behind a small, stable facade.

Features
--------

[](#features)

- DNS verification via CNAME or TXT record checks
- Laravel Forge provisioning with SSL creation and activation polling
- Wildcard driver for subdomain hostnames that do not need Forge provisioning
- Lifecycle events for verified, provisioning, activated, failed, and removed states
- Artisan commands for SSL renewal and domain reconciliation
- Shipped `ManagedDomain` Eloquent model (UUID primary key) or bring your own model via the `ProvisionableDomain` contract and the `HasProvisionableDomain` trait
- Test helpers: `FakeForge` and `FakeDnsResolver` for in-process testing without network calls
- Master kill-switch (`FORGE_DOMAIN_MANAGE`) so the package can be installed before Forge credentials exist

Requirements
------------

[](#requirements)

- PHP 8.3 or higher
- Laravel 12 or 13

Installation
------------

[](#installation)

Install via Composer:

```
composer require plin-code/laravel-forge-domain
```

Publish the configuration file:

```
php artisan vendor:publish --tag="forge-domain-config"
```

Publish and run the migration:

```
php artisan vendor:publish --tag="forge-domain-migrations"
php artisan migrate
```

Quick Start
-----------

[](#quick-start)

Call `ForgeDomain::onboard()` after persisting the domain record. The facade dispatches a `VerifyDomainJob` that checks DNS, then hands off to the provisioning driver.

```
use PlinCode\LaravelForgeDomain\Facades\ForgeDomain;
use PlinCode\LaravelForgeDomain\Models\ManagedDomain;
use PlinCode\LaravelForgeDomain\Support\DomainKind;

$domain = ManagedDomain::create([
    'hostname' => 'app.customer.com',
    'kind'     => DomainKind::Custom,
]);

ForgeDomain::onboard($domain);
```

To remove a domain from Forge:

```
ForgeDomain::remove($domain);
```

Configuration
-------------

[](#configuration)

After publishing, the config lives at `config/forge-domain.php`.

### `drivers`

[](#drivers)

Maps each `DomainKind` value to a provisioner driver name. The defaults are:

```
'drivers' => [
    'custom'    => 'forge',
    'subdomain' => 'wildcard',
],
```

### `manage`

[](#manage)

Master kill-switch. When `false`, the forge driver logs operations instead of calling the Forge API. Useful before Forge credentials exist.

```
FORGE_DOMAIN_MANAGE=true

```

### `forge`

[](#forge)

Forge API credentials and target server/site identifiers.

KeyEnv varDescription`token``FORGE_DOMAIN_TOKEN`Forge personal access token`organization``FORGE_DOMAIN_ORGANIZATION`Forge organization slug (optional)`server_id``FORGE_DOMAIN_SERVER_ID`ID of the target Forge server`site_id``FORGE_DOMAIN_SITE_ID`ID of the target Forge site`server_ip``FORGE_DOMAIN_SERVER_IP`Public IP of the server (used for A-record checks)### `verification`

[](#verification)

Controls how DNS ownership is confirmed before provisioning.

KeyEnv varDescription`method``FORGE_DOMAIN_VERIFICATION``cname` or `txt` (default `cname`)`cname_target``FORGE_DOMAIN_CNAME_TARGET`The CNAME value customers must point at`txt_prefix`(hardcoded)Prefix for the TXT record name (default `_forge-verify`)### `ssl`

[](#ssl)

KeyDefaultDescription`active_days``90`Expected SSL validity window in days`renew_days_before``14`Days before expiry at which renewal is triggered`poll_tries``15`Number of polling attempts when waiting for Forge to activate the certificate`poll_backoff``30`Seconds between polling attempts### `reconcile`

[](#reconcile)

KeyDefaultDescription`mode``log`Set to `cleanup` to have the reconciler delete orphaned Forge domains automatically### `models`

[](#models)

Swap out the shipped `ManagedDomain` model with your own:

```
'models' => [
    'managed_domain' => \App\Models\Domain::class,
],
```

Drivers
-------

[](#drivers-1)

### `forge`

[](#forge-1)

The `forge` driver calls the Laravel Forge API to create a domain entry and issue an SSL certificate for the hostname. It polls until the certificate is active, then dispatches `DomainActivated`. When `FORGE_DOMAIN_MANAGE` is `false` the driver logs each step and returns without touching the API.

### `wildcard`

[](#wildcard)

The `wildcard` driver is a no-op provisioner intended for subdomains already covered by a wildcard SSL certificate. It transitions the domain directly to `active` without any Forge API calls.

### How `kind` maps to a driver

[](#how-kind-maps-to-a-driver)

The `drivers` config key maps the string value of `DomainKind` to a driver name:

```
// DomainKind::Custom->value === 'custom'  => 'forge'
// DomainKind::Subdomain->value === 'subdomain' => 'wildcard'
```

You can point either kind at a custom driver name and register your own `DomainProvisioner` implementation in the service container.

Verification
------------

[](#verification-1)

Before provisioning, the package verifies DNS ownership using the method configured in `verification.method`.

**CNAME**: the verifier resolves the CNAME record for the hostname and checks it matches `verification.cname_target`.

**TXT**: the verifier resolves TXT records for `{txt_prefix}.{hostname}` and checks that one of them contains the domain's `verification_token`.

The domain model stores which method was requested in its `verification_method` column.

Models
------

[](#models-1)

### Using the shipped `ManagedDomain`

[](#using-the-shipped-manageddomain)

The package ships `PlinCode\LaravelForgeDomain\Models\ManagedDomain`, which uses UUID primary keys and the `forge_domains` table. It implements `ProvisionableDomain` via the `HasProvisionableDomain` trait and is ready to use out of the box.

### Bringing your own model

[](#bringing-your-own-model)

Implement `ProvisionableDomain` on any Eloquent model and add the `HasProvisionableDomain` trait for the default implementation:

```
use Illuminate\Database\Eloquent\Model;
use PlinCode\LaravelForgeDomain\Concerns\HasProvisionableDomain;
use PlinCode\LaravelForgeDomain\Contracts\ProvisionableDomain;
use PlinCode\LaravelForgeDomain\Support\DomainKind;
use PlinCode\LaravelForgeDomain\Support\DomainStatus;
use PlinCode\LaravelForgeDomain\Support\VerificationMethod;

class Domain extends Model implements ProvisionableDomain
{
    use HasProvisionableDomain;

    protected $casts = [
        'kind'                => DomainKind::class,
        'status'              => DomainStatus::class,
        'verification_method' => VerificationMethod::class,
        'ssl_expires_at'      => 'datetime',
    ];
}
```

Your table must include these columns: `hostname`, `kind`, `status`, `verification_method` (nullable), `verification_token` (nullable), `dns_target` (nullable), `forge_domain_id` (nullable unsigned bigint), `ssl_expires_at` (nullable timestamp), and `failure_reason` (nullable text).

Update the config to point at your model:

```
'models' => [
    'managed_domain' => \App\Models\Domain::class,
],
```

### `ProvisionableDomain` contract

[](#provisionabledomain-contract)

The interface that all domain models must satisfy:

```
interface ProvisionableDomain
{
    public function getKey(): mixed;
    public function getHostname(): string;
    public function getKind(): DomainKind;
    public function getVerificationMethod(): ?VerificationMethod;
    public function getVerificationToken(): ?string;
    public function getDnsTarget(): ?string;
    public function getForgeDomainId(): ?int;
    public function setForgeDomainId(?int $id): void;
    public function getStatus(): DomainStatus;
    public function markVerified(): void;
    public function markProvisioning(): void;
    public function markSslActive(\DateTimeInterface $expiresAt): void;
    public function markFailed(string $reason): void;
    public function markRemoved(): void;
}
```

Events
------

[](#events)

All events carry a public `$domain` property typed as `ProvisionableDomain`.

EventFired when`DomainVerified`DNS verification passes`DomainProvisioning`Forge provisioning begins`DomainActivated`SSL certificate is confirmed active`DomainFailed`Any step fails permanently`DomainRemoved`The domain is deleted from ForgeRegister listeners in your `EventServiceProvider` or using `#[AsEventListener]`:

```
use PlinCode\LaravelForgeDomain\Events\DomainActivated;

public function handle(DomainActivated $event): void
{
    $event->domain->getHostname(); // 'app.customer.com'
}
```

Commands
--------

[](#commands)

### `forge-domain:renew-ssl`

[](#forge-domainrenew-ssl)

Queries for domains whose `ssl_expires_at` is within the configured `renew_days_before` window and dispatches `RenewSslJob` for each one. Run this on a daily schedule:

```
// routes/console.php
Schedule::command('forge-domain:renew-ssl')->daily();
```

### `forge-domain:reconcile`

[](#forge-domainreconcile)

Dispatches `ReconcileDomainsJob`, which compares the domain IDs stored in your database against the domain IDs returned by the Forge API and reports (or cleans up) any divergence. When `reconcile.mode` is `log`, divergences are written to the application log. When set to `cleanup`, orphaned Forge domains are deleted.

> **Warning.** Setting `reconcile.mode` to `cleanup` will permanently delete every Forge domain on the configured site that the package does not track. Only enable this option when the Forge site is dedicated exclusively to package-managed domains (any manually created domain on that site will be removed without further confirmation).

```
Schedule::command('forge-domain:reconcile')->weekly();
```

Testing
-------

[](#testing)

The package ships two test fakes. Swap them in with Laravel's `bind` or `instance` helpers in your test setup.

### `FakeForge`

[](#fakeforge)

An in-memory `ForgeClient` that records creates, certificate state changes, and deletes without hitting the Forge API.

```
use PlinCode\LaravelForgeDomain\Support\FakeForge;
use PlinCode\LaravelForgeDomain\Contracts\ForgeClient;

$fake = new FakeForge();
$this->app->instance(ForgeClient::class, $fake);

// Force a certificate to report as active
$fake->setActive($forgeDomainId, true);

// Assert a domain was created
expect($fake->created)->toHaveKey(1);
```

### `FakeDnsResolver`

[](#fakednsresolver)

An in-memory `DnsResolver` that lets you seed CNAME, A, and TXT records per hostname.

```
use PlinCode\LaravelForgeDomain\Support\FakeDnsResolver;
use PlinCode\LaravelForgeDomain\Contracts\DnsResolver;

$resolver = new FakeDnsResolver();
$resolver->setCname('app.customer.com', ['proxy.myapp.com']);
$resolver->setTxt('_forge-verify.app.customer.com', ['forge-abc123']);

$this->app->instance(DnsResolver::class, $resolver);
```

Run the test suite:

```
composer test
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for recent changes.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Daniele Barbaro](https://github.com/plin-code)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

38

—

LowBetter than 83% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity38

Early-stage or recently created project

 Bus Factor1

Top contributor holds 97.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7653cedfb706bdbaceab17cb57fa55d5e2744faeb722cfc1e2af8c3fd88f13ef?d=identicon)[danielebarbaro](/maintainers/danielebarbaro)

---

Top Contributors

[![danielebarbaro](https://avatars.githubusercontent.com/u/4376886?v=4)](https://github.com/danielebarbaro "danielebarbaro (36 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (1 commits)")

---

Tags

domainsforgelaravelmulti-tenantssllaravelsslprovisioningmulti-tenantforgedomains

###  Code Quality

TestsPest

Static AnalysisPHPStan, Rector

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/plin-code-laravel-forge-domain/health.svg)

```
[![Health](https://phpackages.com/badges/plin-code-laravel-forge-domain/health.svg)](https://phpackages.com/packages/plin-code-laravel-forge-domain)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.1M337](/packages/psalm-plugin-laravel)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k53.0M589](/packages/laravel-scout)[spatie/laravel-health

Monitor the health of a Laravel application

87511.3M154](/packages/spatie-laravel-health)[laravel/ai

The official AI SDK for Laravel.

1.0k2.1M169](/packages/laravel-ai)[illuminate/broadcasting

The Illuminate Broadcasting package.

7126.9M203](/packages/illuminate-broadcasting)[illuminate/notifications

The Illuminate Notifications package.

483.0M1.1k](/packages/illuminate-notifications)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
