Skip to main content

Configuration reference

Everything lives in config/legal-consent.php. The keys you are most likely to touch:

KeyDefaultWhat it does
documentsexample terms / privacy / newsletter / imprint registry (ships pre-populated — edit it, don't author it from scratch; imprint is the informational one)The registry: each key maps to its legal_basis (contract / acknowledgement / consent / informational) and source. The quick start works precisely because terms is already registered here. Every registered document except an informational one appears on the registration form — a document that asks something has to be asked before an account exists. Per document, 'ask_at_registration' => false takes it off the form (rules, checklist and recorder together) without changing anything else: a mandatory document still gates, so the subject meets it at the re-consent screen instead. For a page that binds nobody — an Impressum, a cookie policy — use informational rather than this flag; that one leaves the gate and the notice sweeps as well.
documents.<key>.locale_fallbackfalseFor an acknowledgement only. A reader of a language nobody has translated yet is shown the version the gate already holds them to — fallback_locale, then default_locale — with a line in the dialog naming its language, and a release covers the languages whose text is ready, and always the source language. The ledger row names the locale that was shown. A contract and a consent bind, so they never fall back, and legal-consent:doctor fails on an entry that asks. An informational page always falls back, to default_locale.
default_locale · localesde · [de, en]The primary locale and the allowed set (publishing an unlisted locale is refused). An empty list means the default locale alone.
fallback_localedeWhere a document is not published in the locale a consent is being recorded in, the recorder resolves this one instead of failing, and the ledger row carries the fallback's locale. For a mandatory document the package walks a longer chain — requested locale, then this one, then default_locale — everywhere a subject meets it: the registration form shows it, EnsureLegalConsent holds a subject to it, and the banner, Consent::statusFor(), hasCurrent() and the settings screen report it. A voluntary consent is never resolved that way. See A language your texts are not translated into. It does not apply to the read path: Consent::published() has no locale fallback on purpose, because a page must show the text of the locale it claims to be showing or nothing at all. The one exception there is an informational page, which falls back to default_locale and to nothing else. See Link the full text with the item's own locale.
retention_after_end3 yearsHow long proof is kept before legal-consent:prune removes superseded or orphaned records, counted from the moment a record stopped being relevant: for a superseded one, the acceptance of the record that replaced it. Enforced only once schedule.prune is on. An English relative time (3 years) or an ISO 8601 duration (P3Y); a value that does not put the cutoff at least a day in the past is refused, the sweep deletes nothing, and legal-consent:doctor reports it.
retention_by_document[]A retention period of its own for a document whose acceptance has to stay provable for longer, keyed by document key and written like retention_after_end: 'partner_terms' => '10 years' keeps that document's superseded and orphaned records, and its delivery proofs, for ten years after they stopped being relevant. Every other document keeps the default. An empty value is no period of its own. Any other value that is not a period is refused like the default, the sweep deletes nothing, and legal-consent:doctor names the document.
cache.store · cache.ttlapp default · 86400Where and how long rendered documents are cached (self-invalidates on a content change). cache.store also decides where the activation lock lives, and that is the load-bearing half: publishing and releasing serialize on it so one document cannot end with two active versions. A store that cannot lock across processes — null, array, or anything without a LockProvider — leaves that invariant to the database alone. Since 0.24.0 the database enforces it on all three engines — a partial unique index on PostgreSQL and SQLite, a generated column with a unique index on MySQL — so a lock-less store degrades a lost race from a silent second active version into an orderly constraint violation. The package detects a store that cannot lock and logs a warning rather than refusing, so check your logs for cannot serialize if you run more than one web or queue process.
cache.prefixlegal:docThe key prefix every cached document and enforceable set is written under. Change it when two applications share one cache store and must not read each other's documents — the store itself is the boundary, and a shared Redis with one prefix has none. legal-consent:cache-flush reads the same value, so a changed prefix does not strand the old keys behind a flush that no longer names them.
cache.enforceable_ttl60How long (seconds) the gate caches the set of active versions. The time windows are read against the clock on every request, so a scheduled announce_from or enforce_from takes effect on time whatever this says. It bounds how long an is_active change made outside a publish, such as a manual UPDATE, stays unseen: a publish flushes the set immediately, and legal-consent:cache-flush does the same for a change of your own.
change_items.requiredfalseMake a per-locale change description a precondition of releasing a change that owes a notice. Off by default — see Saying what changed.
document_urlnullWhere a document is readable: fn (LegalDocument $document): ?string, or an invokable class-string (a closure blocks config:cache). Set it and every surface that shows a document links its title to the full text — the registration checkboxes, the re-consent gate, and "Your consents". Left null, titles render as plain text, which is what every release before 0.13 did. Worth setting: consent has to be informed (Art. 7(1)), the request intelligible (Art. 7(2)), and the terms retrievable before agreeing (§ 305 Abs. 2 BGB) — and the gate is the sharpest case, because there the subject cannot continue until they agree. Resolve the URL from the document's locale, not the page's: the resolver is handed the model for exactly that reason.
notice_mailshell on, everything else nullThe seams on the three change notices. view is the package's own Markdown shell (set it to null for Laravel's global template); theme takes a Laravel theme name or a ::-namespaced view — the package ships legal-consent::mail.theme, publishable under legal-consent-mail. identity.declarant / postal_address name the declaring person (§ 126b BGB) and are appended to the notice and therefore to the proof row; leave them null and nothing is added. from, reply_to, subject_prefix and subject_effective_date are transport and framing. notification.{mode} swaps a mode's notification for your own subclass of ChangeNotification, or of the shipped notice for that mode (ReconsentRequired, DeemedConsentNotice or LegalChangeInformational), which keeps everything you do not override; a value that is not one is ignored in favor of the shipped class rather than killing a queued sweep halfway through. A class that writes the recipient into a line keeps them in the proof after Consent::forget(), and the delivery proof logs a warning when it first sees one (see Retention). Every seam except the shell is inert by default, because the notice body is hashed into an append-only row: a seam that changed the mail unasked would move bytes nobody can correct afterwards. Multi-tenant? Bind ResolvesNoticeIdentity instead of using the static block — one global declarant names the wrong legal person in every tenant but one.
notifications.channels[mail, database]Channels for the change notifications.
notifications.max_recipients_per_runnullHolds back any version whose audience exceeds it — without stamping the watermark, so the notice is still owed on the next run. Release with dispatch-notices --force. null means no brake, deliberately: a limit that were on by default would withhold a legally required notice from an installation that never asked for one.
notice_periodsper regimeAdvance-notice days per regime and mode, and every key is read. The mode contributes active_reconsent_min_days or deemed_consent_min_days; the regime contributes psd2_min_days (floor 60), p2b_standstill_days (floor 15), eecc_min_days (floor 30) or privacy_advance_days (no floor — WP260's "well in advance" is guidance, not a number). The two are combined with max(), not chosen between: a deemed-consent change under P2B owes both its § 308 Nr. 5 benchmark and the Art. 3 standstill. An info-only change has no mode benchmark, so its regime is its only source — before 0.13 it had no advance check at all. A silent editorial change owes no notice, and declaring a regime on one is refused rather than quietly ignored. A scheduled gating change is measured announcement → effective date; a deemed-consent change is measured announcement → objection deadline (the subject must have the full period to object) and is never exempt. Too short a period is refused. A per-document min_lead_days in documents may raise a period, but never undercut a statutory floor: § 675g's two months is hard — neither an override nor this config can talk it down.
durable_mediumproof: true, channels: [mail]proof: whether an append-only legal_notices proof row is written for each notice the mail channel has sent, at the moment it sent it; a queued or refused notice has none. channels only names the medium the row records, email while it lists mail and durable_message otherwise: the notice goes out on notifications.channels.
schedule.dispatch_notices · schedule.close_objection_windowstrue · trueWhether the two notice sweeps are auto-registered on the scheduler (hourly).
schedule.prunefalseWhether the retention sweep is auto-registered (daily). Off by default because it deletes — see Retention.
middleware.allowlist_routes · middleware.allowlist_paths[] · []Extra route names, and extra URI paths (wildcards allowed, for example billing/*), the gate never blocks. The consent route, logout and Livewire's own endpoints are always allowed — you never need to list the latter. Livewire 4 derives its endpoint prefix from APP_KEY, so the gate resolves it from your installation at request time; writing your own /livewire-<hash>/* into this list is worse than listing nothing, because that hash is green in development and wrong in production.
middleware.rights_routes[]The routes where a subject exports or deletes their data, by route name: the page that holds the button as well as the action behind it, because a Livewire action on a gated page is unreachable even though its endpoint is not. The gate never blocks them. Access, portability and erasure (Art. 15, 20, 17 GDPR) do not depend on accepting a new text. legal-consent:doctor notes a gate with none named, and fails on a name that is no route.
gate.subject_filternullWhich authenticated subjects the gate blocks. Null gates all; a fn (Model): bool (or invokable class-string) returning false lets a subject through, so the gate can sit after your own verification or onboarding gates. A closure blocks config:cache — use a class-string in production. A misconfigured value fails safe (gated).
gate.first_usefalseAlso stop a subject who owes a first acceptance of a mandatory document, not only one who owes a re-consent. outstanding() filters on the notice mode of a version change, so a subject who never accepted anything is not in it — an application whose sign-up carries no consent step (OAuth-only, an imported user base) serves those people as though they had accepted. Read strictly: anything but a literal true leaves it off, because a gate that switched itself on for a truthy value would stop everybody and read as an outage. Turn it on together with a first-use screen — mount <livewire:legal-consent.reconsent-form :method="\Pushery\LegalConsent\Enums\ConsentMethod::FirstUseGate" /> on your consent route. Without one it is a dead end, not a loop: the consent route is allowlisted, so the subject lands there and is told nothing is due. legal-consent:doctor reports the combination.
routes.consent_name · routes.consent_pathlegal.consent · /legal-consentWhere to send a subject to act: consent_name is a route name (preferred — it survives a path change), consent_path the fallback URL. Used by the middleware redirect and by the link in every change notice, so a wrong value points the legally-required notice at a dead URL.
routes.return_to_intended · routes.homefalse · /Opt-in: after a re-consent gate is fully cleared, return the subject to the URL the middleware intercepted (stashed automatically), falling back to home. Off keeps the in-place confirmation; a settings-page embed never redirects.
routes.api · routes.api_prefix · routes.api_middlewarefalse · legal · [api, auth]Whether the headless JSON API is registered, under which prefix, behind which middleware.
routes.api_throttle60,1The rate limit the package puts in front of the four API write endpoints itself. Since Laravel 11 the api group carries a limiter only if the application called throttleApi(), and behind these routes sits an append-only ledger. Passed straight to the throttle middleware: '120,1', or the name of a limiter you registered; null takes it off.
routes.web · routes.web_prefix · routes.web_middlewarefalse · legal · [web, auth]Whether the one session-backed route — POST {web_prefix}/consent/withdraw — is registered. It is what the framework-agnostic settings stub's withdraw button posts to; with it off, the stub renders no withdraw form. Redirects back with a flashed legal-consent.status, which the JSON API's 204 cannot do.
routes.web_throttle60,1The same limit for the session-backed writes: the web withdraw route and the actions of the bundled Livewire components (grant, withdraw, object, terminate). They append to the same ledger, and a Livewire request is one POST with a CSRF token. One budget for the whole surface, keyed on the authenticated subject and kept apart from any throttle: your own routes apply. Same shape as api_throttle; null takes it off.
registration.without_form_fieldswarnWhat happens when a mandatory document is about to be recorded from a request carrying no legal_<key> field — an external-provider sign-in raising Registered with no form behind it. warn logs and records; refuse raises UnevidencedConsentException and records nothing at all. The default is lenient because the check can only look for the field name the package generates, and an application with its own form naming fields differently is correct and would otherwise start failing.
ui.variantautoWhich bundled view set is served: auto picks the WireKit-native views when pushery/wirekit ≥ 2.68.0 is installed, plain and wirekit pin the choice. A view published into resources/views/vendor/legal-consent still wins over both.
markdown.html_input · markdown.allow_unsafe_links · markdown.max_nesting_levelstrip · false · 20CommonMark hardening for rendering document sources. Loosen html_input only for sources you fully control — a legal text is rendered into your users' browsers.
admin.abilitynullThe Gate ability that unlocks the admin screens. Null means both screens return 404. See Managing legal texts.
admin.release_count_seconds5How long, in whole seconds, the database may take to count the people a release reaches, the number the admin screens name once a release is written. On PostgreSQL and MySQL a longer count is stopped and the release stands without the number. 0 skips the count, null waits for it. SQLite has no statement timeout and always counts. See The count under a release.
registration.listen_to_registered_eventtrueWhether the Registered event listener records registration consent. See Recording consent.
tamper_evidence · tamper_evidence_keyfalse · unsetThe append-only hash chain and its optional HMAC key. See Optional features.
placeholder_markers['LEGAL-PLACEHOLDER']Text that marks a legal text as a placeholder. legal-consent:verify-documents reports an active version carrying one in its text, title or acceptance sentence, and one on version 0.0.0. Case-sensitive; an empty list leaves only the 0.0.0 check.
age_gateenabled: false, threshold: 16The Art. 8 attestation on registration. See Optional features.
tenancyenabled: falsePer-tenant scoping of documents and consents. See Optional features.
models[]Your subclasses of the package's models, keyed by the package class: 'models' => [LegalConsent::class => \App\Models\LegalConsent::class]. The package then uses your class on every path — every query, every row it writes, and the relations between its models. Use it for your own relations, scopes or casts on a row. Your subclass inherits the table, the tenant scope and the mass-assignment guard, and it has to keep all three: these rows are the proof of what a subject was told and agreed to, and the package writes them only through its own curated paths. A class that does not exist or does not extend the package class is ignored in favor of the package class. In your own code, LegalConsent::model() returns the configured class and LegalConsent::resolve() a new instance of it.

Keys a host can set from the environment​

Publishing the config file freezes every key in it: new keys still arrive, because the provider merges recursively, but a corrected default of an existing key never reaches you again — including a security default this package tightens later. So the keys that let you make a property stricter or looser read from the environment, and publishing is not the price.

Environment variableKeyDefault
LEGAL_CONSENT_ALLOW_UNSAFE_LINKSmarkdown.allow_unsafe_linksfalse
LEGAL_CONSENT_MAX_NESTING_LEVELmarkdown.max_nesting_level20
LEGAL_CONSENT_CACHE_STOREcache.storeunset (the application's default store)
LEGAL_CONSENT_CACHE_TTLcache.ttl86400
LEGAL_CONSENT_ENFORCEABLE_TTLcache.enforceable_ttl60
LEGAL_CONSENT_MAX_RECIPIENTS_PER_RUNnotifications.max_recipients_per_rununset (no cap)
LEGAL_CONSENT_NOTICE_MAX_ATTEMPTSnotifications.max_attempts3
LEGAL_CONSENT_NOTICE_REQUEUE_AFTER_MINUTESnotifications.requeue_after_minutes1440
LEGAL_CONSENT_API_THROTTLEroutes.api_throttle60,1
LEGAL_CONSENT_WEB_THROTTLEroutes.web_throttle60,1
LEGAL_CONSENT_DOUBLE_OPT_IN_CONFIRM_WITHINdouble_opt_in.confirm_withinunset (no limit)
LEGAL_CONSENT_RETENTION_AFTER_ENDretention_after_end3 years
LEGAL_CONSENT_RELEASE_COUNT_SECONDSadmin.release_count_seconds5

Each numeric value is validated before it is trusted and falls back to the shipped default when it is not a whole number or lies below its floor — never to 0, which for a ceiling would usually mean no ceiling at all. LEGAL_CONSENT_MAX_NESTING_LEVEL and the two LEGAL_CONSENT_NOTICE_* values hold to a floor of 1. For the two cache lifetimes 0 is a setting of its own and caches nothing, and for LEGAL_CONSENT_MAX_RECIPIENTS_PER_RUN it holds back every run that has a recipient. LEGAL_CONSENT_ALLOW_UNSAFE_LINKS is read as a boolean, so false, 0 and off all switch it off; anything unparseable leaves it off.

The two durations, LEGAL_CONSENT_DOUBLE_OPT_IN_CONFIRM_WITHIN and LEGAL_CONSENT_RETENTION_AFTER_END, take an English relative time (7 days, 3 years) or an ISO 8601 duration (P7D, P3Y), and an empty value counts as unset. They are checked where they are read: a confirmation window that names no positive period makes Consent::confirm() refuse, a retention that does not put the cutoff at least a day in the past makes legal-consent:prune delete nothing, and legal-consent:doctor reports either.

A config file you published and edit yourself is read the same way. A whole-number setting takes a string that spells one, which is what env() returns, so 'threshold' => env('AGE_GATE_THRESHOLD', 16) works. Any other value counts as unset, the shipped default applies, and legal-consent:doctor names the setting.

The advance-notice minimums under notice_periods are deliberately NOT environment-settable. They are floors fixed by statute — § 675g Abs. 1 BGB / Art. 54 PSD2, Dir. (EU) 2018/1972 Art. 105(4), and the § 308 Nr. 5 benchmark — so the only movement an environment variable could buy is downward, out of a file nobody reviews. A longer period is set per document through min_lead_days, which may only ever lengthen.