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

The certificate registry

Every certificate the app issues is recorded as a RoundlyConsulting\Certificates\Models\Certificate — an ordinary Eloquent model with soft deletes and a factory. That makes “which certificates expire in 14 days?” a normal query:

use RoundlyConsulting\Certificates\Models\Certificate;

Certificate::query()->active()->get();
Certificate::query()->expiring(14)->get();   // expiring within 14 days (default: config threshold)
Certificate::query()->expired()->get();
Certificate::query()->forDomain('app.example.com')->forDriver('kubernetes')->get();
Certificate::query()->coveringDomain('www.example.com')->first();

Query scopes

ScopeMatches
active()Issued or Renewed with expires_at null or in the future.
expired()Expired status, or expires_at in the past.
expiring(?int $days = null)Issued, Renewed or Failed, expiring between now and now + $days (default renewal.threshold_days) — a failed renewal stays due.
forDomain(string $domain)Primary domain equals $domain, case-insensitively.
forDriver(string $driver)Driver equals $driver.
coveringDomain(string $domain)Primary domain equals $domain, or the domains SAN list contains it (case-insensitive).

Helpers and lifecycle

$certificate->isActive();          // active status and not past expires_at
$certificate->isExpired();         // Expired status, or expires_at in the past
$certificate->expiresWithin(14);   // bool
$certificate->daysUntilExpiry();   // int|null — negative once expired
$certificate->certifiable;         // the owning model, if any

// State changes go through the facade — guarded, evented and seen by the fake.
Certificates::revoke($certificate, 'key compromise'); // Revoked + CertificateRevoked
Certificates::expire($certificate);                   // Expired + CertificateExpired
Certificates::prune(30);                              // soft-delete stale expired/failed/revoked rows → int

Change a certificate’s state through the facade: Certificates::renew(), revoke() and expire() guard the status transition, fire the event (CertificateRevoked and CertificateExpired can open alerts) and are recorded by Certificates::fake(). revoke() records the revocation in the registry only — it does not contact the CA or the cluster.

The model’s markIssued(), markRenewed(), markFailed(), markRevoked() and markExpired() are internal: the raw state setters the actions use, with no transition guard and invisible to the fake. Don’t call them from host code.

Columns

ColumnTypeNotes
namestringDerived certificate name; indexed.
domainstringPrimary domain, stored lowercase; indexed.
domainsjson, nullableFull SAN list when more than one domain was requested.
driverstringDriver that issued it (default kubernetes); indexed.
statusCertificateStatusEnum cast (default pending); indexed.
issuer?stringIssuer the provider reports on issue, renewal or certificates:sync.
serial / fingerprint?stringSerial and fingerprint the provider reports; an unchanged fingerprint fails a renewal.
certifiable_type / certifiable_idnullable morphThe owner; key type from key_type.
issued_at?CarbonImmutableSet when issued.
expires_at?CarbonImmutableExpiry; indexed.
last_renewed_at?CarbonImmutableSet on renewal.
last_error?stringFailure reason.
meta?arrayJSON metadata from meta().
created_at / updated_at / deleted_attimestampsSoft deletes.

(driver, name) is unique, pruned rows included — so re-issuing a pruned domain, or a sync that still finds it, revives that row as a fresh registration instead of inserting a duplicate. The certifiable morph uses key_type — set it to uuid or ulid before migrating when your owner models use UUID or ULID keys. table and connection come from config.

A custom model

Point certificates.model at a subclass to add your own scopes, relations or casts. The manager, the issue path, queued renewals, HasCertificates, the expiry check, the fake and the console commands all resolve the configured class. Unset or blank, it resolves the packaged Certificate; anything else must be that model or a subclass of it — a class that does not extend it, or does not exist, throws InvalidConfigurationException naming certificates.model instead of being silently replaced:

namespace App\Models;

use RoundlyConsulting\Certificates\Models\Certificate as BaseCertificate;

class Certificate extends BaseCertificate
{
    // your own scopes, relations and casts
}

// config/certificates.php
'model' => App\Models\Certificate::class,

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.