Elixir License: MIT

Legal compliance module for PhoenixKit. GDPR, CCPA, LGPD, and PIPEDA compliant legal page generation, cookie consent widget, and consent audit logging.

Features

  • 7 compliance frameworks — GDPR (EU/EEA), UK GDPR, CCPA/CPRA (California), US States (15+), LGPD (Brazil), PIPEDA (Canada), Generic
  • Legal page generation — Privacy Policy, Cookie Policy, Terms of Service, Do Not Sell, Data Retention, CCPA Notice, Acceptable Use
  • EEx template system — customizable templates with language support and template override from parent app
  • Cookie consent widget — glass-morphic UI with floating icon, preferences modal, and consent banner
  • Google Consent Mode v2 — built-in integration for analytics/marketing consent signals
  • Consent audit logging — full audit trail with user/session tracking, IP, and hashed user agent
  • Publishing integration — legal pages stored as posts via PhoenixKit Publishing for versioning and multi-language support
  • Admin settings UI — framework selection, company/DPO info, page generation, widget configuration
  • Auto-discovery — implements PhoenixKit.Module behaviour; PhoenixKit finds it at startup with zero config

Installation

Add phoenix_kit_legal to your dependencies in mix.exs:

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

Then fetch dependencies:

mix deps.get

Note: For development or if not yet published to Hex, you can use:

{:phoenix_kit_legal, github: "BeamLabEU/phoenix_kit_legal"}

Automated setup

Run the install task to patch your app automatically:

mix phoenix_kit_legal.install

This task is idempotent — safe to run multiple times. It performs three steps:

StepWhat it does
lib/**/endpoint.exAdds Plug.Static at /phoenix_kit_legal to serve the consent JS
assets/css/app.cssAdds @source "../../deps/phoenix_kit_legal" for Tailwind class scanning
assets/js/app.jsAdds import "../../deps/phoenix_kit_legal/priv/static/assets/phoenix_kit_consent.js"

Then it prints the remaining manual steps (migration, JS hook, router scope, component).

Manual steps after install

1. Copy and run the migration:

cp deps/phoenix_kit_legal/priv/migrations/add_phoenix_kit_consent_logs.exs \
   priv/repo/migrations/$(date +%Y%m%d%H%M%S)_add_phoenix_kit_consent_logs.exs
# Edit: rename MyApp.Repo to your repo module name
mix ecto.migrate

2. Wire up the JS hook in assets/js/app.js:

// Side-effect import — IIFE registers window.PhoenixKitHooks.CookieConsent
import "../../deps/phoenix_kit_legal/priv/static/assets/phoenix_kit_consent.js"

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { ...Hooks, ...window.PhoenixKitHooks },
  params: {_csrf_token: csrfToken}
})

3. Add the router scope in router.ex:

scope "/admin/settings", PhoenixKitWeb.Live.Modules.Legal do
  live "/legal", Settings, :index
end

4. Add the CookieConsent component to your root layout:

<PhoenixKit.Modules.Legal.CookieConsent.cookie_consent
  frameworks={["gdpr"]}
  phoenix_kit_current_scope={@phoenix_kit_current_scope}
/>

Pass phoenix_kit_current_scope={@phoenix_kit_current_scope} so the component can decide server-side whether to render for authenticated users. The assign is already available in root layouts wired via PhoenixKitWeb.Integration. Omitting it is safe — the widget renders for everyone (same as an anonymous visitor), but the "Hide for authenticated users" setting will have no effect.

PhoenixKit auto-discovers the module at startup — no additional configuration needed.

Quick Start

  1. Add the dependency to mix.exs
  2. Run mix deps.get
  3. Enable the module in admin settings (legal_enabled: true)
  4. Select compliance frameworks (e.g., GDPR, CCPA)
  5. Fill in company and DPO contact information
  6. Generate legal pages — they appear under /admin/settings/legal

Compliance Frameworks

FrameworkRegionConsent ModelRequired Pages
GDPREU/EEAOpt-inPrivacy Policy, Cookie Policy
UK GDPRUKOpt-inPrivacy Policy, Cookie Policy
CCPA/CPRACaliforniaOpt-outPrivacy Policy, Do Not Sell
US States15+ US statesOpt-outPrivacy Policy
LGPDBrazilOpt-inPrivacy Policy
PIPEDACanadaOpt-inPrivacy Policy
GenericGlobalNoticePrivacy Policy

Page Types

PageTemplateDescription
Privacy Policyprivacy_policy.eexData collection, processing, and rights
Cookie Policycookie_policy.eexCookie usage and management
Terms of Serviceterms_of_service.eexService terms and conditions
Do Not Selldo_not_sell.eexCCPA opt-out for data sales
Data Retentiondata_retention_policy.eexData retention periods and policies
CCPA Noticeccpa_notice.eexCalifornia-specific privacy notice
Acceptable Useacceptable_use.eexAcceptable use policy

The consent widget provides a glass-morphic UI with:

  • Floating icon (configurable position: bottom-left, bottom-right, top-left, top-right)
  • Consent banner for first-time visitors
  • Preferences modal with 4 consent categories (necessary, analytics, marketing, preferences)
  • Dark mode support via daisyUI CSS variables
  • ARIA-compliant accessibility
  • localStorage persistence with cross-tab sync
  • Automatic DOM injection (no layout changes required)
  • Authentication-aware display (optional hide for logged-in users)

When enabled, the widget fires consent events for Google Tag Manager:

// Default (denied)
gtag('consent', 'default', { analytics_storage: 'denied', ad_storage: 'denied' });

// After user grants analytics
gtag('consent', 'update', { analytics_storage: 'granted' });

Template Customization

Override bundled templates by placing files in your parent app's priv/legal_templates/:

priv/legal_templates/
  privacy_policy.eex        # Base template override
  privacy_policy.de.eex     # German-specific override
  cookie_policy.eex         # Base template override

Template resolution order:

  1. Parent app language-specific: priv/legal_templates/{name}.{lang}.eex
  2. Bundled language-specific template
  3. Parent app base: priv/legal_templates/{name}.eex
  4. Bundled base template

Template Variables

All templates receive:

VariableDescription
@company_nameCompany legal name
@company_addressCompany registered address
@company_countryCompany country
@company_websiteCompany website URL
@registration_numberCompany registration number
@vat_numberVAT/tax ID number
@dpo_nameData Protection Officer name
@dpo_emailDPO email address
@dpo_phoneDPO phone number
@dpo_addressDPO postal address
@frameworksList of selected framework IDs
@effective_dateCurrent date (ISO format)
@languageLanguage code

Full audit trail for GDPR compliance:

alias PhoenixKit.Modules.Legal.ConsentLog

# Log consent for a user
ConsentLog.log_consents(
  %{"analytics" => true, "marketing" => false},
  user_uuid: user.uuid,
  consent_version: "2026-03-27",
  ip_address: "192.168.1.1",
  user_agent: "Mozilla/5.0..."
)

# Check current consent status
ConsentLog.get_consent_status(user_uuid: user.uuid)
# => %{"analytics" => true, "marketing" => false, "necessary" => true}

Architecture

lib/phoenix_kit_legal/
  phoenix_kit_legal.ex          # Entry point, version info
  legal.ex                      # Main module (PhoenixKit.Module behaviour)
  legal_framework.ex            # LegalFramework struct
  page_type.ex                  # PageType struct
  schemas/
    consent_log.ex              # Consent audit trail schema
  services/
    template_generator.ex       # EEx template rendering
  web/
    consent_config_controller.ex  # JSON API for widget config
    cookie_consent.ex           # Phoenix component (consent widget)
    settings.ex                 # Admin settings LiveView
priv/
  legal_templates/              # Bundled EEx templates (7 pages)
  static/assets/
    phoenix_kit_consent.js      # Client-side consent manager

Database Table

phoenix_kit_consent_logs — Consent audit trail (UUIDv7 PK)

ColumnTypePurpose
uuidUUIDv7Primary key
user_uuidUUIDv7Logged-in user (optional)
session_idstringAnonymous session (optional)
consent_typestring"necessary", "analytics", "marketing", "preferences"
consent_givenbooleanWhether consent was granted
consent_versionstringPolicy version at time of consent
ip_addressstringIP when consent recorded
user_agent_hashstringSHA256 hash of user agent
metadataJSONBAdditional metadata

Requires either user_uuid or session_id (at least one must be present).

How It Works

  1. Parent app adds this as a dependency in mix.exs
  2. PhoenixKit scans .beam files at startup and auto-discovers the module (zero config)
  3. settings_tabs/0 registers the admin settings page
  4. Legal pages are generated from EEx templates and stored via the Publishing module
  5. Cookie consent widget is injected client-side via JavaScript
  6. Consent decisions are logged to phoenix_kit_consent_logs for audit compliance

Settings

KeyDefaultDescription
legal_enabledfalseEnable/disable module
legal_frameworks[]Selected compliance frameworks
legal_company_info{}Company details (name, address, etc.)
legal_dpo_contact{}DPO contact details
legal_consent_widget_enabledfalseEnable cookie consent widget
legal_consent_mode"strict"Consent mode: "strict" (opt-in) or "notice"
legal_cookie_banner_position"bottom-right"Widget icon position
legal_policy_version"1.0"Manual policy version string
legal_google_consent_modefalseEnable Google Consent Mode v2
legal_hide_for_authenticatedtrueHide widget for logged-in users

API Endpoint

GET /phoenix_kit/api/consent-config — Returns widget configuration as JSON.

Used by the client-side consent manager to initialize the widget. Cached publicly for 60 seconds. Auth-gating is handled server-side by the component, not this endpoint.

Development

mix deps.get       # Install dependencies
mix test           # Run tests
mix format         # Format code
mix credo --strict # Static analysis (strict mode)
mix dialyzer       # Type checking
mix docs           # Generate documentation
mix precommit      # Compile + format + credo + dialyzer
mix quality        # Format + credo + dialyzer

Dependencies

PackagePurpose
phoenix_kitModule behaviour, Settings API, core infrastructure
phoenix_kit_publishingLegal page storage as posts
phoenix_live_viewAdmin settings LiveView
ecto_sqlConsent log schema
gettextTemplate internationalization

License

MIT — see LICENSE.md for details.