NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

The provider contract is public. Implement CertificateProvider — plus ProvisionsMultipleDomains and ReportsCertificateStatus when your backend can handle SANs and live status:

use Illuminate\Support\Collection;
use RoundlyConsulting\Certificates\Contracts\CertificateProvider;
use RoundlyConsulting\Certificates\Contracts\ProvisionsMultipleDomains;
use RoundlyConsulting\Certificates\Contracts\ReportsCertificateStatus;
use RoundlyConsulting\Certificates\DataTransferObjects\CertificateStatusReport;

final class VaultCertificateProvider implements CertificateProvider, ProvisionsMultipleDomains, ReportsCertificateStatus
{
    public function get(): Collection { /* Collection<int, RemoteCertificate> */ }

    public function exists(string $name, string $domain): bool { /* ... */ }

    public function generate(string $name, string $domain): void { /* provision one domain */ }

    public function generateMany(string $name, array $domains): void { /* one SAN certificate */ }

    public function status(string $name, string $domain): CertificateStatusReport { /* live status + expiry */ }
}

Register it through the facade — for example from a service provider’s boot(). Certificates::extend() adds the driver to CertificateProviderManager, the driver manager behind Certificates::driver(); then select it as the default driver or per call:

use RoundlyConsulting\Certificates\Contracts\CertificateProvider;
use RoundlyConsulting\Certificates\Facades\Certificates;

Certificates::extend('vault', fn (): CertificateProvider => new VaultCertificateProvider());

// CERTIFICATES_DRIVER=vault, or per call:
Certificates::for('app.example.com')->using('vault')->issue();

get() returns RemoteCertificate value objects (name, domain). status() is the proof of issuance: a report that is failed, expired or revoked fails the issue or renewal, and on issue a still-pending one leaves the row requested. Report the expiry so the registry can populate expires_at — without it the registry falls back to validForDays — and the fingerprint, which tells a real renewal from an unchanged certificate.

Certificate stores

CertificateStore persists PEM material — leaf, key and chain — as a StoredCertificate. The package ships FilesystemCertificateStore; implement the contract inside your own provider to keep material elsewhere:

namespace RoundlyConsulting\Certificates\Contracts;

interface CertificateStore
{
    public function put(string $name, StoredCertificate $material): void;
    public function get(string $name): ?StoredCertificate;
    public function exists(string $name): bool;
    public function delete(string $name): void;
    public function names(): array; // list<string>
}

The container binds CertificateStore to a FilesystemCertificateStore on drivers.acme.store. The shipped acme and filesystem drivers build their own store from their config keys, so rebinding the contract does not redirect them.

Macros

CertificatesManager and CertificateBuilder are macroable — bolt on your own domain methods, for example from AppServiceProvider::boot(). A manager macro is callable on the facade:

use RoundlyConsulting\Certificates\CertificatesManager;

CertificatesManager::macro('issueForTeam', function (Team $team, string $domain) {
    /** @var CertificatesManager $this */
    return $this->for($domain)->owner($team)->issue();
});

Certificates::issueForTeam($team, 'app.example.com');

To override a use case instead, bind your own action in the container — the manager resolves every action on each call (see DI and actions).

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.