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.
This commit is contained in:
Graham McIntire 2026-02-01 10:56:07 -06:00
parent ad753cf0c6
commit 45f8bc7459
No known key found for this signature in database

View file

@ -0,0 +1,169 @@
# 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
```elixir
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:**
```elixir
detected_timezone = get_session(socket, :detected_timezone) || "UTC"
assign(socket, :detected_timezone, detected_timezone)
```
**In handle_event("save"):**
```elixir
user_params = Map.put(user_params, "timezone", socket.assigns.detected_timezone)
```
### Schema Changes
```elixir
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