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

Fulcrum Settings Laravel Package

gaiatools/fulcrum-settings

Fulcrum Settings is a Laravel package for storing and managing application settings in the database, with a simple API, cached reads, and support for runtime configuration changes.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements check first. fulcrum-settings requires PHP 8.3+ and Laravel 12.x / 13.x. v2.0.0 explicitly dropped Laravel 11 support, so if your app is still on 11 this is a non-starter without an upgrade. Read the tag's composer.json before anything else — the release notes don't restate the full matrix.

Install and scaffold.

composer require gaiatools/fulcrum-settings
php artisan vendor:publish --tag=fulcrum-config      # config/fulcrum.php
php artisan vendor:publish --tag=fulcrum-migrations  # settings + setting_values tables
php artisan migrate

If you plan to use Laravel Modules, note that module integration is opt-in via config (added in v2.0.0) — don't wire it up until the base setup works.

Where to look first.

  1. config/fulcrum.php — caching, activator type, tenant resolution. This file answers most "why is my value stale?" questions later.
  2. The Fulcrum facade — get(), resolve(), isActive(), set(), forUser(), forTenant(), reveal(). This is 90% of your day-to-day surface area.
  3. GaiaTools\FulcrumSettings\Database\Migrations\SettingMigration — setting definitions are migrations, not seeders. That's the mental model shift.

First use case: kill a hardcoded flag.

The fastest value you'll get is moving an env/config() flag into the database so it can change without a deploy. Generate a setting migration:

php artisan make:setting-migration create_new_dashboard_flag
return new class extends SettingMigration
{
    public function up(): void
    {
        $this->createSetting('feature.new_dashboard')
            ->description('Enable the new dashboard UI')
            ->boolean()
            ->default(false)
            ->save();
    }

    public function down(): void
    {
        $this->deleteSetting('feature.new_dashboard');
    }
};

Then in a controller, middleware, or Blade view:

use GaiaTools\FulcrumSettings\Facades\Fulcrum;

if (Fulcrum::isActive('feature.new_dashboard')) {
    // ...
}

Ship that, then layer rules on top once you trust the plumbing.

Implementation Patterns

Resolution order is the contract you build against. Ascending priority (lower number = higher priority), first matching rule wins, then the setting default, then the $default argument, then null. Design your rule priorities with gaps (10, 20, 30) so you can insert rules later without renumbering.

Settings migrations are the source of truth for definitions; set() is for runtime overrides.

This is the single most important pattern distinction:

Concern Mechanism Lives in
Key exists, type, default, description SettingMigration Version control, deployed
Per-tenant / per-user overrides Fulcrum::forTenant(...)->set(...) Database, runtime
Rule-based targeting Rules on the setting Version control (usually)

Keep rule definitions in migrations when they encode product logic ("premium customers get 20%"). Use the facade for operational changes ("turn off the banner"). Mixing the two is how you get a settings table nobody can reason about.

Use upsert() for idempotent migrations. If your deploy pipeline can run a setting migration twice, or if you're backfilling a setting that a previous migration created, upsert() beats createSetting():

$this->upsert('emergency_banner', function ($setting) {
    $setting->string()->default('')->description('Site-wide emergency message');
});

modifySetting() + apply() for additive changes to existing keys — updateDefault(), updateDescription(). Prefer this over delete-and-recreate so you don't orphan rule rows.

Typed settings classes for anything read in more than two places. Rather than scattering string keys, define a class with #[SettingProperty] attributes (Spatie-settings-style). This gives you IDE completion, a single place to change a key name, and a natural home for defaults. It's the difference between a settings package and a settings system.

Targeting rules: build them in migrations, debug them in the DB.

$this->createSetting('discount_percent')
    ->integer()
    ->default(5)
    ->rule(fn ($rule) => $rule
        ->name('Premium Customers')
        ->priority(10)
        ->whereInSegment('segment', 'premium')
        ->then(20)
    )
    ->save();

The condition types (whereEquals, whereInSegment, geo, time, user-agent) and rollout strategies are worth reading in references/targeting-rules.md before you invent a condition the package already has.

Context is the integration seam with the rest of your app. The typical wiring: a middleware that sets tenant, and resolving per-request.

use GaiaTools\FulcrumSettings\Support\FulcrumContext;

// In middleware, after tenant resolution:
FulcrumContext::set('tenant_id', $tenant->id);

// Or per-call:
$value = Fulcrum::forUser($user)->get('theme');
$value = Fulcrum::forTenant('tenant-123')->get('max_projects');

FulcrumContext is ambient and mutable — treat it like Auth::user(): set it early, rely on it downstream, and never assume it's clean.

Rollouts for progressive delivery. Percentage-based delivery uses consistent hashing, so the same user gets bucketed stably across requests. This is the right tool for "roll out to 5% of users" — but read the fine print: this is not an experimentation platform. No stats engine, no holdout analysis.

Masked settings for secrets-adjacent config. ->masked() encrypts the value, and reads return masked unless you opt in:

$apiKey = Fulcrum::reveal()->get('stripe_api_key');

Use this for third-party API keys and webhook secrets that operations needs to rotate without a deploy. Don't use it for values that end up in logs — masking protects storage, not your Log::info($value) calls. reveal() should be rare and greppable; if you find it in twenty places, your config boundary is in the wrong spot.

Multi-tenancy: defaults plus overrides, not per-tenant rows for everything.

The pattern that scales: one global setting with a default, plus tenant-specific values created on demand. forTenant($id)->get('key') should fall through to the global default when no tenant row exists. Only materialize tenant rows for tenants that actually deviate — you do not want N tenants × M settings rows on day one.

Testing. Settings state is database state, so tests that assert on flags need explicit setup:

  • Reset FulcrumContext in setUp()/tearDown() — it leaks between tests.
  • Clear the settings cache between tests if you're asserting on writes-then-reads within a single test.
  • For unit tests, inject a fake or resolve through the container rather than hitting the facade against a real DB.

Gotchas and Tips

Cache invalidation is handled — but only for writes that commit. v2.0.0 added invalidation of cached settings after committed writes, and v2.0.0's fix list includes coalescing that invalidation while preserving successful writes. The implication: if you write settings inside a transaction that later rolls back, don't assume the cache reflects your intent. And if you're writing settings from multiple processes, prefer the facade over raw DB updates — bypassing the package's write path bypasses invalidation.

Cached results are keyed by evaluation context. v2.0.0 fixed both "isolate cached setting results by evaluation context" and "vary cached settings only by relevant request inputs." Practical consequence: two users with different attributes resolved should get different cached entries. If you're debugging a "wrong value for the wrong user" report, clear the settings cache first, then look at your FulcrumContext population — a stale or wrong context value is the more common cause than a cache bug.

Queue workers and Octane: context leaks are the classic failure. v2.0.0 shipped several fixes here — "isolate Fulcrum context between worker operations," "clean worker state on early Laravel 11," "preserve tenancy listener context during worker cleanup," "track lifecycle state in the resolving container." If you're on 2.0.x, this is fixed. If you're evaluating, this is a signal that long-lived workers were a real problem area. Regardless of version, explicitly reset context at job start if a job resolves settings for a specific tenant. Don't rely on the previous job having cleaned up.

Morph map aliases — audit existing rows, don't just upgrade. v2.0.1 fixes resolution of setting value morph-map aliases. The failure mode before the fix wasn't an exception; it was a silent fallback to the default, or a write landing under a type string that a later read couldn't match. Upgrading the library does not repair rows already written with mismatched types.

If any environment ran v2.0.0 with Relation::morphMap() registered:

SELECT DISTINCT settingable_type FROM setting_values ORDER BY 1;

Compare that list against your Relation::morphMap() aliases and your FQCNs. Any row storing an FQCN where the app registers an alias (or vice versa) needs a one-off backfill. Do this before you trust the upgrade.

Nullable morph types exist — the null vs 'null' ambiguity is real. v2.0.1's other fix is documentation for nullable setting value morph types, which implies global-scope settings with no owner are supported. When querying those directly, WHERE settingable_type IS NULL and WHERE settingable_type = 'null' are different rows. Always go through the facade; if you must query the table, be explicit about null handling.

immutable() needs a force flag. Settings marked immutable reject modification unless explicitly forced. This is a feature — it protects operational constants from an admin UI bug — but it will surprise you in a modifySetting() migration. Read the exception message, don't work around it.

set() writes; don't call it in a loop. Fulcrum::set('key', $value) is a persisted write plus (in 2.0+) cache invalidation. It belongs in an admin controller action or an artisan command, not in a request lifecycle hook or a collection loop. For bulk changes, batch them.

Settings migrations run inside your normal migration pipeline. That means deploy ordering matters: if you deploy code that reads feature.new_dashboard before the migration runs, you get the $default argument (or null). Always pass an explicit default at the call site — Fulcrum::get('api.rate_limit', default: 100) — so a missing setting degrades to a sane value rather than null propagating into arithmetic.

Debugging checklist, in order:

  1. php artisan config:clear and the settings cache clear — stale config is the cheapest thing to rule out.
  2. Dump FulcrumContext at the point of resolution. Wrong context = wrong rules matching = wrong value.
  3. List the setting's rules sorted by priority. First match wins; a priority-1 rule you forgot about beats everything.
  4. Check the setting's immutable/masked flags before assuming the value is what's stored.
  5. Only then write an SQL query against setting_values.

Extension points worth knowing about:

  • Custom types — if your product needs money, duration, or a domain enum, register a custom type rather than serializing to string and parsing at every call site. This is the cleanest extension point in the package.
  • Events and contracts — useful for audit logging. If a stakeholder asks "who changed the billing threshold," the package doesn't ship an audit trail out of the box; a listener on the write events is your wrapper.
  • Laravel Modules integration — opt-in, and it changes contextual access, middleware aliases, and storage/sync semantics. Read references/laravel-modules.md before enabling it; don't discover the storage change at deploy time.

Support surface is small. Single-vendor package, three GitHub stars. That doesn't make it bad — it makes it unguaranteed. Confirm the Apache-2.0 license permits your internal forking policy, and treat the codebase as something one of your developers may need to read and patch. Budget a recurring line item for reviewing patch releases: this package's own history shows correctness fixes landing in patch versions, which means "install and forget" is the wrong operating posture.

Know when to walk away. This is not an experimentation platform. If product asks for A/B tests with statistical significance, holdout cohorts, or sub-second flag propagation across regions, this is the wrong tool — reach for a dedicated flagging service and keep fulcrum-settings for configuration and coarse targeting. The package is at its best for multi-tenant defaults-and-overrides, admin-editable operational config, and per-user/team preferences — i.e., the settings subsystem you'd otherwise build yourself.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo