All Docs
FeaturesMaking Tax DigitalUpdated February 25, 2026

Keeping the Dashboard Fast as Your Data Grows

Keeping the Dashboard Fast as Your Data Grows

Release v1.0.47 · Performance · Database

The Problem

Two of the most frequently hit queries in the application had no database indexes to back them:

  1. Recent Activity feed — The dashboard's recentActivity query fetches the latest entries from the audit_log table, filtered by orgId and sorted by createdAt DESC with a LIMIT. Without an index covering both columns, Postgres had to filter all rows for an organisation, sort the entire result set in memory, and then discard most of it to satisfy the LIMIT.

  2. Notification bell — On every render, the notification bell issues a query against the notifications table filtered by orgId, userId, and read status. Again, no index meant a sequential scan through every notification row matching the organisation.

Both of these queries run on every dashboard page load, making them high-frequency targets. Because both tables grow without bound — every action creates an audit log entry, every system event creates a notification — query cost was set to grow linearly with platform usage.

The Fix

Two composite indexes were added in src/db/schema.ts:

// Speeds up: SELECT ... FROM audit_log WHERE orgId = ? ORDER BY createdAt DESC LIMIT ?
index('audit_log_org_created_idx').on(auditLog.orgId, auditLog.createdAt)

// Speeds up: SELECT ... FROM notifications WHERE orgId = ? AND userId = ? AND read = ?
index('notifications_org_user_read_idx').on(notifications.orgId, notifications.userId, notifications.read)

Why Composite Indexes?

  • audit_log_org_created_idx (orgId, createdAt) — The leading column orgId narrows the scan to a single organisation's rows. The trailing createdAt means those rows are already sorted, so Postgres can satisfy the ORDER BY ... LIMIT with an index scan rather than a sort step.
  • notifications_org_user_read_idx (orgId, userId, read) — All three filter columns are covered in a single index. Postgres can resolve the entire WHERE clause from the index without touching the table heap for the majority of query executions.

Impact

QueryBeforeAfter
Dashboard recentActivityFull sort on filtered rowsIndex range scan + pre-sorted
Notification bell renderSequential scanIndex-only lookup
Estimated latency saving10–30ms per load

The saving is modest today but compounds over time — as audit logs and notifications accumulate across all landlord organisations on the platform, these indexes prevent what would otherwise become an increasingly expensive sequential scan.

No Action Required

This is a schema migration change. The indexes are applied automatically as part of the standard deployment process. There is no configuration change or action required by users or administrators.