Skip to content

Dashboard API

The role dashboard (/app), the phone home (/m) and the native app's Dashboard tab do not go through src/lib/api.ts. They call one database function straight through Supabase, dashboard_screen(), which answers every tile of the screen, the sidebar badges and the unread count in one request (PRF-002). Screen behaviour: Role dashboards.

The function is the boundary. It is SECURITY INVOKER: it holds no rights of its own and calls, as the signed-in person, the functions each tile always read. Every dash_* function checks the caller's permission itself; badge_counts() counts under the caller's row policies. anon cannot call it.

Calls

Call Who What it answers
rpc('dashboard_screen', { p_profile, p_scope, p_main }) any signed-in login (each tile checks its own permission) one dashboard tab, the badges and the unread count
rpc('get_journey_board', { p_days: 30 }) bookings.view (checked in the function; without it the board is empty) the journey board for the three journey widgets, sent beside the call above
GET /badge-counts → badge_counts() any signed-in login the sidebar badges on every page that is not the dashboard or the phone home
rpc('ntf_unread_count') any signed-in login the bell on the phone screens other than the home

The old per-tile calls — rpc('dash_approvals_inbox', …) and the rest — still work and are still used by other screens.

dashboard_screen

Arguments:

Name Values Default
p_profile a dashboard tab: leadership, finance-manager, chartered-accountant, accountant, cashier, operations, ticketing, visa, b2b, sales, auditor, hr-it; or phone for /m —
p_scope all or mine — the Mine / All switch, passed to the tiles that take it all
p_main true for the person's main tab: it also carries the open incidents true

Answer:

{
  "profile": "finance-manager",
  "scope": "all",
  "at": "2026-09-26T09:15:02.113Z",
  "tiles": {
    "approvals.inbox": { "count": 9, "items": [ … ], "why": { "rule": "ACC-020", "text": "…" } },
    "fin.cash-position": { "value": 1250000, "unit": "inr", … }
  },
  "errors": { "fin.payables-due": { "message": "…", "code": "XX000" } },
  "badges": { "approvals": 3, "finance": 5, "journals": 1, … },
  "unread": 2
}
  • tiles — keyed by widget id. Each value is exactly what that tile's function returns when the person calls it with the tile's arguments. A tile the person may not see is absent — its function refused them (42501).
  • errors — a tile whose function failed for another reason, with the database's message. The other tiles are unaffected.
  • badges — the badge_counts() answer, including its errors list.
  • unread — ntf_unread_count(); null if it failed.

Errors: no session → 42501; an unknown p_profile → 22023.

The native app calls it the same way, one tab at a time, with the profile it detected from the shared role profiles (src/lib/dashboardProfiles.ts); it reads tiles and errors and leaves badges and unread to its Home tab.

Two tiles carry one field more since 20261001180000: lead.collections-month (dash_collections_month) has today: { value, count, date } — receipts verified today, India time — and fin.receivables-overdue (dash_receivables_overdue) has ledger: { customers, partners } — the party-ledger total in secondary, split.

Which function and arguments feed each tile is dashboard_tile() in the same migration; which tiles each tab carries is dashboard_profile_tiles(). Both mirror the app's widget list, and src/test/dashboardScreen.drift.test.ts fails the build when they disagree.

Where to look

Concern Path
Functions and indexes supabase/migrations/20260930060000_the_dashboard_is_one_call.sql
Browser side src/components/dashboard/widgets/screen.ts, src/hooks/useJourney.ts (useDashboardJourneyBoard)
Tests supabase/tests/the_dashboard_is_one_call.sql, src/test/dashboardRoundTrips.test.tsx, src/test/dashboardScreen.drift.test.ts