torann/registry
Torann/Registry is a simple Laravel package for storing persistent key-value settings in the database with cache support. Use a clean registry API to get, set, and manage site-wide configuration values without extra boilerplate.
Architecture fit
The package is a thin key/value store bolted onto standard Laravel scaffolding: one service provider, one facade, a get/set/has/forget contract, a config file, and a migration for a single registry table. That is a very small architectural surface area — it does not introduce a new runtime, a queue dependency, an event bus, or an external service. For a TPM, this is the favorable case: the integration is additive and reversible, and the blast radius of adopting or abandoning it is bounded to the call sites that use it.
The important architectural judgment is not whether the package's internals are good. It is that the package occupies the "runtime-editable configuration" slot in the system, and that slot has well-understood downstream consumers: admin UI, release process, support rotation, and eventually feature-flagging and multi-tenancy. The package itself does not solve those; it only makes the storage trivially available. Treat it as a primitive, not a platform.
The dot-notation key scheme with nested/array value support is sufficient for the realistic near-term inventory: banners, maintenance toggles, upload limits, onboarding copy arrays, default locale, pricing display rules. It is not sufficient for per-user targeting, percentage ramps, or audience segmentation — and it should not be asked to be. The moment those requirements appear, the correct move is to graduate to Laravel Pennant or an equivalent, not to extend this package.
The one genuine architectural concern is the in-memory cache layer combined with multi-node deployments. This is the only place where the package's design shapes your infrastructure decisions rather than the other way around. See "Technical risk" below.
Integration feasibility
Feasibility is high and the ramp is short. The documented path is three commands (composer require, vendor:publish, migrate), the provider auto-discovers on Laravel 5.5+, and the facade and registry() helper resolve without touching config/app.php. Any Laravel developer on the team can complete the first read/write inside an hour. The realistic engineering cost is not the install — it is the wrapper and the discipline around it, which is roughly one to two days including the typed accessor, key registry, seeder, and audit hook.
The single highest-leverage sequencing decision is to build the internal Settings wrapper before the second call site exists, bind it to an interface, and forbid the facade from being imported anywhere else. This is not ceremony. It converts a third-party dependency into a one-file replacement seam, gives you a place to enforce types and tenant prefixes, and makes the eventual migration to spatie/laravel-settings or Pennant a refactor rather than a hunt. A TPM should treat "wrapper landed and adopted as the only access path" as the actual acceptance criterion for the integration, not "package installed."
Technical risk
set, one get, one cache clear, and a scan of logs for PHP 8.2+ deprecation noise. Do not let this be inferred from "it's a Laravel package.""25" written where an integer was expected fails comparisons quietly. Mitigation is entirely on your side: typed accessors in the wrapper plus validation against a key registry at the write path.mysqldump, and visible in any admin UI built over them. This belongs in the engineering standard alongside the wrapper rule, because the failure is a security incident, not a bug.memory is per-request and resets (tests only), json writes to the local filesystem and silently diverges across containers and breaks on read-only filesystems. Neither belongs in production. database plus a cache is the only sensible default.Key questions to resolve before committing
spatie/laravel-settings? Write the numeric or event-based threshold (e.g., flag count, first targeting requirement, first audit requirement from a customer) before adoption, not after.Stack fit
The fit is good and deliberately shallow. It is a conventional Laravel package: service provider, facade, publishable config, publishable migration. It integrates with the same cache and database layers the application already operates and does not require a new vendor, a new runtime, or new infrastructure. For a team already on Eloquent plus a cache store, the mental model is one sentence: "a key/value table with a cache in front."
The one stack-level constraint to state plainly is that this is not a configuration replacement. Laravel's config/*.php files remain the source of truth for anything that is structural, environment-dependent, or needed at boot. The registry is for values that must change without a deploy — the overlap set is small and should be defined in the key registry, not discovered by accident.
Migration path
The migration is additive and low-drama, but two details deserve attention before the first production migrate:
varchar with a unique index is a wall you hit when keys grow a tenant prefix. Changing this before the first production migrate is a two-minute edit; after, it is a schema migration with downtime risk.type column, no tenant column, no updated_by. You can either add them at adopt time or accept that per-tenant scoping is done by key prefixing. Prefixing is the documented approach and is fine, but committing to it now avoids a data migration later.Compatibility
Verify the matrix yourself on the target Laravel and PHP versions — pin the version in composer.json, run publish/migrate/set/get/cache-clear, and check logs for deprecations. Treat a clean spike as a hard gate on the adoption decision. If the spike fails, the fallback (fork into the repo) is cheap precisely because the surface is small; budget for it as a plausible outcome rather than a disaster.
Testing compatibility is part of this: swapping REGISTRY_DRIVER=memory in phpunit.xml makes the registry an in-process array with no database, no teardown, and no ordering surprises. Tests should assert against the Settings interface, never the facade, so they survive a backend swap or an outright replacement.
Sequencing
App\Support\Settings with typed accessors, bound to an interface, plus config/settings.php enumerating every legal key with type and default. Add the audit hook now — an actor/key/old/new log line or table — because retrofitting it after writes are in the wild means losing the history you actually wanted.has()-guarded, so fresh installs, CI, and tests are deterministic and match production shape.config/settings.php before set() is reachable.The ordering is deliberately: gate compatibility, establish the seam, make it deterministic, prove it in production topology, and only then expand scope. Expanding call sites before the wrapper exists is the failure mode that turns a two-day integration into a permanent undocumented dependency.
Maintenance
Ownership effectively transfers to your team. With no stars and no dependents, there is no meaningful upstream maintenance signal, and the responsible planning assumption is that you maintain it — including patching it for future Laravel/PHP releases or forking it into the repo. The saving grace is the size: a provider, a facade, a driver, a migration, and a small config file. Maintenance cost is best modeled as "a small internal library we happened to source externally," not "a vendor relationship."
Two recurring maintenance chores should be scheduled, not hoped for:
SELECT key FROM registry against config/settings.php and delete unreferenced keys. Without this, deleted features leave keys behind forever and the table becomes archaeology.Support
Support load splits into two distinct buckets with different owners.
Operator support (the common case) is answering "who changed this and can we change it back." Without an audit trail, that question is a five-minute answer or an incident review, and the package provides nothing. Build the audit hook in the wrapper at integration time; it is an hour of work and it converts the most frequent support request from archaeology into a lookup. The admin surface also needs a visible "last modified by / when" for the same reason.
Engineering support is debugging "the value is wrong." The layered diagnostic is fast and should be documented for the team: check what is actually stored (Registry::all() or a direct table read) → if storage is right and reads are wrong, it is the cache → if storage is wrong, it is the write path or serialization. php artisan tinker against the facade beats adding logging. Also document the cache-clear gotcha: php artisan config:clear does not clear the registry's own cache; you must flush the registry cache key or forget() and re-set. This will otherwise burn an hour of someone's afternoon.
Scaling
Scaling behavior splits cleanly along read/write lines.
Reads scale well. In-memory caching means repeated access within a request does not hit the backend, so the registry is effectively free at read time even on hot pages — provided you follow the view-composer pattern rather than calling the facade inside a loop in a template.
Writes do not need to scale, and should not be asked to. This is a low-write store by design. If write volume is high, the design is wrong — you want a different system.
Topology is the real scaling constraint. Multi-node deployment plus invalidate-on-write caching produces stale reads on sibling nodes. The options, in order of preference: a shared cache store (Redis) all nodes read from; a short TTL with an explicitly accepted staleness window; or writes restricted to a single admin process. Whichever you pick, it is a decision that must be made consciously and verified in staging with two app containers, because a single-node dev environment will never surface it. This is the one operational risk that scales with the business rather than staying fixed.
Key-space scaling has a ceiling worth naming: the key column length and uniqueness constraint. Tenant prefixing consumes characters fast. Decide the key naming convention (domain.subject.setting, flat) and enforce it through the key registry rather than trusting convention, and validate key length at the write path so you fail loudly instead of truncating silently.
Failure modes
Ordered by expected frequency and severity:
set('limits.uploads.mb', '25') writes a string; a comparison elsewhere fails with no error. Mitigation: typed accessors in the wrapper, write validation against the key registry, and an explicit round-trip test of every value type whenever serialization or the column type changes.null vs. absent ambiguity. get($key, $default) returns the default both when the row is missing and when the stored value is null. If storing null is a legitimate state, use a sentinel or check has() first. Document this — it surprises people.get() falls through to whatever default the call site happened to pass, so staging, CI, and production silently diverge. Mitigation: the defaults seeder, run in every environment..env or a secrets manager, reinforced at code review.memory in production means settings vanish per request; json in a containerized deployment means silent divergence and failures on read-only filesystems. Mitigation: pin the driver in config and prohibit the other two outside tests.Ramp-up
Ramp-up is short and does not require specialist knowledge, which is a real advantage for a small team. A Laravel-fluent developer is productive in under an hour and fully conversant with the wrapper, key registry, and diagnostics in a day. The cost is not learning the API — it is absorbing the operational rules, and those need to be written down rather than transmitted orally.
The onboarding artifact should be a short internal standard covering, at minimum: the wrapper is the only access path; never read during boot; typed accessors only, never raw get() at call sites; secrets never go here; flags are staging-only and expire; default to database plus shared cache; check storage → cache → write path in that order when debugging; and the cache-clear gotcha. Six bullets and a paragraph. Without it, the rules live in one engineer's head and decay the moment they change teams.
For the TPM specifically, the ramp-up framing to stakeholders is: this is a small, contained capability that removes a class of change requests from the release pipeline — support copy, banners, limits, onboarding text, early-stage flags, and pre-billing tier differentiation. The measurable outcomes to track are release-cadence relief (fewer config-only deploys), reduction in "can you change this string" interrupts, and time-to-change on runtime settings. The strategic value is as a probe: real usage generates the actual requirements — audit, typing, targeting, secret handling, percentage ramps — that tell you precisely when to graduate to spatie/laravel-settings, Pennant, or a flag platform. Adopt it deliberately, with the graduation threshold written before adoption, and the decision to eventually leave it becomes evidence-based rather than a matter of taste.
How can I help you explore Laravel packages today?