AshCookieConsent

View Source

Hex.pm Documentation License

GDPR-compliant cookie consent management for Ash Framework applications.

Features

  • Ash-Native: Built as an Ash.Resource with full policy support
  • GDPR Compliant: Complete audit trail with consent timestamps and policy versions
  • Phoenix Integration: Works with traditional controllers and LiveView
  • Three-Tier Storage: Browser cookies + Phoenix session + database persistence
  • Cross-Device Support: Consent follows users across devices when logged in
  • Customizable UI: Phoenix Components with AlpineJS for interactivity
  • Lightweight: Minimal dependencies, no heavy JavaScript frameworks
  • Conditional Script Loading: Load analytics/marketing scripts only with consent
  • Comprehensive Testing: 172 passing tests covering all integration points

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 'alpinejs'
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: [
    './js/**/*.js',
    '../lib/*_web.ex',
    '../lib/*_web/**/*.*ex',
    '../deps/ash_cookie_consent/lib/**/*.ex'  // 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
<!-- 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

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>
# 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

  • No Database Query Per Request: Session cache eliminates DB roundtrips
  • Fast Initial Load: Assigns checked first (no I/O)
  • Works Offline: Cookie-based storage for anonymous users
  • Audit Trail: Database provides GDPR-compliant history

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:

  • ✅ Timestamp of consent (consented_at)
  • ✅ Policy version consented to (terms)
  • ✅ Specific categories consented (groups)
  • ✅ Expiration tracking (expires_at)
  • ✅ Full audit trail via Ash timestamps

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

Comparison with Alternatives

FeatureAshCookieConsentphx_cookie_consentGeneric 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)

  • Phase 1: Core Ash resource and domain (ConsentSettings)
  • Phase 2: Phoenix Components (ConsentModal, ConsentScript) and UI layer
  • Phase 3: Integration layer (Plug, LiveView hooks, Storage)
    • ✅ Cookie management module
    • ✅ Storage module (three-tier hierarchy)
    • ✅ Phoenix Plug for traditional controllers
    • ✅ LiveView Hook for LiveView apps
    • ✅ 163 comprehensive tests
    • ✅ Complete documentation (5 guides)
  • 🚧 Phase 4: Polish and Hex publishing (In Progress)
    • ✅ Migration guide
    • ✅ Usage rules for AI assistants
    • ⏳ README enhancements
    • ⏳ Code quality (Credo, Dialyzer)
    • ⏳ Hex.pm publishing
  • Phase 5: Production integration and iteration

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