When dealing with most point-in-time values (created at, updated at, expires at, scheduled for, etc), it's not necessary to store the time zone of the user.
The idea is to realize that the time zone is just a presentation/communication detail of an absolute concept: instant in time. Although, time zones are a very important thing to consider, neither the application nor the user actually care about time zone all that much.
- The server. Needs an absolute way to store and compare instants-in-time reliably.
- The user. Needs to see instants-in-time, always presented in his/her time zone.
We only need time zone for translating the instant-in-time as the server stores it, to a version the user expects. You can almost think of time zone as just a formatting detail. So, the solution:
- The back-end only deals with datetimes in a single time zone: GMT.
- The front-end only deals with datetimes in a single time zone: Local time (user)
With that in mind, the way to take care of it is to push all time zone calculations to the border between front-end and back-end: The code directly talking to the web browser (the controller layer in most frameworks). Yes, I know. Technically it is still part of the backend, but let's forget that detail for now :/ It is the layer mostly concerned with the communication between the two.
This keeps both the server and front end simple, as they only ever have to deal with one time zone. And it puts the time-zone code in one place: the controller layer.
Here is how the process should go:
After that, all requests to the server should follow this flow:
- The browser makes request to the server (includes tz-offset cookie automatically). Any datetime in the request is sent in the end-user's timezone (the one from the cookie), but the time zone is not included in the datetime (it's already on the cookie).
- The server uses the time zone offset from the cookie to transform any datetimes found in the request to GMT.
- From that point on, it's GMT all the way to the DB.
- When the server obtains data from the database, any datetimes retrieved are in GMT.
- As the server prepares the HTTP response, it uses the tz-offset value from the cookie to transform the datetimes in the response to the correct time zone for the user.
As an example, in the Play Framework when using Scala you typically create an implicit Format[DateTime] object to perform the serialization of a type to JSON (play-json using typeclasses). You can have this method take as an implicit parameter the actual HTTP request, from which it can read the cookie (or header/queryparam) with the time zone offset and serialize/parse the datetime accordingly.
There are times where you DO need to keep the time zone and deal with it on the backend code AND possibly front end code: Broadcast times, office hours, airfare times, etc. The preceding advice does not apply to these types of times and you'll have to come up with an explicit way to handle them.
- Set all servers (OS, Database, Web Server, etc) to GMT
- In the database, store every datetime representing a point-in-time in GMT.
- Write all your backend code assuming GMT.
- Include end-user's tz-offset on all requests.
- Don't burden the browser with time zone calculations. Once it reaches the browser, all times should be in the user's time zone already.
- Recognize date/times that are not absolute instants-in-time and treat them different (office hours, broadcast times, etc). For these, you will need to keep track of the time zones in the database and your backend code will have to deal them explicitly.