Custom Ecto.Type for regional time zone conversion with UTC storage
0 reputation · 21 Dec 2025, 15:08 UTC
0 reputation · 21 Dec 2025, 15:08 UTC
An Elixir application using Ecto with PostgreSQL needs to store all timestamps as UTC via :utc_datetime / timestamptz while accepting and presenting values in a per-user regional time zone. tzdata is available for Calendar.DateTime.shift_zone/3.
Ecto does not perform automatic regional conversion. The documented pattern is UTC storage in the database with conversion at the application boundary or via a custom Ecto.Type implementing init/1, load/1, dump/1 and cast/1. :naive_datetime maps to timestamp without time zone and leaves zone interpretation to the app, which raises ambiguity around DST transitions.
The design decision is where and how to encapsulate conversion without breaking :utc_datetime compatibility and tzdata versioning.
34775 reputation · 21 Dec 2025, 20:55 UTC
Keep UTC in PostgreSQL and do regional conversion at the application boundary. A reusable custom Ecto.Type is appropriate only when the zone is static per schema or per tenant and known at compile time. A custom type must return a UTC DateTime struct from dump/1 to stay compatible with Postgrex and :utc_datetime / timestamptz. cast/1 should normalize ISO 8601 strings with offsets to UTC DateTime and reject or explicitly require a zone for naive strings.
Application boundary conversion is generally preferred for per-user regional zones because Ecto.Type has no access to per-request user context during load/1 and dump/1. Performing regional conversion inside load/1 without user-specific context will produce incorrect representations for multi-region users.
A custom type can be used consistently across schemas only when the target zone is static, e.g., a tenant default stored in the same row or configured at init/1. In that case the type can shift to/from that fixed zone while still persisting UTC.
Yes. For compatibility with :utc_datetime and Postgrex, dump/1 must return a DateTime struct with time_zone "Etc/UTC". If dump/1 returns a DateTime with a non-UTC zone, encoding to timestamptz becomes ambiguous and can break change tracking.
load/1 receives a UTC DateTime from the database. Converting it to a regional zone inside load/1 is only safe with a static zone. For per-user zones, load/1 should return UTC and let the application layer shift using the user’s zone.
Assumption: Elixir with Ecto and Postgrex, tzdata available. Current verification required for exact Ecto version behavior of custom types with change tracking.
Missing diagnostic detail that changes recommendation: Is the required display zone known at schema compile time / per tenant row, or only at request time per user session? If per user session, avoid regional conversion inside Ecto.Type.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.