The shift comes from mixing date-only values with timestamps: BAW's Date type is a timestamp (a moment in time, serialised in UTC); the browser turns "2025-06-30" into midnight in the browser's time zone, the server (or a browser in another zone) shows that moment in its zone, and midnight in Sydney is the previous evening in Berlin - hence the day change. Rules:
- Decide per field whether it is a date or a moment. Birthdays, due dates and invoice dates are calendar dates; "submitted at" is a moment. Store calendar dates as strings (2025-06-30) or as Date values normalised to 12:00 UTC (the traditional workaround - a day change needs a 12-hour offset which no time zone has); store moments as Date and format them per viewer.
- Coaches: the UI Toolkit Date Picker has options for date-only handling (bind it to a String in ISO format, or use its "date only / UTC" style configuration); check the control's documentation of your toolkit version and use the same setting everywhere.
- Server scripts: never build dates with new Date(y, m, d) on the server (server zone) for calendar dates; parse and format explicitly with the Java classes and an explicit zone.
- Database: DATE columns receive the date part in the JVM's zone when you pass a Date; pass the ISO string (or a java.sql.Date built from the string) for calendar dates, and TIMESTAMP for moments; keep JVM (server) time zone and database session time zone identical and documented - on CP4BA the containers run in UTC.
- E-mails and documents: format moments in the recipient's zone explicitly.
// server script helpers
// calendar date string -> Date at 12:00 UTC (safe for storage in a Date variable)
function dateOnly(iso) { var p = iso.split("-"); return new Date(Date.UTC(+p[0], +p[1] - 1, +p[2], 12, 0, 0)); }
// Date -> calendar date string in a given zone
function toIso(d, zone) {
var f = new Packages.java.text.SimpleDateFormat("yyyy-MM-dd"); f.setTimeZone(Packages.java.util.TimeZone.getTimeZone(zone)); return String(f.format(d));
}
tw.local.invoiceDate = dateOnly(tw.local.invoiceDateText); // "2025-06-30"
tw.local.dueText = toIso(tw.local.dueDate, "Europe/Berlin");
// moment for the reader
var f = new Packages.java.text.SimpleDateFormat("dd.MM.yyyy HH:mm z"); f.setTimeZone(Packages.java.util.TimeZone.getTimeZone(tw.local.userZone));
tw.local.submittedText = String(f.format(tw.local.submittedAt));
// coach (client side): show a date-only value without zone shift
var iso = ${{InvoiceDate}}.getData(); // "2025-06-30" when bound to a String
var parts = iso.split("-"); var local = new Date(+parts[0], +parts[1]-1, +parts[2]); // midnight local, display onlyChecklist for an existing app with the problem: list every Date variable and classify it; switch calendar dates to String (or normalise to noon UTC) in the next snapshot with a migration script for running instances; align JVM and database zones; and add a test user in a far-away time zone to the regression tests.
References