Custom sales BI
Sales Dashboards
No more arguing about which number is right: 13 dashboards, one rule, and the API returns the same value the card shows.
- dashboards in production
- 13
- commits in 4 months
- 1,017
- test files
- 555
- SQL migrations
- 95
Figures measured from the repository.
Today's funnel
- Came in128
- Contacted96
- Qualified41
- Meeting booked23
Leads per hour
Today's ranking
| SDR | Attempts | Meetings |
|---|---|---|
| 1Ana R. | 52 | 8 |
| 2Bruno T. | 47 | 6 |
| 3Carla M. | 39 | 5 |
| 4Diego S. | 31 | 4 |
Context and problem
The sales team of a credit fintech made decisions on scattered numbers: BI dashboard replicas, side spreadsheets and one-off extracts. Every consumer applied its own rule, and the same metric showed different values on the floor TV, in the management meeting and in the e-mailed report.
The platform brings it all into one place: 13 dashboards for SDRs, closers, post-sale, credit and leadership, TV panels that run on their own, and an API that gives other systems exactly the same number shown on the card.
Environment constraints
- The CRM quota belongs to the account, not the token. More integrations do not add throughput; they just split the same budget. Everything that talks to the CRM must be controlled in one place.
- Treacherous sources. The CRM returns batch errors inside an HTTP 200 and reports totals that do not match composite filters; the data warehouse sometimes returns fewer rows without saying so.
- A wrong number is worse than a late one. A frozen metric with missing items stays wrong forever.
- Role-based access without friction. Company single sign-on, permissions per tab and per action, and TVs that work with nobody logged in.
Decisions that shaped the product
Architecture
The CRM, a data warehouse, spreadsheets and bureau databases feed staggered schedulers. Every CRM call goes through a client with a queue, priorities and quota control. Business rules are pure, tested functions, and the result becomes a daily snapshot in PostgreSQL. The same API serves the dashboards, the TVs and external consumers, who get exactly the number on the card.When the CRM returns a quota error (sometimes inside an HTTP 200), the breaker opens per account and method and background work yields to interactive requests. A partial read never becomes a number: the whole result is rejected and the TV keeps showing the last known good data.
Operating lessons
- Errors inside success. Batch sub-errors arrive in an HTTP 200 and do not trip a breaker on their own. The client inspects every response body before treating it as success, and never retries a quota error.
- Totals that lie. With composite filters, pagination continues until consecutive empty pages and deduplicates by id instead of trusting the reported total.
- Rows that vanish. Data-warehouse queries carry a sentinel count; if the row count does not match, the read fails loudly.
- Thundering herd on restart. Without staggering, every scheduler fired at once on each restart and drained the quota. Now they start offset.
- Reprocessing the past safely. Rebuilds run as a dry run by default, with a database lock and a volume guard before writing.
My role
I conceived and built the platform end to end (product, architecture, backend, frontend, integrations and operations) in a spec-driven workflow (59 specs) with an incident log in a “measure, don’t opine” format.
What the product delivers
- Single sign-on with OAuth2 + PKCE, two login modes switchable without logging anyone out, and sessions with an absolute cap.
- Permissions per tab and per action, mapped from directory groups and editable in the admin panel.
- TV panels via tokenized links, in a carousel, with no login.
- Excel export toggled per dashboard, and a portable report as a single HTML file that runs the same components offline.
- Daily snapshots, versioned goals and an audit trail.
What I would do differently
- Run CI on pull requests too. Today automatic validation happens only after merge.
- One source per rule from day one. Rules fixed in one file and forgotten in another caused rework until the identity tests existed.
- Plan the move out of in-memory state earlier. The single process was the right call, but every new in-memory cache raises the cost of scaling later.