Headline
Real-time multi-room rendering, now without polling.
v6.2.1 ships the third generation of our calendar engine, a long-awaited WebSocket subscription channel, and a tightened audit log. If you run more than three locations, you'll see the biggest difference on calendar load times — down 64% on the largest tenants we tested.
Section 1
Highlights
Three changes drive the bulk of this release. The calendar engine rewrite (CALENDAR-3, owners: Anya + Theo), the new WebSocket subscription channel that powers it, and a bigger-scope audit log that now records every change to organization_settings — not just the high-value fields.
We've held back this version one week longer than usual to bake the WebSocket backpressure path. Connection-storms during early staging caused the gateway to drop 2.1% of connections under load — that's gone now, but it took two iterations on the broker config to land.
Why we did this now
Two of our largest operators told us, separately, that calendar load was their single biggest paper-cut — “everything feels fast except the thing I look at all day.” We took the hint. Profiling showed three issues stacked together: unnecessary main-thread re-paints during scrolling, an over-eager polling fallback, and a layout pass we could pre-compute on the worker.
Section 2
Bookings
The calendar engine itself is a near-total rewrite. The old engine had grown from a good-enough month view to a multi-resource, multi-location, multi-timezone surface, and the seams were starting to show. The new engine is built as a worker-first virtualised renderer, with a deterministic layout pass that takes the same inputs and always produces the same DOM.
What changed
- Worker-first layout. The hot path moved off the main thread. Scrolling no longer races with hover state.
- Range-aware virtualisation. The renderer only paints the rooms and weeks you can see. The list of all rooms in a location is no longer a performance ceiling.
- WebSocket subscriptions. When a booking changes anywhere in the tenant, the calendar updates locally without a full refetch.
Subscription channel
Below is the minimum subscription you need to keep an external dashboard in sync. The channel auto-batches at 250ms and falls back to long-poll if the WebSocket handshake fails three times in a row.
import { createClient } from '@litehq/sdk'
const client = createClient({ apiKey: process.env.LITEHQ_API_KEY! })
const subscription = client.subscribe({
channel: 'bookings',
filter: { locationId: 'loc_north_quay' },
onMessage(event) {
if (event.kind === 'booking.upserted') refreshRow(event.bookingId)
if (event.kind === 'booking.deleted') removeRow(event.bookingId)
},
onError(err) {
// Auto-reconnects with exponential backoff.
console.error('lost subscription', err.code)
},
})
// Tidy on unmount.
return () => subscription.close()Section 3
Billing
Token-overage handling got a small but important upgrade. Previously, if a member moved tiers mid-cycle, any overage already accrued was billed at the old tier's rate. From v6.2.1, overage is re-priced retroactively to the new tier — which is what most operators told us they expected from day one.
Plain-English example
Sara's on the Flex tier (60 hr / month for $480). At day 20 she upgrades to Resident($720). Pre-v6.2.1, the 4 hours of overage she accrued on Flex would have been billed at the Flex overage rate ($16/hr = $64). Post-v6.2.1, that same 4 hours is billed at Resident's overage rate ($12/hr = $48) — saving Sara $16 and avoiding the “why did I get charged this?” ticket.
Stripe webhook
If you have a custom integration listening to customer.subscription.updated, no action needed — the re-price happens in our edge function before the next invoice is finalised, so your webhook payloads are unchanged. If you have a self-built billing dashboard reading our internal overage_charges table, see the Migration notes section.
Section 4
Performance
- Calendar engine:p95 first-paint dropped 64% on tenants with >10k bookings (was 980ms, now 350ms on a Chromebook over 4G).
- Booking detail panel: open time down from 320ms to 80ms.
- Member portal home: 18% reduction in JS bundle size after we moved the avatar generator behind a dynamic import.
- Edge function cold start: down 11% on the median (was 280ms, now 248ms) after we deferred the Sentry init for fast-path requests.
Before v6.2.1 (n=50) TTFB 140ms FCP 720ms LCP 980ms TBT 340ms CLS 0.04 After v6.2.1 (n=50) TTFB 136ms FCP 320ms ⟶ -55% LCP 350ms ⟶ -64% TBT 110ms ⟶ -68% CLS 0.02 ⟶ -50%
Section 5
Fixes & polish
- Recurring bookings on DST boundaries now produce the correct child events. The bug had been latent since v6.1.4; only triggered in Tasmania during the April-to-October transition.
- Empty location switcher — when an operator had access to no locations the dropdown crashed. Now shows a CTA to create the first location.
- Webhook signature error message was leaking the expected signature when payload verification failed. It now logs to Sentry with redaction and returns a stable 400 with no detail in the body.
- Resource list pagination had a single off-by-one row at the boundary when the result count was exactly a multiple of the page size.
- Invoice PDFs— line items with non-ASCII characters in the description (e.g. “Café space hire”) now render correctly. The fix was forcing the pdf-lib font subset to include Latin-1 + Latin-Ext.
- Member directory accent search— searching for “Léa” now matches a member stored as “Lea” (and vice versa). Diacritic-fold before the trigram match.
- AI assistant timestamp drift— assistant message timestamps now honour the tenant timezone, not the operator's browser.
Section 6
Migration notes
Two notes, both small. Neither requires action unless you have custom integrations directly against our internal tables.
1. Subscriptions channel — opt-in
The new WebSocket channel is opt-in. Nothing changes for you until you start a subscription. The polling fallback continues to work indefinitely, but we'll deprecate the documentation for it in v6.4.0.
litehq features enable subscriptions
─────────────
Subscriptions enabled for tenant 'north-quay'.
Polling fallback will remain available until v6.4.0.2. overage_charges table reshape
The internal overage_charges table grew a new nullable column repriced_to_tier. Existing rows are unaffected. If you read this table directly, add the column to your select list. The public /v1/invoices API is unchanged.
ALTER TABLE public.overage_charges ADD COLUMN repriced_to_tier text NULL; -- Old rows: repriced_to_tier IS NULL → use legacy tier rate. -- New rows: repriced_to_tier IS NOT NULL → reprice applied.
Audit log scope
If you read the audit log, you'll see more rows. Roughly 4x more, on average. That's expected — the trigger now records every field change on organization_settings, not just the previously-named high-value fields. Aggregations and dashboards that filter by field_name IN (...) should keep working unchanged.
Security note: Audit log insertions are best-effort isolated from the parent transaction. A failed audit row never blocks the user-facing operation. If you build alerting on this stream, tolerate gaps.
Found this useful?
Share the release notes with your team.
One short link per release, hosted publicly. No login wall.