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
| Scope | Matches |
|---|---|
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 → intChange 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
| Column | Type | Notes |
|---|---|---|
name | string | Derived certificate name; indexed. |
domain | string | Primary domain, stored lowercase; indexed. |
domains | json, nullable | Full SAN list when more than one domain was requested. |
driver | string | Driver that issued it (default kubernetes); indexed. |
status | CertificateStatus | Enum cast (default pending); indexed. |
issuer | ?string | Issuer the provider reports on issue, renewal or certificates:sync. |
serial / fingerprint | ?string | Serial and fingerprint the provider reports; an unchanged fingerprint fails a renewal. |
certifiable_type / certifiable_id | nullable morph | The owner; key type from key_type. |
issued_at | ?CarbonImmutable | Set when issued. |
expires_at | ?CarbonImmutable | Expiry; indexed. |
last_renewed_at | ?CarbonImmutable | Set on renewal. |
last_error | ?string | Failure reason. |
meta | ?array | JSON metadata from meta(). |
created_at / updated_at / deleted_at | timestamps | Soft 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 cryptoBy 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.