Simple personal certificate management and automated renewal reminders. https://certified.env0.nl
  • Blade 54.7%
  • PHP 44.2%
  • CSS 0.7%
  • JavaScript 0.4%
Find a file
Joël van de Wouw bfba58fc0e
Some checks failed
tests / ci (8.3) (push) Has been cancelled
tests / ci (8.4) (push) Has been cancelled
New landing page
2026-10-26 13:42:09 +01:00
.forgejo/workflows Optimalisations of the platform 2026-10-26 13:09:18 +01:00
app New landing page 2026-10-26 13:42:09 +01:00
bootstrap Optimalisations of the platform 2026-10-26 13:09:18 +01:00
config Optimalisations & Mistral improvements 2026-10-26 13:21:55 +01:00
database Optimalisations & Mistral improvements 2026-10-26 13:21:55 +01:00
lang New landing page 2026-10-26 13:42:09 +01:00
public Initial commit 2026-07-12 14:44:58 +02:00
resources New landing page 2026-10-26 13:42:09 +01:00
routes Optimalisations & Mistral improvements 2026-10-26 13:21:55 +01:00
storage Initial commit 2026-07-12 14:44:58 +02:00
tests Optimalisations & Mistral improvements 2026-10-26 13:21:55 +01:00
.DS_Store Optimalisations of the platform 2026-10-26 13:09:18 +01:00
.editorconfig Initial commit 2026-07-12 14:44:58 +02:00
.env.example Organisations 2026-10-26 13:24:19 +01:00
.gitattributes Initial commit 2026-07-12 14:44:58 +02:00
.gitignore Stop tracking .claude and ignore it 2026-07-12 15:50:57 +02:00
.npmrc Initial commit 2026-07-12 14:44:58 +02:00
artisan Initial commit 2026-07-12 14:44:58 +02:00
boost.json Initial commit 2026-07-12 14:44:58 +02:00
composer.json Optimalisations & Mistral improvements 2026-10-26 13:21:55 +01:00
composer.lock Optimalisations & Mistral improvements 2026-10-26 13:21:55 +01:00
package-lock.json Optimalisations of the platform 2026-10-26 13:09:18 +01:00
package.json Optimalisations of the platform 2026-10-26 13:09:18 +01:00
phpstan.neon Initial commit 2026-07-12 14:44:58 +02:00
phpunit.xml Legal, marketing and UI updates 2026-08-04 10:18:10 +02:00
pint.json Initial commit 2026-07-12 14:44:58 +02:00
README.md Organisations 2026-10-26 13:24:19 +01:00
vite.config.js New landing page 2026-10-26 13:42:09 +01:00

Certified

Certified is a self-hosted certificate manager built on Laravel 13, Livewire 4, and Flux. It helps you keep track of certificates (diplomas, licenses, compliance documents, etc.), organize them into categories, and get notified before they expire.

Features

  • Certificate management — upload, edit, search and download certificates, organized by category; certificates without an expiry date (diplomas) are supported
  • Encryption at rest — documents are encrypted with the application key before they are written to disk
  • Renewals with history — uploading a renewed certificate keeps the previous document and dates as an earlier version
  • OCR extraction — automatically read title, issuer, number, holder, issue and expiry date on upload; admins configure the available models (Mistral, Klippa) with the platform's API keys, users pick one or turn scanning off
  • Organizations — employers invite their people, list the certificates they require, and see per member what is valid, expiring, expired or missing; members share their own certificates per requirement, can stop sharing at any time and see who opened them
  • Expiry notifications — email reminders at the user's own lead time, a week before, and on the day itself; missed days are caught up
  • Calendar feed — a private iCal link with every expiry date, for Google Calendar, Outlook or Apple Calendar
  • Secure sharing — share certificates with an external recipient through an unguessable, expiring and revocable link (optionally password protected), with an access log
  • Public verification — opt-in per certificate, protected by a self-hosted ALTCHA proof-of-work instead of a third-party CAPTCHA
  • Multi-language support

Stack

  • Laravel 13 (Octane-ready)
  • Livewire 4 + Flux UI
  • Tailwind CSS 4 / Vite
  • Laravel Fortify for authentication

Getting started

composer install
npm install

cp .env.example .env
php artisan key:generate

php artisan migrate

npm run dev
# in another terminal
php artisan serve

Running in production

Besides the web server, two processes have to run:

# Mail and notifications are queued — without a worker, nothing is sent.
php artisan queue:work --tries=3

# Expiry reminders (08:00) and the billing sync (07:30).
php artisan schedule:work   # or a cron entry running `schedule:run` every minute

Documents are encrypted with APP_KEY. Never change it without moving the old key to APP_PREVIOUS_KEYS, or the stored documents can no longer be read.

Upgrading an existing installation

After php artisan migrate, encrypt documents that were uploaded before encryption at rest existed (safe to run more than once):

php artisan certificates:encrypt-files

The migration also makes public verification an explicit per-certificate choice; certificates that already had both a number and a holder last name stay verifiable. SSO providers that don't send an email_verified claim (such as Microsoft Entra ID) have to be marked as trusted under Admin → SSO providers, otherwise sign-in through them is refused.

OCR no longer uses API keys per user: add at least one model under Admin → OCR models, after which users choose it again in their certificate settings.

Testing

composer test

CI runs the same command on Forgejo Actions (.forgejo/workflows/tests.yml).

Admin access

Promote an existing user to admin by email:

php artisan user:admin user@example.com

OCR models

Admins add OCR models under Admin → OCR models: a provider (Mistral or Klippa), an optional model name (Mistral) or template (Klippa), and the platform's API key, which is stored encrypted. Users on a paid plan pick one of the enabled models under Settings → Certificates, or keep scanning off. Both providers read images and PDFs; Mistral uploads a PDF to the platform's Mistral workspace, reads it through a signed link and deletes the copy straight after.

File storage

Documents and other uploads go to the disk named by FILESYSTEM_UPLOADS_DISK: local, or bunny for a private Bunny Storage zone in the EU. Documents are encrypted by the app before they are stored, so Bunny only ever holds encrypted bytes, and they are always served through the app (never from a CDN URL). See .env.example for the settings and the two commands to check the zone and copy existing files over. Built assets can go through a Bunny pull zone by setting ASSET_URL.