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— thebadge_counts()answer, including itserrorslist.unread—ntf_unread_count();nullif 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 |