---
title: "The certificate registry — Certificates for Laravel | Roundly"
description: "The Eloquent Certificate model — columns, query scopes, expiry helpers, state changes through the facade, a swappable model and the schema."
url: https://roundly-consulting.com/open-source/docs/certificates-for-laravel/registry
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Certificates for Laravel](https://roundly-consulting.com/open-source/docs/certificates-for-laravel.md)

# 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:

```php
use RoundlyConsulting\Certificates\Models\Certificate;

Certificate::query()->active()->get();
Certificate::query()->expiring(14)->get();   // expiring within 14 days or already past (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 — plus a stuck Renewing row — expiring within $days (default renewal.threshold\_days) or already past expiry: a failed renewal stays due, before and after expiry. |
| `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

```php
$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 — and certificates:sync never brings a revoked row back, pruned or not; only a fresh issue() does.

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 (sync leaves a pruned revoked row pruned). Two domains that fold into one name (a.b.com and a-b.com) never share the row: it stays with the domain that registered first. 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:

```php
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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/certificates-for-laravel.md)
