towerops/docs/plans/2026-02-01-cloudflare-timezone-detection-design.md
Graham McIntire 45f8bc7459
Add design for Cloudflare timezone detection on signup
Documents approach to automatically populate user timezone from cf-timezone
header during registration, with UTC fallback when header is not present.
2026-02-01 10:56:07 -06:00

5.4 KiB

Cloudflare Timezone Detection on Signup

Date: 2026-02-01 Status: Approved Author: Claude Code

Overview

Automatically populate the user's timezone during registration by reading Cloudflare's cf-timezone header. This provides a better default than "UTC" for users behind Cloudflare's network.

Problem Statement

Currently, all new users get timezone: "UTC" as their default, even though Cloudflare provides accurate timezone detection via the cf-timezone header. Users must manually update their timezone in settings after registration.

Proposed Solution

Capture the cf-timezone header during the registration flow and use it to set the user's timezone field automatically.

Architecture

Component Design

Browser Request → Plug (CaptureTimezone) → Session[:detected_timezone]
                                                    ↓
                              LiveView mount reads session
                                                    ↓
                              Form submit merges timezone into params
                                                    ↓
                              Accounts.register_user_with_organization
                                                    ↓
                              User created with correct timezone

Files to Modify

  1. NEW: lib/towerops_web/plugs/capture_timezone.ex

    • Plug that extracts cf-timezone header
    • Stores in session as :detected_timezone
    • Fallback: "UTC" if header missing
  2. MODIFY: lib/towerops_web/endpoint.ex

    • Add CaptureTimezone plug to browser pipeline
    • Runs on all browser requests (minimal overhead)
  3. MODIFY: lib/towerops_web/live/user_registration_live.ex

    • Read :detected_timezone from session in mount/3
    • Store in socket assigns
    • Merge into user params in handle_event("save")
  4. MODIFY: lib/towerops/accounts/user.ex

    • Add :timezone to registration_changeset/3 cast fields
    • No validation needed (internal value, already validated by Cloudflare)

Implementation Details

Plug Implementation

defmodule ToweropsWeb.Plugs.CaptureTimezone do
  @moduledoc """
  Captures the cf-timezone header from Cloudflare and stores it in session.
  Used to pre-populate timezone during user registration.
  """
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    timezone =
      case get_req_header(conn, "cf-timezone") do
        [tz] when is_binary(tz) -> tz
        _ -> "UTC"
      end

    put_session(conn, :detected_timezone, timezone)
  end
end

LiveView Changes

In mount/3:

detected_timezone = get_session(socket, :detected_timezone) || "UTC"
assign(socket, :detected_timezone, detected_timezone)

In handle_event("save"):

user_params = Map.put(user_params, "timezone", socket.assigns.detected_timezone)

Schema Changes

def registration_changeset(user, attrs, opts \\ []) do
  user
  |> cast(attrs, [:email, :password, :timezone, :privacy_policy_consent, :terms_of_service_consent])
  # ... rest unchanged
end

Behavior

Header Present

  • Cloudflare sends: cf-timezone: America/Chicago
  • User registered with: timezone: "America/Chicago"

Header Missing (Development/Non-Cloudflare)

  • No header detected
  • User registered with: timezone: "UTC" (fallback)

Cloudflare Timezone Format

  • Uses IANA timezone database format (e.g., "America/Chicago", "Europe/London")
  • Compatible with Elixir's Timex and Calendar libraries
  • No conversion needed

Testing Strategy

  1. Unit Tests: Plug extracts header correctly, handles missing header
  2. Integration Tests: LiveView receives timezone, registration includes timezone
  3. Manual Testing:
    • Development (no header): Should default to "UTC"
    • Production (with header): Should use Cloudflare's detected timezone

Edge Cases

  • Multiple timezones: Cloudflare sends single value, no issue
  • Invalid timezone: Trust Cloudflare's validation (they use IANA database)
  • User changes timezone later: Users can still update in settings
  • Invitation flow: Works identically for both invitation and normal registration

Performance Impact

  • Plug overhead: 1 header lookup + 1 session write per request (~negligible)
  • Session size: +1 small string value (~15-30 bytes)
  • LiveView mount: 1 session read (already happening)

Security Considerations

  • Header is read-only from Cloudflare (users cannot spoof it easily)
  • Timezone is non-sensitive data (no privacy concerns)
  • No validation needed (trust Cloudflare's IANA-compliant values)

Rollout Plan

  1. Deploy plug and schema changes
  2. Verify in production logs that cf-timezone header is present
  3. Monitor new user registrations for correct timezone population
  4. No user communication needed (transparent improvement)

Future Enhancements

  • Display detected timezone in registration form (optional)
  • Allow users to override detected timezone before submitting (optional)
  • Use timezone for email scheduling and notification preferences

Alternatives Considered

  1. JavaScript-based detection: Less reliable, requires client-side code
  2. GeoIP-based timezone: Less accurate, requires additional lookups
  3. Ask user during signup: Additional friction, worse UX

References

  • Cloudflare cf-timezone header documentation
  • IANA Timezone Database
  • Phoenix LiveView session handling