AshCookieConsent

Hex.pmDocumentationLicense

GDPR-compliant cookie consent management for Ash Framework applications.

Features

Live Demo

See it in action: ehs-enforcement.sertantai.com

This production application uses AshCookieConsent for GDPR-compliant cookie management with database persistence for authenticated users.

Why AshCookieConsent?

Built for Ash Framework: Unlike generic cookie consent libraries, AshCookieConsent leverages Ash's powerful resource system for consent management, making it a natural fit for Ash applications.

Flexible Storage: Three-tier storage system (assigns → session → cookie → database) provides optimal performance while maintaining GDPR compliance. Works great for anonymous users while supporting cross-device sync for authenticated users.

Developer-Friendly: Simple API with helper functions, Phoenix components, and comprehensive documentation. Get consent management working in minutes, not hours.

Production-Ready: Thoroughly tested with 163 passing tests, used in production Ash applications, and following Elixir/Phoenix best practices.

Quick Example

# 1. Add to router
plug AshCookieConsent.Plug, resource: MyApp.Consent.ConsentSettings

# 2. Add modal to layout
<.consent_modal current_consent={@consent} cookie_groups={AshCookieConsent.cookie_groups()} />

# 3. Check consent in your code
if AshCookieConsent.consent_given?(conn, "analytics") do
  # Load analytics scripts
end

# 4. Conditionally load scripts
<.consent_script consent={@consent} group="analytics" src="https://analytics.example.com/script.js" />

That's it! Your app now has GDPR-compliant cookie consent management.

Installation

1. Add Dependency

Add ash_cookie_consent to your list of dependencies in mix.exs:

def deps do
  [
    {:ash_cookie_consent, "~> 0.1"}
  ]
end

2. Install AlpineJS

The consent modal requires AlpineJS for interactivity. Add it to your assets/js/app.js:

import Alpine from &#39;alpinejs&#39;
window.Alpine = Alpine
Alpine.start()

And install via npm:

cd assets && npm install alpinejs --save

3. Configure Tailwind CSS

Add the library path to your assets/tailwind.config.js to include component styles:

module.exports = {
  content: [
    &#39;./js/**/*.js&#39;,
    &#39;../lib/*_web.ex&#39;,
    &#39;../lib/*_web/**/*.*ex&#39;,
    &#39;../deps/ash_cookie_consent/lib/**/*.ex&#39;  // Add this line
  ],
  // ...
}

Setup Guide

1. Define Your ConsentSettings Resource

defmodule MyApp.Consent.ConsentSettings do
  use Ash.Resource,
    domain: MyApp.Consent,
    data_layer: AshPostgres.DataLayer

  postgres do
    table "consent_settings"
    repo MyApp.Repo
  end

  attributes do
    uuid_primary_key :id

    attribute :terms, :string, allow_nil?: false
    attribute :groups, {:array, :string}, default: []
    attribute :consented_at, :utc_datetime
    attribute :expires_at, :utc_datetime

    timestamps()
  end

  actions do
    defaults [:read, :destroy]

    create :create do
      primary? true
      accept [:terms, :groups, :consented_at, :expires_at]

      change fn changeset, _context ->
        now = DateTime.utc_now() |> DateTime.truncate(:second)
        expires = DateTime.add(now, 365, :day) |> DateTime.truncate(:second)

        changeset
        |> Ash.Changeset.change_attribute(:consented_at, now)
        |> Ash.Changeset.change_attribute(:expires_at, expires)
      end
    end

    update :update do
      primary? true
      accept [:terms, :groups, :expires_at]
    end
  end
end

2. Generate Migration

mix ash_postgres.generate_migrations --name add_consent_settings
mix ecto.migrate

3. Add Integration Layer

For Traditional Phoenix Controllers (Plug)

Add the plug to your browser pipeline:

# lib/my_app_web/router.ex
pipeline :browser do
  plug :accepts, ["html"]
  plug :fetch_session
  plug :fetch_live_flash
  plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
  plug :protect_from_forgery
  plug :put_secure_browser_headers

  # Add the consent plug (MUST come after :fetch_session)
  plug AshCookieConsent.Plug, resource: MyApp.Consent.ConsentSettings
end

For LiveView Applications (Hook)

Add the hook to your LiveView modules:

# lib/my_app_web.ex
defmodule MyAppWeb do
  def live_view do
    quote do
      use Phoenix.LiveView,
        layout: {MyAppWeb.Layouts, :app}

      # Add the consent hook
      on_mount {AshCookieConsent.LiveView.Hook, :load_consent}

      unquote(html_helpers())
    end
  end

  defp html_helpers do
    quote do
      # Import consent components
      import AshCookieConsent.Components.ConsentModal
      import AshCookieConsent.Components.ConsentScript
    end
  end
end

4. Add Consent Modal to Layout

<!-- In your root.html.heex -->

<body>
  <%= @inner_content %>

<!-- Consent Modal -->

  <.consent_modal
    current_consent={assigns[:consent]}
    cookie_groups={assigns[:cookie_groups] || AshCookieConsent.cookie_groups()}
    privacy_url="/privacy"
  />

<!-- LiveView Cookie Update Handler -->

  <script>
    window.addEventListener("phx:update-consent-cookie", (e) => {
      const consent = e.detail.consent;
      const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
      document.cookie = `_consent=${encodeURIComponent(consent)}; expires=${expires}; path=/; SameSite=Lax`;
    });
  </script>
</body>

Usage

Checking Consent

Use the helper functions to check if consent has been given:

# In a controller or LiveView
if AshCookieConsent.consent_given?(conn, "analytics") do
  # Load analytics scripts
end

# Check if any consent exists
if AshCookieConsent.has_consent?(conn) do
  # User has made a consent choice
end

# Check if consent is needed
if AshCookieConsent.needs_consent?(conn) do
  # Show consent modal
end

Conditional Script Loading

The ConsentScript component conditionally loads scripts based on user consent:

External Scripts

<!-- Google Analytics -->

<.consent_script
  consent={@consent}
  group="analytics"
  src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
  async={true}
/>

<!-- Facebook Pixel -->

<.consent_script
  consent={@consent}
  group="marketing"
  src="https://connect.facebook.net/en_US/fbevents.js"
  defer={true}
/>

<!-- Plausible Analytics -->

<.consent_script
  consent={@consent}
  group="analytics"
  src="https://plausible.io/js/script.js"
  defer={true}
  data-domain="example.com"
/>

Inline Scripts

<.consent_script consent={@consent} group="analytics">
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'GA_MEASUREMENT_ID');
</.consent_script>

Customizing Cookie Categories

# In your config/config.exs
config :ash_cookie_consent,
  cookie_groups: [
    %{
      id: "essential",
      label: "Essential Cookies",
      description: "Required for the website to function",
      required: true
    },
    %{
      id: "analytics",
      label: "Analytics",
      description: "Help us understand how you use our site",
      required: false
    },
    %{
      id: "marketing",
      label: "Marketing",
      description: "Used to deliver personalized ads",
      required: false
    }
  ]

Customizing the Modal

<.consent_modal
  current_consent={@consent}
  cookie_groups={AshCookieConsent.cookie_groups()}
  title="Cookie Settings"
  description="We value your privacy. Choose which cookies you want to accept."
  accept_all_label="Accept All Cookies"
  reject_all_label="Only Essential"
  customize_label="Manage Preferences"
  privacy_url="/privacy-policy"
  modal_class="my-custom-modal"
  button_class="my-custom-button"
/>

How It Works

Three-Tier Storage System

The library implements a hierarchical storage system for optimal performance and reliability:

  1. Connection/Socket Assigns (Fastest - in-memory, request-scoped)
  2. Phoenix Session (Fast - server-side, encrypted)
  3. Browser Cookie (Medium - client-side, signed)
  4. Database (Ash) (Persistent - long-term storage)

When Consent is Loaded:

  1. Check assigns → if found, use it (fastest)
  2. Check session → if found, use it
  3. Check cookie → if found, use it
  4. Check database (if authenticated) → if found, use it
  5. If nothing found → show consent modal

When Consent is Updated:

  1. Save to cookie (for persistence)
  2. Save to session (for performance)
  3. Update assigns (for current request)
  4. Save to database (if authenticated user - extensible)

Performance Benefits

Documentation

Comprehensive guides are available:

Full API documentation is available at HexDocs.

GDPR Compliance

AshCookieConsent helps you comply with GDPR Article 7(1), which requires you to demonstrate that consent was given:

Important: GDPR compliance requires more than just technical implementation. Ensure your privacy policy and consent text meet legal requirements.

Comparison with Alternatives

Feature AshCookieConsent phx_cookie_consent Generic JS Library
Ash-Native ❌ (Ecto)
Phoenix Integration ⚠️ (Manual)
LiveView Support ⚠️ (Limited)
Three-Tier Storage
Conditional Scripts
Database Audit Trail
Maintained ❌ (Archived) Varies
Test Coverage ✅ (163 tests) ⚠️ Varies

Implementation Status

Current Version: 0.1.0 (Phase 4 - Polish & Publishing)

Note: Database synchronization for authenticated users requires adding a user relationship to ConsentSettings. See the Extending Guide for implementation details.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

Development Setup

git clone https://github.com/shotleybuilder/ash_cookie_consent.git
cd ash_cookie_consent
mix deps.get
mix test

Running Tests

# Run all tests
mix test

# Run with coverage
mix test --cover

# Run specific test file
mix test test/ash_cookie_consent/plug_test.exs

Support

License

This project is licensed under the MIT License - see the LICENSE file for details.

Credits

Inspired by phx_cookie_consent by pzingg.

Built with Ash Framework by Zach Daniel and the Ash community.

Repository

https://github.com/shotleybuilder/ash_cookie_consent