Summit is an open-source sales CRM. Its dashboard has no role check, and a rep and a manager still see different forecasts.
$184,000 or $1,083,900.
Those are the two weighted forecasts Summit computes from its seed data. Which one you see depends on who signed in, and the function that computes them doesn't know who that is.
Here is the whole calculation:
var open = deals.filter(function (x) { return OPEN_STAGES.indexOf(x.deal_stage) >= 0; });
var forecast = open.reduce(function (s, x) { return s + (x.amount || 0) * ((x.probability || 0) / 100); }, 0);
It takes the deals it was given, keeps the open ones, multiplies each amount by its probability and adds them up. There is no if (manager) anywhere in the Dashboard function. The two totals differ only in which rows the server handed back.
The server decides which rows
PolicyDef(role="tenant_user", default_access="none", rules=[
PolicyRule(entity="account", can_read=True),
PolicyRule(entity="contact", can_read=True),
PolicyRule(entity="lead", can_read=True, can_create=True, can_update=True,
filter_field="owner_username", filter_match="$user.name"),
PolicyRule(entity="deal", can_read=True, can_create=True, can_update=True,
filter_field="owner_username", filter_match="$user.name"),
PolicyRule(entity="activity", can_read=True, can_create=True, can_update=True,
filter_field="owner_username", filter_match="$user.name"),
])
A rep can read every account and contact in the company, and can't edit either. Leads, deals and activities are theirs alone. The manager role has rules on the same five entities with no filter.
The seed makes the split easy to see. There are 12 accounts and 20 contacts in the shared book, and 14 leads, 16 deals and 20 activities spread across four reps. The rep, Jordan Blake, owns 6 of the 16 deals and 4 of those are open. The manager sees all 16, and 11 are open. Weighted, that's the $184,000 and the $1,083,900.
On a fresh seed the rep's Accounts tab lists Helios Financial. The rep's Deals tab shows neither Helios deal, because both belong to a colleague, Sam Carter.
You can check the boundary without logging in
Nothing in this app is public. Five unauthenticated requests, one per object:
for s in account contact lead deal activity; do
curl -s -o /dev/null -w "$s %{http_code}\n" https://summit.supero.live/api/public/$s
done
On 5 November 2026 every one of them returned 403. Summit's PUBLIC_SCHEMAS is an empty list. Of the 19 apps in our repo, two declare that, and the other is a clinical-trials app running three sites.
The interface asks before it writes
One place does look at the role, and it's worth reading for what it is. Converting a lead is four writes for a manager and two for a rep. The code asks whether the current user may create an account and a contact before it tries, and a rep may create neither, so a rep's conversion writes only the deal and the lead.
That check isn't what protects anything. It stops the app attempting writes the policy doesn't grant. The protection is the policy, and the interface is just being polite about it.
What this costs you
A dashboard that adds up what it's given can't leak what it wasn't given. That's the benefit. The cost is that a rep's forecast is a rep's forecast. If a rep wants the team number, nothing in this design shows it to them, because the rows were never sent.
Open source. Code: supero-apps, under apps/crm/summit. Live: summit.supero.live, with a manager login and a rep login on the sign-in page.
More on Supero: supero.dev. Docs and the full write-up: docs.supero.dev.
Disclosure: I work on Supero, the platform this is built on.
Top comments (2)
Putting the filter on the server and keeping the dashboard function role-blind is the right design: one calculation, no if-manager branch to drift. The one thing I'd add is a label on the total, like "your deals" vs "team", so a rep never compares their $184,000 to a manager's number without knowing they're different scopes.
Fair point, the tile just says Weighted forecast and doesn't say whose deals it covers. I'd label it from what the server sends back, so the dashboard code stays role-blind, something like your deals vs all deals.