Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Credentials Laravel Package

beyondcode/laravel-credentials

Store and manage app credentials like API keys and secrets in the database with encryption. beyondcode/laravel-credentials adds a simple management UI and a Credential model so you can securely retrieve secrets anywhere in your Laravel app.

View on GitHub
Deep Wiki
Context7

Getting Started

Install, publish, migrate. The provider auto-discovers, so the day-one sequence is three commands:

composer require beyondcode/laravel-credentials
php artisan vendor:publish --provider="BeyondCode\Credentials\CredentialsServiceProvider"
php artisan migrate

You get config/credentials.php (model class, cache settings, UI route + middleware) and one migration creating a flat credentials table: one row per dotted key, value encrypted with Crypt under your APP_KEY.

Where to look first, in order:

  1. config/credentials.php — the model binding, cache TTL, and the middleware wrapping the management UI. Fix the middleware before anything else.
  2. routes/ or the config's route key — find where the built-in Blade CRUD screen mounts. Load it in a browser while logged out and confirm it 302s to login, not straight to the table.
  3. php artisan credentials:list — keys only, never values. This is your first debugging tool.
  4. The credentials table itself — confirm rows land as ciphertext (eyJ...), not plaintext pasted in via SQL.

First use case, keep it small. Move one plaintext .env secret — a Stripe test key or SMTP password — into the DB, hydrate it back into config() at boot, and leave every call site untouched:

$secret = credentials('services.stripe.secret');

The rest of the app keeps calling config('services.stripe.secret'). That's the whole value proposition on day one: you changed the source of a secret, not the fifty places that read it.

Implementation Patterns

Config hydration at boot — the pattern worth adopting. This is the only integration that lets you adopt the package incrementally, and the only one where rollback means deleting six lines:

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    if ($this->app->runningInConsole() && !$this->app->runningUnitTests()) {
        return; // never touch the DB during migrate/seed/package:discover
    }

    if (! Schema::hasTable('credentials')) {
        return; // fresh install / CI before migration
    }

    config([
        'services.stripe.secret' => credentials('services.stripe.secret'),
        'mail.password'          => credentials('mail.password'),
    ]);
}

Both guards are load-bearing. Drop the console check and php artisan migrate on a clean box dies on a missing table. Drop the hasTable check and your first deploy to a new environment dies the same way.

Namespace every key. The table is flat with no scoping, so the key string is your only isolation:

prod.mail.password
staging.mail.password
tenant.42.stripe.secret

This also makes credentials:list readable at fifty rows instead of a wall of password, key, token.

One write path for rotation. Don't let people run credentials:set ad hoc from prod shells. Wrap it:

// app/Console/Commands/RotateCredential.php
public function handle()
{
    $key = $this->argument('key');

    Credentials::set($key, $this->secret('New value'));
    Cache::flush(); // or forget the package cache key from config/credentials.php

    $this->info("Rotated {$key}");
}

Call this from your deploy pipeline. You want the cache clear to be inseparable from the write — the single most common production bug with this package is a write that a running PHP-FPM worker doesn't see.

Resolve inside handle(), never in a constructor. For queued jobs this is both a correctness and a security rule: the constructor's serialized payload would carry the plaintext secret into your queue backend, and it would carry a value that may have rotated before the job runs.

public function handle(): void
{
    $client = new StripeClient(credentials('services.stripe.secret'));
    // ...
}

Resolve once per request, not per item. Every uncached call is a query. In a collection pipeline or a loop, hoist it:

$apiKey = credentials('services.mailgun.key');

$users->each(fn ($u) => $mailer->send($u, $apiKey));

Testing — bypass the table. Two levels, pick by test type:

// Feature test against in-memory SQLite
protected function setUp(): void
{
    parent::setUp();
    Credentials::set('services.stripe.secret', 'sk_test_fake');
}

For unit tests, or anything you don't want touching the DB, bind a fake against the underlying contract in the container. Critically: use the exact same key strings as production. A test that passes against stripe.secret while prod reads services.stripe.secret proves nothing.

Custom model for audit. config/credentials.php's model key is the intended extension point — point it at a subclass and add updated_by, rotated_at, soft deletes. This is the cheapest way to answer "who changed this" and costs you maybe thirty lines. Do it before you populate the table; retrofitting audit columns onto an encrypted store is far more annoying than designing them in.

Per-tenant credentials — resolve per request, never globally. If tenants supply their own keys, hydrate after tenant context resolves, and keep them out of config() entirely. In Octane or any long-lived worker, a tenant secret written to global config leaks to the next request on that worker.

$key = credentials("tenant.{$tenant->id}.stripe.secret");

Gotchas and Tips

It's archived — last release 2020-10-19, Laravel 8 only. On Laravel 9+ you'll fight illuminate/support constraints. The good news: the package is genuinely thin (model, facade, two commands, a small Blade view), so a fork is a day of work, not a project. The bad news: you are now the maintainer of a dependency that holds secrets. Pin the fork, name an owner, put it on the owned-dependency list. The other option — and often the better one — is to treat the source as a spec and rebuild it on Laravel's native encrypted cast, which is maintained and does the same thing.

APP_KEY rotation permanently destroys every credential. There is no re-encryption command, no migration path, no recovery. Back up APP_KEY with the same rigor as the DB dump, and write the re-entry procedure down before anyone rotates it. This is the single sharpest edge on the package.

Encryption at rest ≠ access control. DB dump + APP_KEY = plaintext. Treat the credentials table as .env-class data: restrict DB access, and don't let a broad-access backup bucket become the weak link.

The management UI is real, and it's a CRUD screen for secrets. It mounts Blade routes behind whatever middleware is in config/credentials.php. Verify in your own app that the middleware actually requires auth — an unauthenticated credentials admin panel is a total compromise. auth alone is usually not enough; you want an authorization gate like can:manage-credentials.

Stale cache after a terminal write. If caching is on and you run credentials:set from a shell, PHP-FPM workers may serve the old value until the cache clears. Symptom: "works locally, doesn't work in prod," or a rotated key that keeps 401ing. Clear the cache in the same script that writes.

Missing keys return null silently, and keys are case-sensitive. A null secret turns into a missing Authorization header three layers down and a mystery 401 from a third party. Fail at the boundary:

$secret = credentials('services.stripe.secret')
    ?? throw new RuntimeException('Missing credential: services.stripe.secret');

Now it's a greppable stack trace in your logs instead of an hour of packet sniffing.

Never dd() a credential. credentials:list is safe (keys only). credentials:get, a dumped config array, or an uncaught exception with a secret in the payload are not. Assume deploy logs are readable by more people than you think. A pre-commit grep for credentials_get and a rule against dump(config()) in review covers most of it.

Debugging a failed lookup — in this order:

  1. php artisan credentials:list — is the key there, with exactly the right casing?
  2. Cache — clear it and retry. Rules out the most common cause.
  3. Is the stored value actually ciphertext (eyJ...)? If it's plaintext, someone wrote via raw SQL and the package won't read it.
  4. Is APP_KEY the one that encrypted the row? (Environment mismatch is the second most common cause.)

What you are explicitly giving up. No rotation, no versioning, no audit trail, no per-secret access control, no envelope encryption. Decide that before you populate the table, not after — and if the answer is "we need two of those," skip the package and build the same table shape on Laravel's native encrypted cast, where you own the maintenance and can add them incrementally.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib