- Blade 54.7%
- PHP 44.2%
- CSS 0.7%
- JavaScript 0.4%
| .forgejo/workflows | ||
| app | ||
| bootstrap | ||
| config | ||
| database | ||
| lang | ||
| public | ||
| resources | ||
| routes | ||
| storage | ||
| tests | ||
| .DS_Store | ||
| .editorconfig | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| .npmrc | ||
| artisan | ||
| boost.json | ||
| composer.json | ||
| composer.lock | ||
| package-lock.json | ||
| package.json | ||
| phpstan.neon | ||
| phpunit.xml | ||
| pint.json | ||
| README.md | ||
| vite.config.js | ||
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.