# Code Guidelines and Best Practices

## Purpose

This document serves as a foundational reference for our development team, ensuring consistency, maintainability, and quality in our codebase. It defines standards and best practices for building the Mila membership management application.

## Project Context

We are building a membership management system (Mila) using the following technology stack:

**Backend & Runtime:**
- Elixir `~> 1.15` (currently 1.18.3-otp-27)
- Erlang/OTP 27.3.4
- Phoenix Framework `~> 1.8.0`
- Ash Framework `~> 3.0`
- AshPostgres `~> 2.0`
- Ecto `~> 3.10`
- Postgrex `>= 0.0.0`
- AshAuthentication `~> 4.9`
- AshAuthenticationPhoenix `~> 2.10`
- bcrypt_elixir `~> 3.0`

**Frontend & UI:**
- Phoenix LiveView `~> 1.1.0`
- Phoenix HTML `~> 4.1`
- Tailwind CSS 4.0.9
- DaisyUI (as Tailwind plugin)
- Heroicons v2.2.0
- JavaScript (ES2022)
- esbuild `~> 0.9`

**Database:**
- PostgreSQL 17.6 (dev), 16 (prod)

**Testing:**
- ExUnit (built-in)
- Ecto.Adapters.SQL.Sandbox

**Development Tools:**
- asdf 0.16.5 (version management)
- Just 1.43.0 (task runner)
- Credo `~> 1.7` (code analysis)
- Sobelow `~> 0.14` (security analysis)
- mix_audit `~> 2.1` (dependency audit)

**Infrastructure:**
- Docker & Docker Compose
- Bandit `~> 1.5` (HTTP server)

---

## Table of Contents

1. [Setup and Architectural Conventions](#1-setup-and-architectural-conventions)
2. [Coding Standards and Style](#2-coding-standards-and-style)
3. [Tooling Guidelines](#3-tooling-guidelines)
4. [Testing Standards](#4-testing-standards)
5. [Security Guidelines](#5-security-guidelines)
6. [Performance Best Practices](#6-performance-best-practices)
7. [Documentation Standards](#7-documentation-standards)
8. [Accessibility Guidelines](#8-accessibility-guidelines)

---

## 1. Setup and Architectural Conventions

### 1.1 Project Structure

Our project follows a domain-driven design approach using Phoenix contexts and Ash domains:

```
lib/
├── accounts/              # Accounts domain (AshAuthentication)
│   ├── accounts.ex        # Domain definition
│   ├── user.ex            # User resource
│   ├── token.ex           # Token resource
│   ├── user_identity.exs  # User identity helpers
│   └── user/              # User-related modules
│       ├── changes/       # Ash changes for user
│       └── preparations/  # Ash preparations for user
├── membership/            # Membership domain
│   ├── membership.ex      # Domain definition
│   ├── member.ex          # Member resource
│   ├── custom_field.ex               # Custom field (definition) resource
│   ├── custom_field_value.ex        # Custom field value resource
│   ├── setting.ex         # Global settings (singleton resource)
│   ├── group.ex           # Group resource
│   ├── member_group.ex    # MemberGroup join table resource
│   └── email.ex           # Email custom type
├── membership_fees/        # MembershipFees domain
│   ├── membership_fees.ex # Domain definition
│   ├── membership_fee_type.ex    # Membership fee type resource
│   ├── membership_fee_cycle.ex   # Membership fee cycle resource
│   └── changes/           # Ash changes for membership fees
├── mv/authorization/       # Authorization domain
│   ├── authorization.ex   # Domain definition
│   ├── role.ex            # Role resource
│   ├── permission_sets.ex # Hardcoded permission sets
│   └── checks/            # Authorization checks
├── mv/                    # Core application modules
│   ├── accounts/          # Domain-specific logic
│   │   └── user/
│   │       ├── senders/   # Email senders for user actions
│   │       └── validations/
│   ├── email_sync/        # Email synchronization logic
│   │   ├── changes/       # Sync changes
│   │   ├── helpers.ex     # Sync helper functions
│   │   └── loader.ex      # Data loaders
│   ├── membership/        # Domain-specific logic
│   │   └── member/
│   │       └── validations/
│   ├── membership_fees/   # Membership fee business logic
│   │   ├── cycle_generator.ex  # Cycle generation algorithm
│   │   └── calendar_cycles.ex  # Calendar cycle calculations
│   ├── helpers.ex         # Shared helper functions (ash_actor_opts)
│   ├── constants.ex       # Application constants (member_fields, custom_field_prefix)
│   ├── application.ex     # OTP application
│   ├── mailer.ex          # Email mailer
│   ├── release.ex         # Release tasks
│   ├── repo.ex            # Database repository
│   ├── secrets.ex         # Secret management
│   └── statistics.ex      # Reporting: member/cycle aggregates (counts, sums by year)
├── mv_web/                # Web interface layer
│   ├── components/        # UI components
│   │   ├── core_components.ex
│   │   ├── table_components.ex
│   │   ├── layouts.ex
│   │   └── layouts/       # Layout templates
│   │       ├── sidebar.ex
│   │       └── root.html.heex
│   ├── controllers/       # HTTP controllers
│   │   ├── auth_controller.ex
│   │   ├── page_controller.ex
│   │   ├── locale_controller.ex
│   │   ├── error_html.ex
│   │   ├── error_json.ex
│   │   └── page_html/
│   ├── helpers/           # Web layer helper modules
│   │   ├── member_helpers.ex        # Member display utilities
│   │   ├── membership_fee_helpers.ex # Membership fee formatting
│   │   ├── date_formatter.ex        # Date formatting utilities
│   │   └── field_type_formatter.ex   # Field type display formatting
│   ├── live/              # LiveView modules
│   │   ├── components/    # LiveView-specific components
│   │   │   ├── search_bar_component.ex
│   │   │   └── sort_header_component.ex
│   │   ├── member_live/   # Member CRUD LiveViews
│   │   ├── custom_field_value_live/ # CustomFieldValue CRUD LiveViews
│   │   ├── custom_field_live/
│   │   ├── user_live/     # User management LiveViews
│   │   ├── role_live/     # Role management LiveViews
│   │   ├── membership_fee_type_live/ # Membership fee type LiveViews
│   │   ├── membership_fee_settings_live.ex # Membership fee settings
│   │   ├── global_settings_live.ex # Global settings
│   │   ├── group_live/     # Group management LiveViews
│   │   ├── import_export_live.ex    # CSV import/export LiveView (mount/events/glue only)
│   │   ├── import_export_live/     # Import/Export UI components
│   │   │   └── components.ex       # custom_fields_notice, template_links, import_form, progress, results
│   │   ├── statistics_live.ex      # Statistics page (aggregates, year filter, joins/exits by year)
│   │   └── contribution_type_live/ # Contribution types (mock-up)
│   ├── auth_overrides.ex  # AshAuthentication overrides
│   ├── endpoint.ex        # Phoenix endpoint
│   ├── gettext.ex         # I18n configuration
│   ├── live_helpers.ex    # LiveView lifecycle hooks and helpers
│   ├── live_user_auth.ex  # LiveView authentication
│   ├── router.ex          # Application router
│   └── telemetry.ex       # Telemetry configuration
├── mv_web.ex              # Web module definition
└── mv.ex                  # Application module definition

test/
├── accounts/              # Accounts domain tests
│   ├── user_test.exs
│   ├── email_sync_edge_cases_test.exs
│   ├── email_uniqueness_test.exs
│   ├── user_email_sync_test.exs
│   ├── user_member_deletion_test.exs
│   └── user_member_relationship_test.exs
├── membership/            # Membership domain tests
│   ├── member_test.exs
│   └── member_email_sync_test.exs
├── mv_web/                # Web layer tests
│   ├── components/        # Component tests
│   │   ├── layouts/
│   │   │   └── navbar_test.exs
│   │   ├── search_bar_component_test.exs
│   │   └── sort_header_component_test.exs
│   ├── controllers/       # Controller tests
│   │   ├── auth_controller_test.exs
│   │   ├── error_html_test.exs
│   │   ├── error_json_test.exs
│   │   ├── oidc_integration_test.exs
│   │   └── page_controller_test.exs
│   ├── live/              # LiveView tests
│   │   └── profile_navigation_test.exs
│   ├── member_live/       # Member LiveView tests
│   │   └── index_test.exs
│   ├── user_live/         # User LiveView tests
│   │   ├── form_test.exs
│   │   └── index_test.exs
│   └── locale_test.exs
├── seeds_test.exs         # Database seed tests
└── support/               # Test helpers
    ├── conn_case.ex       # Controller test helpers
    ├── data_case.ex       # Data layer test helpers
    └── fixtures.ex        # Shared test fixtures (Mv.Fixtures)
```

### 1.2 Module Organization

**Module Naming:**

- **Modules:** Use `PascalCase` with full namespace (e.g., `Mv.Accounts.User`)
- **Domains:** Top-level domains are `Mv.Accounts`, `Mv.Membership`, `Mv.MembershipFees`, and `Mv.Authorization`
- **Resources:** Resource modules should be singular nouns (e.g., `Member`, not `Members`)
- **Context functions:** Use `snake_case` and verb-first naming (e.g., `create_user`, `list_members`)

**Module Structure:**

```elixir
defmodule Mv.Membership.Member do
  @moduledoc """
  Represents a club member with their personal information and membership status.
  """
  
  use Ash.Resource,
    domain: Mv.Membership,
    data_layer: AshPostgres.DataLayer
  
  # 1. Ash DSL sections in order (see Spark formatter config)
  admin do
    # ...
  end
  
  postgres do
    # ...
  end
  
  resource do
    # ...
  end
  
  code_interface do
    # ...
  end
  
  actions do
    # ...
  end
  
  policies do
    # ...
  end
  
  attributes do
    # ...
  end
  
  relationships do
    # ...
  end
  
  # 2. Public functions
  
  # 3. Private functions
end
```

### 1.3 Domain-Driven Design

**Use Ash Domains for Context Boundaries:**

Each domain should:
- Have a clear boundary and responsibility
- Define a public API through code interfaces
- Encapsulate business logic within resources
- Handle cross-domain communication explicitly

Example domain definition:

```elixir
defmodule Mv.Membership do
  use Ash.Domain,
    extensions: [AshAdmin.Domain, AshPhoenix]

  admin do
    show? true
  end

  resources do
    resource Mv.Membership.Member do
      define :create_member, action: :create_member
      define :list_members, action: :read
      define :update_member, action: :update_member
      define :destroy_member, action: :destroy
    end
  end
end
```

### 1.4 Dependency Management

- **Use `mix.exs` for all dependencies:** Define versions explicitly
- **Keep dependencies up to date:** Use Renovate for automated updates
- **Version management:** Use `asdf` with `.tool-versions` for consistent environments

### 1.5 Scalability Considerations

- **Database indexing:** Add indexes for frequently queried fields
- **Pagination:** Use Ash's keyset pagination for large datasets (default configured)
- **Background jobs:** Plan for Oban or similar for async processing
- **Caching:** Consider caching strategies for expensive operations
- **Process design:** Use OTP principles (GenServers, Supervisors) for stateful components

---

## 2. Coding Standards and Style

### 2.1 Code Formatting

**Use `mix format` for all Elixir code:**

```bash
mix format
```

**Key formatting rules:**
- **Indentation:** 2 spaces (no tabs)
- **Line length:** Maximum 120 characters (configured in `.credo.exs`)
- **Trailing whitespace:** Not allowed
- **File endings:** Always include trailing newline

**Naming Conventions Summary:**

- **Elixir:** Use `snake_case` for functions/variables, `PascalCase` for modules
- **Phoenix:** Controllers end with `Controller`, LiveViews end with `Live`
- **Ash:** Resources are singular nouns, actions are verb-first (`:create_member`)
- **Files:** Match module names in `snake_case` (`user_controller.ex` for `UserController`)

### 2.2 Function Design

**Verb-First Function Names:**

```elixir
# Good
def create_user(attrs)
def list_members(query)
def send_email(recipient, content)

# Avoid
def user_create(attrs)
def members_list(query)
def email_send(recipient, content)
```

**Use Pattern Matching in Function Heads:**

```elixir
# Good - multiple clauses with pattern matching
def handle_result({:ok, user}), do: {:ok, user}
def handle_result({:error, reason}), do: log_and_return_error(reason)

# Avoid - case/cond when pattern matching suffices
def handle_result(result) do
  case result do
    {:ok, user} -> {:ok, user}
    {:error, reason} -> log_and_return_error(reason)
  end
end
```

**Keep Functions Small and Focused:**

- Aim for functions under 20 lines
- Each function should have a single responsibility
- Extract complex logic into private helper functions

**Use Guard Clauses for Early Returns:**

```elixir
def process_user(nil), do: {:error, :user_not_found}
def process_user(%{active: false}), do: {:error, :user_inactive}
def process_user(user), do: {:ok, perform_action(user)}
```

### 2.3 Error Handling

**Use Tagged Tuples:**

```elixir
# Standard pattern
{:ok, result} | {:error, reason}

# Examples
def create_member(attrs) do
  case Ash.create(Member, attrs) do
    {:ok, member} -> {:ok, member}
    {:error, error} -> {:error, error}
  end
end
```

**Use `with` for Complex Operations:**

```elixir
def register_user(params) do
  with {:ok, validated} <- validate_params(params),
       {:ok, user} <- create_user(validated),
       {:ok, _email} <- send_welcome_email(user) do
    {:ok, user}
  else
    {:error, reason} -> {:error, reason}
  end
end
```

**Let It Crash (with Supervision):**

Don't defensively program against every possible error. Use supervisors to handle process failures:

```elixir
# In your application.ex
children = [
  Mv.Repo,
  MvWeb.Endpoint,
  {Phoenix.PubSub, name: Mv.PubSub}
]

Supervisor.start_link(children, strategy: :one_for_one)
```

### 2.4 Functional Programming Principles

**Immutability:**

```elixir
# Good - return new data structures
def add_role(user, role) do
  %{user | roles: [role | user.roles]}
end

# Avoid - mutation (not possible in Elixir anyway)
# This is just conceptual - Elixir prevents mutation
```

**Pure Functions:**

Write functions that:
- Return the same output for the same input
- Have no side effects
- Are easier to test and reason about

```elixir
# Pure function
def calculate_total(items) do
  Enum.reduce(items, 0, fn item, acc -> acc + item.price end)
end

# Impure function (side effects)
def create_and_log_user(attrs) do
  Logger.info("Creating user: #{inspect(attrs)}")  # Side effect
  Ash.create!(User, attrs)                         # Side effect
end
```

**Pipe Operator:**

Use the pipe operator `|>` for transformation chains:

```elixir
# Good
def process_members(query) do
  query
  |> filter_active()
  |> sort_by_name()
  |> limit_results(10)
end

# Avoid
def process_members(query) do
  limit_results(sort_by_name(filter_active(query)), 10)
end
```

### 2.5 Elixir-Specific Patterns

**Avoid Using Else with Unless:**

```elixir
# Good
unless user.admin? do
  {:error, :unauthorized}
end

# Avoid - confusing
unless user.admin? do
  {:error, :unauthorized}
else
  perform_admin_action()
end
```

**Use `Enum` over List Comprehensions for Clarity:**

```elixir
# Preferred for readability
users
|> Enum.filter(&(&1.active))
|> Enum.map(&(&1.name))

# List comprehension (use when more concise)
for user <- users, user.active, do: user.name
```

**String Concatenation:**

```elixir
# Good - interpolation
"Hello, #{user.name}!"

# Avoid - concatenation with <>
"Hello, " <> user.name <> "!"
```

---

## 3. Tooling Guidelines

### 3.1 Elixir & Erlang/OTP

**Version Management with asdf:**

Always use the versions specified in `.tool-versions`:

```bash
# Install correct versions
asdf install

# Verify versions
elixir --version  # Should show 1.18.3
erl -version      # Should show 27.3.4
```

**OTP Application Design:**

```elixir
defmodule Mv.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Start the database repository
      Mv.Repo,
      # Start the Telemetry supervisor
      MvWeb.Telemetry,
      # Start the PubSub system
      {Phoenix.PubSub, name: Mv.PubSub},
      # Start the Endpoint
      MvWeb.Endpoint
    ]

    opts = [strategy: :one_for_one, name: Mv.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
```

### 3.2 Phoenix Framework

**Context-Based Organization:**

- Use contexts to define API boundaries
- Keep controllers thin - delegate to contexts or Ash actions
- Avoid direct Repo/Ecto calls in controllers

```elixir
# Good - thin controller
defmodule MvWeb.MemberController do
  use MvWeb, :controller

  def create(conn, %{"member" => member_params}) do
    case Mv.Membership.create_member(member_params) do
      {:ok, member} ->
        conn
        |> put_flash(:info, "Member created successfully.")
        |> redirect(to: ~p"/members/#{member}")
      
      {:error, error} ->
        conn
        |> put_flash(:error, "Failed to create member.")
        |> render(:new, error: error)
    end
  end
end
```

**Phoenix LiveView Best Practices:**

```elixir
defmodule MvWeb.MemberLive.Index do
  use MvWeb, :live_view

  # Use mount for initial setup
  def mount(_params, _session, socket) do
    {:ok, assign(socket, members: [], loading: true)}
  end

  # Use handle_params for URL parameter handling
  def handle_params(params, _url, socket) do
    {:noreply, apply_action(socket, socket.assigns.live_action, params)}
  end

  # Use handle_event for user interactions
  def handle_event("delete", %{"id" => id}, socket) do
    # Handle deletion
    {:noreply, socket}
  end

  # Use handle_info for asynchronous messages
  def handle_info({:member_updated, member}, socket) do
    {:noreply, update_member_in_list(socket, member)}
  end
end
```

**Component Design:**

```elixir
# Function components for stateless UI elements
def button(assigns) do
  ~H"""
  <button class="btn btn-primary" {@rest}>
    <%= render_slot(@inner_block) %>
  </button>
  """
end

# Use attrs and slots for documentation
attr :id, :string, required: true
attr :title, :string, default: nil
slot :inner_block, required: true

def card(assigns) do
  ~H"""
  <div id={@id} class="card">
    <h2 :if={@title}><%= @title %></h2>
    <%= render_slot(@inner_block) %>
  </div>
  """
end
```

### 3.3 CSV Import Configuration

**CSV Import Limits:**

CSV import functionality supports configurable limits to prevent resource exhaustion:

```elixir
# config/config.exs
config :mv,
  csv_import: [
    max_file_size_mb: 10,  # Maximum file size in megabytes
    max_rows: 1000          # Maximum number of data rows (excluding header)
  ]
```

**Accessing Configuration:**

Use `Mv.Config` helper functions:

```elixir
# Get max file size in bytes
max_bytes = Mv.Config.csv_import_max_file_size_bytes()

# Get max file size in megabytes
max_mb = Mv.Config.csv_import_max_file_size_mb()

# Get max rows
max_rows = Mv.Config.csv_import_max_rows()
```

**Best Practices:**
- Set reasonable limits based on server resources
- Display limits to users in UI
- Validate file size before upload
- Process imports in chunks (default: 200 rows per chunk)
- Cap error collection (default: 50 errors per import)

### 3.4 Page-Level Authorization

**CheckPagePermission Plug:**

Use `MvWeb.Plugs.CheckPagePermission` for page-level authorization:

```elixir
# lib/mv_web/router.ex
defmodule MvWeb.Router do
  use MvWeb, :router
  
  # Add plug to router pipeline
  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MvWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug MvWeb.Plugs.CheckPagePermission  # Page-level authorization
  end
end
```

**Permission Set Route Matrix:**

Routes are mapped to permission sets:
- `own_data`: Can access `/profile` and `/members/:id` (own linked member only)
- `read_only`: Can read all data, cannot modify
- `normal_user`: Can read and modify most data
- `admin`: Full access to all routes

**Usage in LiveViews:**

```elixir
# Check page access before mount
def mount(_params, _session, socket) do
  actor = current_actor(socket)
  
  if MvWeb.Authorization.can_access_page?(actor, "/admin/roles") do
    {:ok, assign(socket, :roles, load_roles(actor))}
  else
    {:ok, redirect(socket, to: ~p"/")}
  end
end
```

**Public Paths:**

Public paths (login, OIDC callbacks) are excluded from permission checks automatically.

### 3.5 System Actor Pattern

**When to Use System Actor:**

Some operations must always run regardless of user permissions. These are **systemic operations** that are mandatory side effects:

- **Email synchronization** (Member ↔ User)
- **Email uniqueness validation** (data integrity requirement)
- **Cycle generation** (if defined as mandatory side effect)
- **Background jobs**
- **Seeds**

**Implementation:**

Use `Mv.Helpers.SystemActor.get_system_actor/0` for all systemic operations:

```elixir
# Good - Email sync uses system actor
def get_linked_member(user) do
  system_actor = SystemActor.get_system_actor()
  opts = Helpers.ash_actor_opts(system_actor)
  
  case Ash.get(Mv.Membership.Member, id, opts) do
    {:ok, member} -> member
    {:error, _} -> nil
  end
end

# Bad - Using user actor for systemic operation
def get_linked_member(user, actor) do
  opts = Helpers.ash_actor_opts(actor)  # May fail if user lacks permissions!
  # ...
end
```

**System Actor Details:**

- System actor is a user with admin role (email: "system@mila.local")
- Cached in Agent for performance
- Falls back to admin user from seeds if system user doesn't exist
- Should NEVER be used for user-initiated actions (only systemic operations)

**DO NOT use system actor as a fallback:**

- **Never** fall back to `Mv.Helpers.SystemActor.get_system_actor()` when an actor is missing or nil (e.g. in validations, changes, or when reading from context).
- Fallbacks hide bugs (callers forget to pass actor) and can cause privilege escalation (unauthenticated or low-privilege paths run with system rights).
- If no actor is available, fail explicitly (validation error, Forbidden, or clear error message). Fix the caller to pass the correct actor instead of adding a fallback.
- Use system actor only where the operation is **explicitly** a systemic operation (see list above); never as a "safety net" when actor is absent.

**User Mode vs System Mode:**

- **User Mode**: User-initiated actions use the actual user actor, policies are enforced
- **System Mode**: Systemic operations use system actor, bypass user permissions

**Authorization Bootstrap Patterns:**

Two mechanisms exist for bypassing standard authorization:

1. **system_actor** (systemic operations) - Admin user for operations that must always succeed
   ```elixir
   # Good: Systemic operation
   system_actor = SystemActor.get_system_actor()
   Ash.read(Member, actor: system_actor)
   
   # Bad: User-initiated action
   # Never use system_actor for user-initiated actions!
   ```

2. **authorize?: false** (bootstrap only) - Skips policies for circular dependencies
   ```elixir
   # Good: Bootstrap (seeds, SystemActor loading)
   Accounts.create_user!(%{email: admin_email}, authorize?: false)
   
   # Bad: User-initiated action
   Ash.destroy(member, authorize?: false)  # Never do this!
   ```

**Decision Guide:**
- Use **system_actor** for email sync, cycle generation, validations, and test fixtures
- Use **authorize?: false** only for bootstrap (seeds, circular dependencies)
- Always document why `authorize?: false` is necessary
- **Note:** NoActor bypass was removed to prevent masking authorization bugs in tests

**See also:** `docs/roles-and-permissions-architecture.md` (Authorization Bootstrap Patterns section)

### 3.6 Ash Framework

**Resource Definition Best Practices:**

```elixir
defmodule Mv.Membership.Member do
  use Ash.Resource,
    domain: Mv.Membership,
    data_layer: AshPostgres.DataLayer

  # Follow section order from Spark formatter config
  postgres do
    table "members"
    repo Mv.Repo
  end

  attributes do
    uuid_primary_key :id
    
    attribute :first_name, :string do
      allow_nil? false
      public? true
    end
    
    attribute :email, :string do
      allow_nil? false
      public? true
    end
    
    timestamps()
  end

  actions do
    # Define specific actions instead of using defaults.accept_all
    create :create_member do
      accept [:first_name, :last_name, :email]
      
      change fn changeset, _context ->
        # Custom validation or transformation
        changeset
      end
    end

    read :read do
      primary? true
    end

    update :update_member do
      accept [:first_name, :last_name, :email]
    end

    destroy :destroy
  end

  code_interface do
    define :create_member
    define :list_members, action: :read
    define :update_member
    define :destroy_member, action: :destroy
  end

  identities do
    identity :unique_email, [:email]
  end
end
```

**Ash Policies:**

```elixir
policies do
  # Admin can do everything
  policy action_type([:read, :create, :update, :destroy]) do
    authorize_if actor_attribute_equals(:role, :admin)
  end

  # Users can only read and update their own data
  policy action_type([:read, :update]) do
    authorize_if relates_to_actor_via(:user)
  end
end
```

**Ash Validations:**

```elixir
validations do
  validate present(:email), on: [:create, :update]
  validate match(:email, ~r/@/), message: "must be a valid email"
  validate string_length(:first_name, min: 2, max: 100)
end
```

### 3.4 AshPostgres & Ecto

**Migrations with Ash:**

```bash
# Generate migration for all changes
mix ash.codegen --name add_members_table

# Apply migrations
mix ash.setup
```

**Repository Configuration:**

```elixir
defmodule Mv.Repo do
  use AshPostgres.Repo,
    otp_app: :mv

  # Install PostgreSQL extensions
  def installed_extensions do
    ["citext", "uuid-ossp"]
  end
end
```

**Avoid N+1 Queries:**

```elixir
# Good - preload relationships
members = 
  Member
  |> Ash.Query.load(:custom_field_values)
  |> Mv.Membership.list_members!()

# Avoid - causes N+1 queries
members = Mv.Membership.list_members!()
Enum.map(members, fn member ->
  # This triggers a query for each member
  Ash.load!(member, :custom_field_values)
end)
```

### 3.5 Authentication (AshAuthentication)

**Resource with Authentication:**

```elixir
defmodule Mv.Accounts.User do
  use Ash.Resource,
    domain: Mv.Accounts,
    data_layer: AshPostgres.DataLayer,
    extensions: [AshAuthentication]

  authentication do
    strategies do
      password :password do
        identity_field :email
        hashed_password_field :hashed_password
      end

      oauth2 :rauthy do
        client_id fn _, _ -> 
          Application.fetch_env!(:mv, :rauthy)[:client_id]
        end
        # ... other config
      end
    end
  end
end
```

### 3.6 Frontend: Tailwind CSS & DaisyUI

**Utility-First Approach:**

```heex
<!-- Good - use utility classes -->
<div class="card bg-base-100 shadow-xl">
  <div class="card-body">
    <h2 class="card-title">Member Name</h2>
    <p class="text-sm text-gray-600">Email: member@example.com</p>
    <div class="card-actions justify-end">
      <button class="btn btn-primary">Edit</button>
    </div>
  </div>
</div>

<!-- Avoid - custom CSS for standard patterns -->
<div class="custom-member-card">
  <h2 class="custom-title">Member Name</h2>
  <!-- ... -->
</div>
```

**Responsive Design:**

```heex
<!-- Use Tailwind's responsive prefixes -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
  <%= for member <- @members do %>
    <.member_card member={member} />
  <% end %>
</div>
```

**DaisyUI Components:**

```heex
<!-- Leverage DaisyUI component classes -->
<!-- Note: Navbar has been replaced with Sidebar (see lib/mv_web/components/layouts/sidebar.ex) -->
<div class="drawer lg:drawer-open">
  <input id="drawer-toggle" type="checkbox" class="drawer-toggle" />
  <div class="drawer-content">
    <!-- Page content -->
  </div>
  <div class="drawer-side">
    <label for="drawer-toggle" class="drawer-overlay"></label>
    <aside class="w-64 min-h-full bg-base-200">
      <!-- Sidebar content -->
    </aside>
  </div>
</div>
```

**Custom Tailwind Configuration:**

Update `assets/tailwind.config.js` for custom needs:

```javascript
module.exports = {
  content: [
    "../lib/mv_web.ex",
    "../lib/mv_web/**/*.*ex"
  ],
  theme: {
    extend: {
      colors: {
        brand: "#FD4F00",
      }
    },
  },
  plugins: [
    require("@tailwindcss/forms"),
    // DaisyUI loaded from vendor
  ]
}
```

### 3.7 JavaScript & esbuild

**Minimal JavaScript Philosophy:**

Phoenix LiveView handles most interactivity. Use JavaScript only when necessary:

```javascript
// assets/js/app.js
import "phoenix_html"
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken}
})

// Show progress bar on live navigation
topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

liveSocket.connect()
```

**Custom Hooks (when needed):**

```javascript
let Hooks = {}

Hooks.DatePicker = {
  mounted() {
    // Initialize date picker
    this.el.addEventListener("change", (e) => {
      this.pushEvent("date_selected", {date: e.target.value})
    })
  }
}

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

### 3.8 Code Quality: Credo

**Static Code Analysis:**

We use **Credo** for static code analysis to ensure code quality, consistency, and maintainability. Credo checks are **mandatory** and must pass before code can be merged.

**Run Credo Regularly:**

```bash
# Check code quality
mix credo

# Strict mode for CI
mix credo --strict
```

**CI Enforcement:**

- ✅ **All Credo checks must pass in CI pipeline**
- ✅ Pull requests will be blocked if Credo checks fail
- ✅ Run `mix credo --strict` locally before pushing to catch issues early
- ✅ Address all Credo warnings and errors before requesting code review

**Key Credo Checks Enabled:**

- Consistency checks (spacing, line endings, parameter patterns)
- Design checks (FIXME/TODO tags, alias usage)
- Readability checks (max line length: 120, module/function names, **module documentation**)
- Refactoring opportunities (cyclomatic complexity, nesting)
- Warnings (unused operations, unsafe operations)

**Documentation Enforcement:**

- ✅ `Credo.Check.Readability.ModuleDoc` - **ENABLED** (as of November 2025)
- All modules require `@moduledoc` documentation
- Current coverage: 51 @moduledoc declarations across 47 modules (100% core modules)
- CI pipeline enforces documentation standards

**Address Credo Issues:**

```elixir
# Before
def complex_function(user, data, opts) do
  if user.admin? do
    if data.valid? do
      if opts[:force] do
        # deeply nested logic
      end
    end
  end
end

# After - flatten with guard clauses
def complex_function(user, _data, _opts) when not user.admin?,
  do: {:error, :unauthorized}

def complex_function(_user, data, _opts) when not data.valid?,
  do: {:error, :invalid_data}

def complex_function(_user, data, opts) do
  if opts[:force] do
    process_data(data)
  else
    validate_and_process(data)
  end
end
```

### 3.9 Security: Sobelow

**Run Security Analysis:**

```bash
# Security audit
mix sobelow --config

# With verbose output
mix sobelow --config --verbose
```

**Security Best Practices:**

- Never commit secrets to version control
- Use environment variables for sensitive configuration
- Validate and sanitize all user inputs
- Use parameterized queries (Ecto handles this)
- Keep dependencies updated

### 3.10 Dependency Auditing & Updates

**Regular Security Audits:**

```bash
# Audit dependencies for security vulnerabilities
mix deps.audit

# Audit hex packages
mix hex.audit

# Security scan with Sobelow
mix sobelow --config
```

**Update Dependencies:**

```bash
# Update all dependencies
mix deps.update --all

# Update specific dependency
mix deps.update phoenix

# Check for outdated packages
mix hex.outdated
```

### 3.11 Email: Swoosh

**Mailer Configuration:**

```elixir
defmodule Mv.Mailer do
  use Swoosh.Mailer, otp_app: :mv
end
```

**Sending Emails:**

```elixir
defmodule Mv.Accounts.WelcomeEmail do
  use Phoenix.Swoosh, template_root: "lib/mv_web/templates"
  import Swoosh.Email

  def send(user) do
    new()
    |> to({user.name, user.email})
    |> from({"Mila", "noreply@mila.example.com"})
    |> subject("Welcome to Mila!")
    |> render_body("welcome.html", %{user: user})
    |> Mv.Mailer.deliver()
  end
end
```

### 3.12 Internationalization: Gettext

**Define Translations:**

```elixir
# In LiveView or controller
gettext("Welcome to Mila")

# With interpolation
gettext("Hello, %{name}!", name: user.name)

# Domain-specific translations
dgettext("auth", "Sign in with email")
```

**Extract and Merge:**

```bash
# Extract new translatable strings
mix gettext.extract

# Merge into existing translations
mix gettext.merge priv/gettext --on-obsolete=mark_as_obsolete
```

### 3.13 Task Runner: Just

**Common Commands:**

```bash
# Start development environment
just run

# Run tests
just test

# Run linter
just lint

# Run security audit
just audit

# Reset database
just reset-database

# Format code
just format

# Regenerate migrations
just regen-migrations migration_name
```

**Define Custom Tasks:**

Edit `Justfile` for project-specific tasks:

```makefile
# Example custom task
setup-dev: install-dependencies start-database migrate-database
    mix phx.gen.secret
    @echo "Development environment ready!"
```

---

## 4. Testing Standards

### 4.1 Test Setup and Organization

**Test Directory Structure:**

Mirror the `lib/` directory structure in `test/`:

```
test/
├── accounts/              # Tests for Accounts domain
│   ├── user_test.exs
│   ├── email_sync_test.exs
│   └── ...
├── membership/            # Tests for Membership domain
│   ├── member_test.exs
│   └── ...
├── mv_web/                # Tests for Web layer
│   ├── controllers/
│   ├── live/
│   └── components/
└── support/               # Test helpers
    ├── conn_case.ex       # Controller test setup
    ├── data_case.ex       # Database test setup
    └── fixtures.ex        # Shared test fixtures (Mv.Fixtures)
```

**Test File Naming:**

- Use `_test.exs` suffix for all test files
- Match the module name: `user.ex` → `user_test.exs`
- Use descriptive names for integration tests: `user_member_relationship_test.exs`

### 4.2 ExUnit Basics

**Test Module Structure:**

```elixir
defmodule Mv.Membership.MemberTest do
  use Mv.DataCase, async: true  # async: true for parallel execution

  alias Mv.Membership.Member

  describe "create_member/1" do
    test "creates a member with valid attributes" do
      attrs = %{
        first_name: "John",
        last_name: "Doe",
        email: "john@example.com"
      }

      assert {:ok, %Member{} = member} = Mv.Membership.create_member(attrs)
      assert member.first_name == "John"
      assert member.email == "john@example.com"
    end

    test "returns error with invalid attributes" do
      attrs = %{first_name: nil}

      assert {:error, _error} = Mv.Membership.create_member(attrs)
    end
  end

  describe "list_members/0" do
    setup do
      # Setup code for this describe block
      {:ok, member: create_test_member()}
    end

    test "returns all members", %{member: member} do
      members = Mv.Membership.list_members()
      assert length(members) == 1
      assert List.first(members).id == member.id
    end
  end
end
```

### 4.3 Test Types

#### 4.3.1 Unit Tests

Test individual functions and modules in isolation:

```elixir
defmodule Mv.Membership.EmailTest do
  use ExUnit.Case, async: true

  alias Mv.Membership.Email

  describe "valid?/1" do
    test "returns true for valid email" do
      assert Email.valid?("user@example.com")
    end

    test "returns false for invalid email" do
      refute Email.valid?("invalid-email")
      refute Email.valid?("missing-at-sign.com")
    end
  end
end
```

#### 4.3.2 Integration Tests

Test interactions between multiple modules or systems:

```elixir
defmodule Mv.Accounts.UserMemberRelationshipTest do
  use Mv.DataCase, async: true

  alias Mv.Accounts.User
  alias Mv.Membership.Member

  describe "user-member relationship" do
    test "creating a user automatically creates a member" do
      attrs = %{
        email: "test@example.com",
        password: "SecurePassword123"
      }

      assert {:ok, user} = Mv.Accounts.create_user(attrs)
      assert {:ok, member} = Mv.Membership.get_member_by_user_id(user.id)
      assert member.email == user.email
    end

    test "deleting a user cascades to member" do
      {:ok, user} = create_user()
      {:ok, member} = Mv.Membership.get_member_by_user_id(user.id)

      assert :ok = Mv.Accounts.destroy_user(user)
      assert {:error, :not_found} = Mv.Membership.get_member(member.id)
    end
  end
end
```

#### 4.3.3 Controller Tests

Test HTTP endpoints:

```elixir
defmodule MvWeb.PageControllerTest do
  use MvWeb.ConnCase, async: true

  test "GET /", %{conn: conn} do
    conn = get(conn, ~p"/")
    assert html_response(conn, 200) =~ "Welcome to Mila"
  end
end
```

#### 4.3.4 LiveView Tests

Test LiveView interactions:

```elixir
defmodule MvWeb.MemberLive.IndexTest do
  use MvWeb.ConnCase, async: true

  import Phoenix.LiveViewTest

  setup do
    member = create_test_member()
    %{member: member}
  end

  test "displays list of members", %{conn: conn, member: member} do
    {:ok, view, html} = live(conn, ~p"/members")

    assert html =~ "Members"
    assert html =~ member.first_name
  end

  test "deletes member", %{conn: conn, member: member} do
    {:ok, view, _html} = live(conn, ~p"/members")

    assert view
           |> element("#member-#{member.id} a", "Delete")
           |> render_click()

    refute has_element?(view, "#member-#{member.id}")
  end
end
```

#### 4.3.5 Component Tests

Test function components:

```elixir
defmodule MvWeb.Components.SearchBarComponentTest do
  use MvWeb.ConnCase, async: true

  import Phoenix.LiveViewTest
  import MvWeb.Components.SearchBarComponent

  test "renders search input" do
    assigns = %{search_query: "", id: "search"}

    html = 
      render_component(&search_bar/1, assigns)

    assert html =~ "input"
    assert html =~ ~s(type="search")
  end
end
```

### 4.4 Test Helpers and Fixtures

**Create Test Helpers:**

```elixir
# test/support/fixtures.ex
defmodule Mv.Fixtures do
  def member_fixture(attrs \\ %{}) do
    default_attrs = %{
      first_name: "Test",
      last_name: "User",
      email: "test#{System.unique_integer()}@example.com"
    }

    {:ok, member} =
      default_attrs
      |> Map.merge(attrs)
      |> Mv.Membership.create_member()

    member
  end
end
```

**Use Setup Blocks:**

```elixir
describe "with authenticated user" do
  setup %{conn: conn} do
    user = create_user()
    conn = log_in_user(conn, user)
    %{conn: conn, user: user}
  end

  test "can access protected page", %{conn: conn} do
    conn = get(conn, ~p"/profile")
    assert html_response(conn, 200) =~ "Profile"
  end
end
```

### 4.5 Database Testing with Sandbox

**Use Ecto Sandbox for Isolation:**

```elixir
# test/test_helper.exs
ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(Mv.Repo, :manual)
```

```elixir
# test/support/data_case.ex
defmodule Mv.DataCase do
  use ExUnit.CaseTemplate

  using do
    quote do
      import Ecto
      import Ecto.Changeset
      import Ecto.Query
      import Mv.DataCase

      alias Mv.Repo
    end
  end

  setup tags do
    pid = Ecto.Adapters.SQL.Sandbox.start_owner!(Mv.Repo, shared: not tags[:async])
    on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
    :ok
  end
end
```

### 4.6 Test Coverage

**Run Tests with Coverage:**

```bash
# Run tests
mix test

# Run with coverage
mix test --cover

# Run specific test file
mix test test/membership/member_test.exs

# Run specific test line
mix test test/membership/member_test.exs:42
```

**Coverage Goals:**

- Aim for >80% overall coverage
- 100% coverage for critical business logic
- Focus on meaningful tests, not just coverage numbers

### 4.7 Testing Best Practices

**Testing Philosophy: Focus on Business Logic, Not Framework Functionality**

We test our business logic and domain-specific behavior, not core framework features. Framework features (Ash validations, Ecto relationships, etc.) are already tested by their respective libraries.

**What We Test:**
- Business rules and validations specific to our domain
- Custom business logic (slug generation, calculations, etc.)
- Integration between our resources
- Database-level constraints (unique constraints, foreign keys, CASCADE)
- Query performance (N+1 prevention)

**What We Don't Test:**
- Framework core functionality (Ash validations work, Ecto relationships work, etc.)
- Standard CRUD operations without custom logic
- Framework-provided features that are already tested upstream
- Detailed slug generation edge cases (Umlauts, truncation, etc.) if covered by reusable change tests

**Example:**
```elixir
# ✅ GOOD - Tests our business rule
test "slug is immutable (doesn't change when name is updated)" do
  {:ok, group} = Membership.create_group(%{name: "Original"}, actor: actor)
  original_slug = group.slug
  
  {:ok, updated} = Membership.update_group(group, %{name: "New"}, actor: actor)
  assert updated.slug == original_slug  # Business rule: slug doesn't change
end

# ❌ AVOID - Tests framework functionality
test "Ash.Changeset validates required fields" do
  # This is already tested by Ash framework
end
```

**Descriptive Test Names:**

```elixir
# Good - describes what is being tested
test "creates a member with valid email address"
test "returns error when email is already taken"
test "sends welcome email after successful registration"

# Avoid - vague or generic
test "member creation"
test "error case"
test "test 1"
```

**Arrange-Act-Assert Pattern:**

```elixir
test "updates member email" do
  # Arrange - set up test data
  member = member_fixture()
  new_email = "new@example.com"

  # Act - perform the action
  {:ok, updated_member} = Mv.Membership.update_member(member, %{email: new_email})

  # Assert - verify results
  assert updated_member.email == new_email
end
```

**Test One Thing Per Test:**

```elixir
# Good - focused test
test "validates email format" do
  attrs = %{email: "invalid-email"}
  assert {:error, _} = Mv.Membership.create_member(attrs)
end

test "requires email to be present" do
  attrs = %{email: nil}
  assert {:error, _} = Mv.Membership.create_member(attrs)
end

# Avoid - testing multiple things
test "validates email" do
  # Tests both format and presence
  assert {:error, _} = Mv.Membership.create_member(%{email: nil})
  assert {:error, _} = Mv.Membership.create_member(%{email: "invalid"})
end
```

**Use describe Blocks for Organization:**

```elixir
describe "create_member/1" do
  test "success case" do
    # ...
  end

  test "error case" do
    # ...
  end
end

describe "update_member/2" do
  test "success case" do
    # ...
  end
end
```

**Avoid Testing Implementation Details:**

```elixir
# Good - test behavior
test "member can be created with valid attributes" do
  attrs = valid_member_attrs()
  assert {:ok, %Member{}} = Mv.Membership.create_member(attrs)
end

# Avoid - testing internal implementation
test "create_member calls Ash.create with correct params" do
  # This is too coupled to implementation
end
```

**Keep Tests Fast:**

- Use `async: true` when possible
- Avoid unnecessary database interactions
- Mock external services
- Use fixtures efficiently

**Performance Tests:**

Performance tests that explicitly validate performance characteristics should be tagged with `@tag :slow` or `@describetag :slow` to exclude them from standard test runs. This improves developer feedback loops while maintaining comprehensive coverage.

**When to Tag as `:slow`:**

- Tests that explicitly measure execution time or validate performance characteristics
- Tests that use large datasets (e.g., 50+ records) to test scalability
- Tests that validate query optimization (N+1 prevention, index usage)

**Tagging Guidelines:**

- Use `@tag :slow` for individual tests
- Use `@describetag :slow` for entire describe blocks (not `@moduletag`, as it affects all tests in the module)
- Performance tests should include measurable assertions (query counts, timing with tolerance, etc.)

**UI Tests:**

UI tests that validate basic HTML rendering, Phoenix LiveView navigation, or framework functionality (Gettext translations, form elements, UI state changes) should be tagged with `@tag :ui` or `@describetag :ui` to exclude them from fast CI runs. Use `@tag :ui` for individual tests and `@describetag :ui` for describe blocks. UI tests can be consolidated when they test similar elements (e.g., multiple translation tests combined into one). Do not tag business logic tests (e.g., "can delete a user"), validation tests, or data persistence tests as `:ui`.

**Running Tests:**

```bash
# Fast tests only (excludes slow and UI tests)
mix test --exclude slow --exclude ui
# Or use the Justfile command:
just test-fast

# UI tests only
mix test --only ui
# Or use the Justfile command:
just ui

# Performance tests only
mix test --only slow
# Or use the Justfile command:
just slow

# All tests (including slow and UI tests)
mix test
# Or use the Justfile command:
just test
# Or use the Justfile command:
just test-all
```

**Test Organization Best Practices:**

- **Fast Tests (Standard CI):** Business logic, validations, data persistence, edge cases
- **UI Tests (Full Test Suite):** Basic HTML rendering, navigation, translations, UI state
- **Performance Tests (Full Test Suite):** Query optimization, large datasets, timing assertions

This organization ensures fast feedback in standard CI while maintaining comprehensive coverage via promotion before merge.
---

## 5. Security Guidelines

### 5.0 No system-actor fallbacks (mandatory)

**Do not use the system actor as a fallback when an actor is missing.**

Examples of forbidden patterns:

```elixir
# ❌ FORBIDDEN - Fallback to system actor when actor is nil
actor = Map.get(changeset.context, :actor) || Mv.Helpers.SystemActor.get_system_actor()

# ❌ FORBIDDEN - "Safety" fallback in validations, changes, or helpers
actor = opts[:actor] || Mv.Helpers.SystemActor.get_system_actor()

# ❌ FORBIDDEN - Default actor in function options
def list_something(opts \\ []) do
  actor = Keyword.get(opts, :actor) || Mv.Helpers.SystemActor.get_system_actor()
  # ...
end
```

**Why:** Fallbacks hide missing-actor bugs and can lead to privilege escalation (e.g. a request without actor would run with system privileges). Always require the caller to pass the actor for user-facing or context-dependent operations; if none is available, return an error or fail validation instead of using the system actor.

**Allowed:** Use the system actor only where the operation is **by design** a systemic operation (e.g. email sync, seeds, test fixtures, background jobs) and you explicitly call `SystemActor.get_system_actor()` at that call site—never as a fallback when `actor` is nil or absent.

### 5.1 Authentication & Authorization

**Use AshAuthentication:**

```elixir
# Authentication is configured at the resource level
authentication do
  strategies do
    password :password do
      identity_field :email
      hashed_password_field :hashed_password
    end

    oauth2 :rauthy do
      # OIDC configuration
    end
  end
end
```

**Implement Authorization Policies:**

```elixir
policies do
  # Default deny
  policy action_type(:*) do
    authorize_if always()
  end

  # Use HasPermission check for role-based authorization
  policy action_type([:read, :update, :create, :destroy]) do
    authorize_if Mv.Authorization.Checks.HasPermission
  end
end
```

**Actor Handling in LiveViews:**

Always use the `current_actor/1` helper for consistent actor access:

```elixir
# In LiveView modules
import MvWeb.LiveHelpers, only: [current_actor: 1, ash_actor_opts: 1, submit_form: 3]

def mount(_params, _session, socket) do
  actor = current_actor(socket)
  
  case Ash.read(Mv.Membership.Member, ash_actor_opts(actor)) do
    {:ok, members} ->
      {:ok, assign(socket, :members, members)}
    {:error, error} ->
      {:ok, put_flash(socket, :error, "Failed to load members")}
  end
end

def handle_event("save", %{"member" => params}, socket) do
  actor = current_actor(socket)
  form = AshPhoenix.Form.for_create(Mv.Membership.Member, :create)
  
  case submit_form(form, params, actor) do
    {:ok, member} ->
      {:noreply, push_navigate(socket, to: ~p"/members/#{member.id}")}
    {:error, form} ->
      {:noreply, assign(socket, :form, form)}
  end
end
```

**Never use bang calls (`Ash.read!`, `Ash.get!`) without error handling:**

```elixir
# Bad - will crash on authorization errors
members = Ash.read!(Mv.Membership.Member, actor: actor)

# Good - proper error handling
case Ash.read(Mv.Membership.Member, actor: actor) do
  {:ok, members} -> # success
  {:error, %Ash.Error.Forbidden{}} -> # handle authorization error
  {:error, error} -> # handle other errors
end
```

### 5.1a Authorization in Tests

**IMPORTANT:** All tests must explicitly provide an actor for Ash operations. The NoActor bypass has been removed to prevent masking authorization bugs.

**Exception: AshAuthentication Bypass Tests**

Tests that verify the AshAuthentication bypass mechanism are a **conscious exception**. These tests must verify that registration/login works **without an actor** via the `AshAuthenticationInteraction` check. To enable this bypass in tests, set the context explicitly:

```elixir
# ✅ GOOD - Testing AshAuthentication bypass (conscious exception)
changeset =
  Accounts.User
  |> Ash.Changeset.for_create(:register_with_password, %{...})
  |> Ash.Changeset.set_context(%{private: %{ash_authentication?: true}})

{:ok, user} = Ash.create(changeset)  # No actor - tests bypass mechanism

# ❌ BAD - Using system_actor masks the bypass test
system_actor = Mv.Helpers.SystemActor.get_system_actor()
Ash.create(changeset, actor: system_actor)  # Tests admin permissions, not bypass!
```

**Test Fixtures:**

All test fixtures use `system_actor` for authorization:

```elixir
# test/support/fixtures.ex
def member_fixture(attrs \\ %{}) do
  system_actor = Mv.Helpers.SystemActor.get_system_actor()
  
  attrs
  |> Enum.into(%{...})
  |> Mv.Membership.create_member(actor: system_actor)
end
```

**Why Explicit Actors in Tests:**

- Prevents masking authorization bugs
- Makes authorization requirements explicit
- Tests fail if authorization is broken (fail-fast)
- Consistent with production code patterns

**Using system_actor in Tests:**

```elixir
# ✅ GOOD - Explicit actor in tests
system_actor = Mv.Helpers.SystemActor.get_system_actor()
Ash.create!(Member, attrs, actor: system_actor)

# ❌ BAD - Missing actor (will fail)
Ash.create!(Member, attrs)  # Forbidden error!
```

**For Bootstrap Operations:**

Use `authorize?: false` only for bootstrap scenarios (seeds, SystemActor initialization):

```elixir
# ✅ GOOD - Bootstrap only
Accounts.create_user!(%{email: admin_email}, authorize?: false)

# ❌ BAD - Never use in tests for normal operations
Ash.create!(Member, attrs, authorize?: false)  # Never do this!
```

### 5.2 Password Security

**Use bcrypt for Password Hashing:**

```elixir
# Configured in AshAuthentication resource
password :password do
  identity_field :email
  hashed_password_field :hashed_password
  hash_provider AshAuthentication.BcryptProvider
  
  confirmation_required? true
end
```

**Password Requirements:**

- Minimum 12 characters
- Mix of uppercase, lowercase, numbers (enforced by validation)
- Use `bcrypt_elixir` for hashing (never store plain text passwords)

### 5.3 Input Validation & Sanitization

**Validate All User Input:**

```elixir
attributes do
  attribute :email, :string do
    allow_nil? false
    public? true
  end
end

validations do
  validate present(:email)
  validate match(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/)
end
```

**SQL Injection Prevention:**

Ecto and Ash handle parameterized queries automatically:

```elixir
# Safe - parameterized query
Ash.Query.filter(Member, email == ^user_email)

# Avoid raw SQL when possible
Ecto.Adapters.SQL.query(Mv.Repo, "SELECT * FROM members WHERE email = $1", [user_email])
```

### 5.4 CSRF Protection

**Phoenix Handles CSRF Automatically:**

```heex
<!-- CSRF token automatically included in forms -->
<.form for={@form} phx-submit="save">
  <!-- form fields -->
</.form>
```

**Configure in Endpoint:**

```elixir
# lib/mv_web/endpoint.ex
plug Plug.Session,
  store: :cookie,
  key: "_mv_key",
  signing_salt: "secret"

plug :protect_from_forgery
```

### 5.5 Secrets Management

**Never Commit Secrets:**

```bash
# .gitignore should include:
.env
.env.*
!.env.example
```

**Use Environment Variables:**

```elixir
# config/runtime.exs
config :mv, :rauthy,
  client_id: System.get_env("OIDC_CLIENT_ID") || "mv",
  client_secret: System.get_env("OIDC_CLIENT_SECRET"),
  base_url: System.get_env("OIDC_BASE_URL")
```

**Generate Secure Secrets:**

```bash
# Generate secret key base
mix phx.gen.secret

# Generate token signing secret
mix phx.gen.secret
```

### 5.6 Security Headers

**Configure Security Headers:**

```elixir
# lib/mv_web/endpoint.ex
plug Plug.Static,
  at: "/",
  from: :mv,
  gzip: false,
  only: MvWeb.static_paths(),
  headers: %{
    "x-content-type-options" => "nosniff",
    "x-frame-options" => "SAMEORIGIN",
    "x-xss-protection" => "1; mode=block"
  }
```

### 5.7 Dependency Security

- **Use Renovate for automated dependency updates**
- **Review changelogs before updating dependencies**
- **Test thoroughly after updates**
- **Run regular audits** (see section 3.10 for audit commands)

### 5.8 Logging & Monitoring

**Sanitize Logs:**

```elixir
# Don't log sensitive information
Logger.info("User login attempt", user_id: user.id)

# Avoid
Logger.info("User login attempt", user: inspect(user))  # May contain password
```

**Configure Logger:**

```elixir
# config/config.exs
config :logger, :default_formatter,
  format: "$time $metadata[$level] $message\n",
  metadata: [:request_id, :user_id]
```

---

## 6. Performance Best Practices

### 6.1 Database Performance

**Indexing:**

```elixir
postgres do
  table "members"
  repo Mv.Repo

  # Add indexes for frequently queried fields
  index [:email], unique: true
  index [:last_name]
  index [:created_at]
end
```

**Avoid N+1 Queries:**

```elixir
# Good - preload relationships
members = 
  Member
  |> Ash.Query.load([:custom_field_values, :user])
  |> Mv.Membership.list_members!()

# Avoid - causes N+1
members = Mv.Membership.list_members!()
Enum.map(members, fn member ->
  custom_field_values = Ash.load!(member, :custom_field_values)  # N queries!
end)
```

**Pagination:**

```elixir
# Use keyset pagination (configured as default in Ash)
Ash.Query.page(Member, offset: 0, limit: 50)
```

**Batch Operations:**

```elixir
# Use bulk operations for multiple records
Ash.bulk_create([member1_attrs, member2_attrs, member3_attrs], Member, :create)
```

### 6.2 LiveView Performance

**Optimize Assigns:**

```elixir
# Good - only assign what's needed
def mount(_params, _session, socket) do
  {:ok, assign(socket, members_count: get_count())}
end

# Avoid - assigning large collections unnecessarily
def mount(_params, _session, socket) do
  {:ok, assign(socket, all_members: list_all_members())}  # Heavy!
end
```

**Use Temporary Assigns:**

```elixir
# For data that's only needed for rendering
def handle_event("load_report", _, socket) do
  report_data = generate_large_report()
  {:noreply, assign(socket, report: report_data) |> assign(:report, temporary_assigns: [:report])}
end
```

**Stream Collections:**

```elixir
# For large collections
def mount(_params, _session, socket) do
  {:ok, stream(socket, :members, list_members())}
end

def render(assigns) do
  ~H"""
  <div id="members" phx-update="stream">
    <div :for={{id, member} <- @streams.members} id={id}>
      <%= member.name %>
    </div>
  </div>
  """
end
```

### 6.3 Caching Strategies

**Function-Level Caching:**

```elixir
defmodule Mv.Cache do
  use GenServer

  def get_or_compute(key, compute_fn) do
    case get(key) do
      nil ->
        value = compute_fn.()
        put(key, value)
        value
      
      value ->
        value
    end
  end
end
```

**ETS for In-Memory Cache:**

```elixir
# In application.ex
:ets.new(:mv_cache, [:named_table, :public, read_concurrency: true])

# Usage
:ets.insert(:mv_cache, {"key", "value"})
:ets.lookup(:mv_cache, "key")
```

### 6.4 Async Processing

**Background Jobs (Future):**

```elixir
# When Oban is added
defmodule Mv.Workers.EmailWorker do
  use Oban.Worker

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"user_id" => user_id}}) do
    user = Mv.Accounts.get_user!(user_id)
    Mv.Mailer.send_welcome_email(user)
    :ok
  end
end

# Enqueue job
%{user_id: user.id}
|> Mv.Workers.EmailWorker.new()
|> Oban.insert()
```

**Task Async for Concurrent Operations:**

```elixir
def gather_dashboard_data do
  tasks = [
    Task.async(fn -> get_member_count() end),
    Task.async(fn -> get_recent_registrations() end),
    Task.async(fn -> get_payment_status() end)
  ]

  [member_count, recent, payments] = Task.await_many(tasks)

  %{
    member_count: member_count,
    recent: recent,
    payments: payments
  }
end
```

### 6.5 Profiling & Monitoring

**Use :observer:**

```elixir
# In iex session
:observer.start()
```

**Use Telemetry:**

```elixir
# Attach telemetry handlers
:telemetry.attach(
  "query-duration",
  [:mv, :repo, :query],
  fn event, measurements, metadata, _config ->
    Logger.info("Query took #{measurements.total_time}ms")
  end,
  nil
)
```

---

## 7. Documentation Standards

### 7.1 Module Documentation

**Use @moduledoc:**

```elixir
defmodule Mv.Membership.Member do
  @moduledoc """
  Represents a club member with their personal information and membership status.

  Members can have custom_field_values defined by the club administrators.
  Each member is optionally linked to a user account for self-service access.

  ## Examples

      iex> Mv.Membership.create_member(%{first_name: "John", last_name: "Doe", email: "john@example.com"})
      {:ok, %Mv.Membership.Member{}}

  """
end
```

**Module Documentation Should Include:**

- Purpose of the module
- Key responsibilities
- Usage examples
- Related modules

### 7.2 Function Documentation

**Use @doc:**

```elixir
@doc """
Creates a new member with the given attributes.

## Parameters

  - `attrs` - A map of member attributes including:
    - `:first_name` (required) - The member's first name
    - `:last_name` (required) - The member's last name
    - `:email` (required) - The member's email address

## Returns

  - `{:ok, member}` - Successfully created member
  - `{:error, error}` - Validation or creation error

## Examples

    iex> Mv.Membership.create_member(%{
    ...>   first_name: "Jane",
    ...>   last_name: "Smith",
    ...>   email: "jane@example.com"
    ...> })
    {:ok, %Mv.Membership.Member{first_name: "Jane"}}

    iex> Mv.Membership.create_member(%{first_name: nil})
    {:error, %Ash.Error.Invalid{}}

"""
@spec create_member(map()) :: {:ok, Member.t()} | {:error, Ash.Error.t()}
def create_member(attrs) do
  # Implementation
end
```

### 7.3 Type Specifications

**Use @spec for Function Signatures:**

```elixir
@spec create_member(map()) :: {:ok, Member.t()} | {:error, Ash.Error.t()}
def create_member(attrs)

@spec list_members(keyword()) :: [Member.t()]
def list_members(opts \\ [])

@spec get_member!(String.t()) :: Member.t() | no_return()
def get_member!(id)
```

### 7.4 Code Comments

**When to Comment:**

```elixir
# Good - explain WHY, not WHAT
def calculate_dues(member) do
  # Annual dues are prorated based on join date to be fair to mid-year joiners
  months_active = calculate_months_active(member)
  @annual_dues * (months_active / 12)
end

# Avoid - stating the obvious
def calculate_dues(member) do
  # Calculate the dues
  months_active = calculate_months_active(member)  # Get months active
  @annual_dues * (months_active / 12)  # Multiply annual dues by fraction
end
```

**Complex Logic:**

```elixir
def sync_member_email(member, user) do
  # Email synchronization priority:
  # 1. User email is the source of truth (authenticated account)
  # 2. Member email is updated to match user email
  # 3. Preserve member email history for audit purposes
  
  if member.email != user.email do
    # Archive old email before updating
    archive_member_email(member, member.email)
    update_member(member, %{email: user.email})
  end
end
```

### 7.5 README and Project Documentation

**Keep README.md Updated:**

- Installation instructions
- Development setup
- Running tests
- Deployment guide
- Contributing guidelines

**Additional Documentation:**

- `docs/` directory for detailed guides
- Architecture decision records (ADRs)
- API documentation (generated with ExDoc)

**Generate Documentation:**

```bash
# Generate HTML documentation
mix docs

# View documentation
open doc/index.html
```

### 7.6 Changelog

**Maintain CHANGELOG.md:**

```markdown
# Changelog

## [Unreleased]

### Added
- Member custom_field_values feature
- Email synchronization between user and member

### Changed
- Updated Phoenix to 1.8.0

### Fixed
- Email uniqueness validation bug

## [0.1.0] - 2025-01-15

### Added
- Initial release
- Basic member management
- OIDC authentication
```

---

## 8. Git Workflow

### 8.1 Branching Strategy

**Main Branches:**

- `main` - Production-ready code

**Feature Branches:**

```bash
# Create feature branch
git checkout -b feature/member-custom-custom_field_values

# Work on feature
git add .
git commit -m "Add custom_field_values to members"

# Push to remote
git push origin feature/member-custom-custom_field_values
```

### 8.2 Commit Messages

**Format:**

```
<type>: <subject>

<body>

<footer>
```

**Types:**

- `feat:` New feature
- `fix:` Bug fix
- `docs:` Documentation changes
- `style:` Code style changes (formatting)
- `refactor:` Code refactoring
- `test:` Adding or updating tests
- `chore:` Maintenance tasks

**Examples:**

```
feat: add email synchronization for members

Implement automatic email sync between user accounts and member records.
When a user updates their email, the associated member record is updated.

Closes #123
```

```
fix: resolve N+1 query in member list

Preload custom_field_values relationship when loading members to avoid N+1 queries.

Performance improvement: reduced query count from 100+ to 2.
```

### 8.3 Code Reviews

**Before Creating PR:**

- Run `mix format`
- Run `mix credo`
- Run `mix test`
- Run `mix sobelow --config`
- Update documentation if needed

**PR Description Should Include:**

- What changed
- Why it changed
- How to test it
- Screenshots (for UI changes)
- Related issues

---

## 9. Deployment

### 9.1 Environment Configuration

**Production Checklist:**

- [ ] Set `SECRET_KEY_BASE` (use `mix phx.gen.secret`)
- [ ] Set `TOKEN_SIGNING_SECRET`
- [ ] Configure `DATABASE_URL`
- [ ] Set `PHX_HOST`
- [ ] Configure OIDC credentials
- [ ] Set up SMTP for email
- [ ] Enable SSL/TLS
- [ ] Configure monitoring

### 9.2 Database Migrations

```bash
# In production Docker container
docker compose -f docker-compose.prod.yml exec app /app/bin/mv eval "Mv.Release.migrate"
```

### 9.3 Building Docker Image

```bash
# Build production image
docker build -t mitgliederverwaltung .

# Or use Just
just build-docker-container
```

### 9.4 Health Checks

**Implement Health Check Endpoint:**

```elixir
# lib/mv_web/controllers/health_controller.ex
defmodule MvWeb.HealthController do
  use MvWeb, :controller

  def index(conn, _params) do
    # Check database connectivity
    case Ecto.Adapters.SQL.query(Mv.Repo, "SELECT 1", []) do
      {:ok, _} -> json(conn, %{status: "healthy"})
      {:error, _} -> conn |> put_status(503) |> json(%{status: "unhealthy"})
    end
  end
end
```

---

## 10. Additional Resources

### 10.1 Official Documentation

- **Elixir:** https://elixir-lang.org/docs.html
- **Phoenix:** https://hexdocs.pm/phoenix/
- **Ash Framework:** https://hexdocs.pm/ash/
- **LiveView:** https://hexdocs.pm/phoenix_live_view/
- **Ecto:** https://hexdocs.pm/ecto/
- **Tailwind CSS:** https://tailwindcss.com/docs
- **DaisyUI:** https://daisyui.com/

### 10.2 Books & Guides

- Programming Elixir by Dave Thomas
- Phoenix in Action by Geoffrey Lessel
- Testing Elixir by Andrea Leopardi & Jeffrey Matthias
- Designing Elixir Systems with OTP by James Edward Gray II

### 10.3 Community

- Elixir Forum: https://elixirforum.com/
- Ash Framework Discord
- Phoenix Framework Slack

---

## 8. Accessibility Guidelines

Building accessible applications ensures that all users, including those with disabilities, can use our application effectively. The following guidelines follow WCAG 2.1 Level AA standards.

### 8.1 Semantic HTML

**Use Semantic Elements:**

```heex
<!-- Good - semantic HTML -->
<nav class="navbar">
  <a href="/">Home</a>
</nav>

<main>
  <article>
    <h1>Member Details</h1>
    <section>
      <h2>Contact Information</h2>
      <p>Email: <%= @member.email %></p>
    </section>
  </article>
</main>

<!-- Avoid - non-semantic divs -->
<div class="navigation">
  <div class="link">Home</div>
</div>
```

### 8.2 ARIA Labels and Roles

**Use ARIA Attributes When Necessary:**

```heex
<!-- Icon-only buttons need labels -->
<button aria-label={gettext("Delete member")} phx-click="delete">
  <.icon name="hero-trash" />
</button>

<!-- Loading states -->
<div role="status" aria-live="polite" aria-busy={@loading}>
  <%= if @loading do %>
    <span aria-label={gettext("Loading...")}>
      <.icon name="hero-loading" class="animate-spin" />
    </span>
  <% end %>
</div>

<!-- Navigation landmarks -->
<nav aria-label={gettext("Main navigation")}>
  <!-- navigation items -->
</nav>
```

### 8.3 Keyboard Navigation

**All Interactive Elements Must Be Keyboard Accessible:**

```heex
<!-- Good - keyboard accessible -->
<.link navigate={~p"/members"} class="btn">
  Members
</.link>

<!-- Good - custom keyboard handler -->
<div 
  tabindex="0" 
  role="button"
  phx-click="toggle"
  phx-keydown="toggle"
  phx-key="Enter"
  aria-pressed={@expanded}>
  Toggle
</div>

<!-- Avoid - div without keyboard support -->
<div phx-click="action">Click me</div>
```

**Tab Order:**

- Ensure logical tab order matches visual order
- Use `tabindex="0"` for custom interactive elements
- Use `tabindex="-1"` to programmatically focus (not in tab order)
- Never use positive `tabindex` values

### 8.4 Color and Contrast

**Ensure Sufficient Contrast:**

```elixir
# Tailwind classes with sufficient contrast (4.5:1 minimum)
# Good
<p class="text-gray-900 bg-white">High contrast text</p>
<p class="text-white bg-gray-900">Inverted high contrast</p>

# Avoid - insufficient contrast
<p class="text-gray-400 bg-gray-300">Low contrast text</p>
```

**Don't Rely Solely on Color:**

```heex
<!-- Good - color + icon + text -->
<div class="alert alert-error">
  <.icon name="hero-exclamation-circle" />
  <span><%= gettext("Error: Email is required") %></span>
</div>

<!-- Avoid - color only -->
<div class="text-red-500">
  <%= gettext("Email is required") %>
</div>
```

### 8.5 Form Accessibility

**Label All Form Fields:**

```heex
<!-- Good - explicit labels -->
<.input
  field={@form[:email]}
  type="email"
  label={gettext("Email Address")}
  required
/>

<!-- Good - with helper text -->
<.input
  field={@form[:password]}
  type="password"
  label={gettext("Password")}
  help={gettext("Must be at least 12 characters")}
  required
/>
```

**Error Messages:**

```heex
<!-- Accessible error messages -->
<.input
  field={@form[:email]}
  type="email"
  label={gettext("Email")}
  errors={@errors[:email]}
  aria-describedby={@errors[:email] && "email-error"}
/>
<span :if={@errors[:email]} id="email-error" role="alert">
  <%= @errors[:email] %>
</span>
```

**Required Fields:**

```heex
<!-- Mark required fields -->
<.input
  field={@form[:first_name]}
  label={gettext("First Name")}
  required
  aria-required="true"
/>
```

### 8.6 Focus Management

**Manage Focus in LiveView:**

```elixir
def handle_event("save", params, socket) do
  case save_member(params) do
    {:ok, member} ->
      socket
      |> put_flash(:info, gettext("Member saved successfully"))
      |> push_navigate(to: ~p"/members/#{member}")
      # Focus will move to the new page
    
    {:error, changeset} ->
      socket
      |> assign(:form, to_form(changeset))
      # Keep focus context
  end
end
```

**Skip Links:**

```heex
<!-- Add skip link for keyboard users -->
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-0 focus:left-0 focus:z-50 focus:p-4 focus:bg-white">
  <%= gettext("Skip to main content") %>
</a>

<main id="main-content">
  <!-- main content -->
</main>
```

### 8.7 Images and Media

**Alt Text for Images:**

```heex
<!-- Good - descriptive alt text -->
<img src="/images/member-photo.jpg" alt={gettext("Photo of %{name}", name: @member.name)} />

<!-- Decorative images -->
<img src="/images/decoration.svg" alt="" role="presentation" />
```

**Icons:**

```heex
<!-- Icons with meaning need labels -->
<.icon name="hero-check-circle" aria-label={gettext("Success")} />

<!-- Decorative icons -->
<.icon name="hero-sparkles" aria-hidden="true" />
```

### 8.8 Tables

**Accessible Data Tables:**

```heex
<table>
  <caption><%= gettext("List of members") %></caption>
  <thead>
    <tr>
      <th scope="col"><%= gettext("Name") %></th>
      <th scope="col"><%= gettext("Email") %></th>
      <th scope="col"><%= gettext("Actions") %></th>
    </tr>
  </thead>
  <tbody>
    <tr :for={member <- @members}>
      <td><%= member.name %></td>
      <td><%= member.email %></td>
      <td>
        <.link navigate={~p"/members/#{member}"} aria-label={gettext("View %{name}", name: member.name)}>
          <%= gettext("View") %>
        </.link>
      </td>
    </tr>
  </tbody>
</table>
```

### 8.9 Live Regions

**Announce Dynamic Content:**

```heex
<!-- Search results announcement -->
<div role="status" aria-live="polite" aria-atomic="true">
  <%= if @searched do %>
    <span class="sr-only">
      <%= ngettext("Found %{count} member", "Found %{count} members", @count) %>
    </span>
  <% end %>
</div>

<!-- Status messages -->
<div role="alert" aria-live="assertive">
  <%= if @error do %>
    <%= @error %>
  <% end %>
</div>
```

### 8.10 Testing Accessibility

**Tools and Practices:**

```bash
# Browser DevTools
# - Chrome: Lighthouse Accessibility Audit
# - Firefox: Accessibility Inspector

# Automated testing (future)
# - pa11y
# - axe-core
```

**Manual Testing:**

1. **Keyboard Navigation:** Navigate entire application using only keyboard
2. **Screen Reader:** Test with NVDA (Windows) or VoiceOver (Mac)
3. **Zoom:** Test at 200% zoom level
4. **Color Blindness:** Use browser extensions to simulate

**Checklist:**

- [ ] All images have alt text (or alt="" for decorative)
- [ ] All form inputs have labels
- [ ] All interactive elements are keyboard accessible
- [ ] Color contrast meets WCAG AA (4.5:1 for normal text)
- [ ] Focus indicators are visible
- [ ] Headings follow logical hierarchy (h1, h2, h3...)
- [ ] Error messages are announced to screen readers
- [ ] Skip links are available
- [ ] Tables have proper structure (th, scope, caption)
- [ ] ARIA labels used for icon-only buttons

### 8.11 DaisyUI Accessibility

DaisyUI components are designed with accessibility in mind, but ensure:

```heex
<!-- Modal accessibility -->
<dialog id="my-modal" class="modal" aria-labelledby="modal-title">
  <div class="modal-box">
    <h2 id="modal-title"><%= gettext("Confirm Deletion") %></h2>
    <p><%= gettext("Are you sure?") %></p>
    <div class="modal-action">
      <button class="btn" onclick="document.getElementById('my-modal').close()">
        <%= gettext("Cancel") %>
      </button>
      <button class="btn btn-error" phx-click="confirm-delete">
        <%= gettext("Delete") %>
      </button>
    </div>
  </div>
</dialog>
```

---

## Conclusion

These guidelines are a living document and should evolve with our project and team. When in doubt, prioritize:

1. **Clarity over cleverness** - Write code that's easy to understand
2. **Consistency over perfection** - Follow established patterns
3. **Testing over hoping** - Write tests for confidence
4. **Documentation over memory** - Don't rely on tribal knowledge
5. **Communication over assumption** - Ask questions, discuss trade-offs
6. **Accessibility for all** - Build inclusive applications

Happy coding! 🚀

