netipar/laravel-consent
A Laravel package for managing user consent and privacy preferences. Track opt-ins and opt-outs, store consent records, and integrate GDPR-friendly consent flows into your application with simple, configurable APIs.
This is a full-stack consent package: a typed Laravel config, a server-side Consent facade, an unstyled Blade banner, and three JS adapters (vanilla, Alpine, Vue 3) that all read the same clientConfig(). The upgrade path from netipar/cookie-consent 1.x is documented, so treat v0.1.0 as a rename/rewrite rather than a patch — plan a small migration rather than a composer update.
Install and publish in this order:
composer require netipar/laravel-consent
php artisan vendor:publish --tag=consent-config # config/consent.php
php artisan vendor:publish --tag=consent-lang # de, en, es, fr, hu
php artisan vendor:publish --tag=consent-migrations # only if you enable logging
php artisan migrate
Look here first: config/consent.php. It is validated by a ConsentConfig object, so a typo throws at boot instead of silently disabling gating — that is the fastest feedback loop you have. Confirm version, categories, cookie.domain, policy_url, and log.enabled before touching any Blade.
First use case — gate a third-party script and render a banner in one pass:
{{-- layouts/app.blade.php --}}
@consentConfig
<x-consent::banner policy-url="{{ Consent::policyUrl() }}" />
<script type="text/plain" data-consent="analytics" src="https://analytics.example/script.js"></script>
// anywhere server-side
if (Consent::allows('analytics')) {
$this->dispatchAnalytics();
}
The type="text/plain" + data-consent pattern is the core primitive — the JS core (activateGatedScripts) rewrites the script tag once the category is granted. Everything else (banner, store, Google mode) builds on that.
Pick one adapter, share one config. Consent::clientConfig() is the single source of truth: categories, texts, cookie attributes, Google settings. Blade, Alpine and Vue all consume it. Do not re-declare categories in JS — read them from the store/plugin.
// Alpine, in app.js
import Alpine from 'alpinejs'
import { registerConsent } from '@netipar/consent-alpine'
registerConsent(Alpine)
Alpine.start()
{{-- inside the banner's default slot --}}
<button @click="acceptAll">Accept all</button>
<button @click="reject">Reject</button>
Two-way gating. Client-side gating is for loading scripts; server-side Consent::allows() is for deciding what to render or record. Use both, but never use the cookie value in JS as an authorization boundary — it is user-editable.
@consent('marketing')
<x-marketing-pixel />
@endconsent
@unlessconsent('marketing')
<p>Marketing cookies are currently off.</p>
@endunlessconsent
Banner slot structure. The component ships unstyled with a default slot and a settings slot (per-category toggles), plus title, description, policy-url, policy-label. Keep the accessibility contract intact: don't remove the settings slot and don't override the focus handling with your own modal trap.
Preference centre as a second entry point. Users who already decided need a way back in. Expose a persistent "Cookie settings" link that reopens the banner — the store/consentBanner component is designed for this, so wire it to a footer link rather than building a separate settings page.
Google Consent Mode v2. Configure google.tags, google.signals, ads_data_redaction, url_passthrough in config, then let the package handle ordering:
'google' => [
'tags' => ['G-XXXXXXX'],
'signals' => ['analytics_storage', 'ad_storage', 'ad_user_data', 'ad_personalization'],
'ads_data_redaction' => true,
'url_passthrough' => true,
],
Tags load only after consent; later grants send consent update and a late config. Do not also drop your own gtag('consent', 'default', ...) snippet in the head — you will race the package's defaults.
Logging and audit trail. Enable log.enabled / CONSENT_LOG_ENABLED only if you need proof of consent. The endpoint is POST /consent/log behind web middleware, so the browser needs a CSRF token; throttled, and idempotent via event_id. Then:
Consent::consentId(); // stable visitor-scoped id
Consent::history(); // prior decisions for this consent id
Schedule pruning, because retention_days is enforced by the model, not by a cron baked into the package:
// routes/console.php
Schedule::command('model:prune')->daily();
Cookie table for the policy page. Consent::cookieTable() flattens the rich cookies entries (provider, translatable purpose, ISO 8601 lifetime). Render it with <x-consent::cookie-table /> or Vue's ConsentCookieTable — no need to maintain a second, drifting list of cookie names in your privacy copy.
Locale-aware policy links. policy_url accepts a string, a [locale => url] map, or a route name. With a map, Consent::policyUrl() resolves per request, which is what you want on a mcamara/laravel-localization site. With a route name, your route must exist in every locale, otherwise resolution is the thing that breaks first in staging.
Livewire / SPA navigation. The Alpine adapter is Livewire-navigation aware; the Vue plugin handles its own router. If you mix both, mount only one. Re-registering consent state on every wire:navigate is handled for you — don't add your own livewire:navigated listener that re-runs activateGatedScripts, or scripts will double-execute.
Server-rendered pages and caching. clientConfig()['texts'] is resolved per request. If you cache rendered HTML containing the banner or @consentConfig, vary the cache on the consent cookie and Accept-Language, or you will serve Hungarian buttons to English visitors and a stale grant state to everyone.
Upgrade from cookie-consent 1.x. Follow the upgrade guide rather than the README: the cookie contract changed to __cookie_consent + __cookie_consent_v and the version check is now enforced. Expect all existing visitors to be re-prompted once after deploy.
version invalidates everything. A decision is only valid for the current config version. Bump it when categories or vendors change (that is the point), but know that every bump re-prompts every visitor and, via the stale-version path, runs the cookie deletion sweep. Don't bump it in a hotfix without saying so.Cookie header. That means request()->cookie() and CookieJar will not see __cookie_consent, and any middleware that encrypts or rewrites cookies can break parsing. If you have custom cookie middleware, test consent detection with it enabled.cookie.domain doesn't match how your other cookies were actually set (e.g. .example.com vs example.com), the deletion silently no-ops and revoked analytics cookies survive. Verify in devtools, not in tests — the browser is the only honest witness._ga_* prefix matching is a feature, not a wildcard you control. Per-category cookies lists support exact names and prefixes; a mis-typed prefix deletes nothing and reports nothing. Log the list once at boot while you're wiring it.event_id idempotency protects you from duplicate rows on retry — send the same id, don't generate a fresh one.APP_KEY invalidates correlation with older log rows. If you need long-lived audit data, record that constraint somewhere durable.reload_on_revoke is a blunt instrument. It guarantees clean teardown of already-running scripts, but it will also discard in-progress form state. Enable it only where third-party scripts cannot be unloaded otherwise.@consentConfig emits JSON into the page. It is convenient and easy to double-emit — once in the layout, once in the component's own partial. Also confirm the output is CSP-compatible in your setup (nonce support exists for gated scripts; check the config directive separately).lang/{de,en,es,fr,hu} and are resolved per request. Adding a locale means publishing consent-lang and overriding, not editing vendor files. Missing keys surface as the fallback locale, not as an error — check Consent::texts() for the locale you think you're serving.ConsentConfig for config shape, the clientConfig() payload for adapter-level customisation, consent:change / consent:log-failed events for analytics-of-the-banner and for alerting when logging fails, and ConsentLog as a normal prunable Eloquent model you can query or export. The bundled Laravel Boost skill consent-development and the examples/ cookbook are worth reading before you design a custom flow.Consent::clientConfig() contain the category, (2) is the cookie version equal to config('consent.version'), (3) did the script tag keep type="text/plain", (4) did your CSP nonce get applied to the activated tag. Those four cover almost every report.How can I help you explore Laravel packages today?