Packages

GDPR-compliant cookie consent management for Ash Framework applications. Provides Phoenix Components, LiveView integration, and database persistence for tracking user consent with full audit trail.

Current section

Files

Jump to
Raw

README.md

# AshCookieConsent

[![Hex.pm](https://img.shields.io/hexpm/v/ash_cookie_consent.svg)](https://hex.pm/packages/ash_cookie_consent)
[![Documentation](https://img.shields.io/badge/hex-docs-purple.svg)](https://hexdocs.pm/ash_cookie_consent)
[![License](https://img.shields.io/hexpm/l/ash_cookie_consent.svg)](https://github.com/shotleybuilder/ash_cookie_consent/blob/main/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](https://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

```elixir
# 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`:

```elixir
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`:

```javascript
import Alpine from 'alpinejs'
window.Alpine = Alpine
Alpine.start()
```

And install via npm:

```bash
cd assets && npm install alpinejs --save
```

### 3. Configure Tailwind CSS

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

```javascript
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

```elixir
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

```bash
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:

```elixir
# 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:

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

```heex
<!-- 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:

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

```heex
<!-- 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

```heex
<.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

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

```heex
<.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:

- **[Getting Started](https://hexdocs.pm/ash_cookie_consent/getting-started.html)** - Quick start guide
- **[Migration Guide](https://hexdocs.pm/ash_cookie_consent/migration-guide.html)** - Integrate into existing apps
- **[Examples](https://hexdocs.pm/ash_cookie_consent/examples.html)** - Usage patterns and code examples
- **[Troubleshooting](https://hexdocs.pm/ash_cookie_consent/troubleshooting.html)** - Common issues and solutions
- **[Extending](https://hexdocs.pm/ash_cookie_consent/extending.html)** - Advanced customization

Full API documentation is available at [HexDocs](https://hexdocs.pm/ash_cookie_consent).

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

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

- ✅ **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](https://hexdocs.pm/ash_cookie_consent/extending.html#adding-user-relationships) for implementation details.

## Contributing

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

### Development Setup

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

### Running Tests

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

- **Documentation**: [hexdocs.pm/ash_cookie_consent](https://hexdocs.pm/ash_cookie_consent)
- **Issues**: [github.com/shotleybuilder/ash_cookie_consent/issues](https://github.com/shotleybuilder/ash_cookie_consent/issues)
- **Discussions**: [Ash Framework Discord](https://discord.gg/ash-framework) (#libraries channel)

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Credits

Inspired by [phx_cookie_consent](https://github.com/pzingg/phx_cookie_consent) by pzingg.

Built with [Ash Framework](https://ash-hq.org/) by Zach Daniel and the Ash community.

## Repository

https://github.com/shotleybuilder/ash_cookie_consent