Skip to content

Marketing Data Hub

A developer's marketing ran on two Excel workbooks and a BI tool with nothing clean to read. We turned them into one database where agencies, sales and marketing each edit only their part - and the BI tool reads the same truth.

Private2 workbooks → 33 tables · access enforced in the database · 12 BI views · spec to handover in one day

Design + Build - strategy, data modeling, security architecture, full-stack engineering · Next.js 16 · React 19 · Postgres with row-level security · BI over a read-only schema · Meta / Google Ads connectors

Commercial project - delivered under NDA, client name withheld.

The spreadsheet was the system

A real-estate developer in Dubai ran its whole marketing operation out of two Excel workbooks. Budget plan and actual per project, line item by month, years ahead, with formulas typed straight into cells. Agency spend by platform. A sales-plan model that worked backwards from square metres and average lot price to the leads, meetings and channel budgets each project needed. Brand search queries and direct visits. And a dashboard brief whose funnel, sales and refusal numbers had nowhere structured to live.

It worked the way spreadsheets work: until more than one party needs to touch it. External agencies had to report their spend without seeing the developer's budget. Sales needed the plan, not the money. The BI team needed a clean source to build dashboards on, and a workbook with formulas in the cells isn't one.

One line carries the whole design:

The database owns the permissions. The interface only reflects them. Who can read or write what is decided in Postgres - so a bug in a screen can't become a leak.

One matrix, enforced where the data lives

Four roles out of the box - superadmin, marketing, sales, agency - and a matrix that says, table by table, who reads and who writes. Marketing writes budgets; sales reads the sales plan and records funnel facts, but never sees the budget; an agency writes digital spend only for the projects it's assigned and sees nothing else.

The matrix is editable in the admin panel, and it isn't a UI setting. Every table carries row-level security generated by one function, so a new table gets the same guarantees as the original ones without anyone writing policies by hand. Blocking a user cuts off a session that's already open, not just the next login - and a test proves it against the real database.

Policies
Every policy is the conjunction of three checks: is the user active, does their role
have read/write on this table code, and - for project-scoped tables - can they see this
project. An agency sees only its assigned projects, or all of them if switched on.
Several physical tables share one permission code (dictionaries, brand queries + visits),
so the matrix stays readable and the audit log stays consistent. The same policy
generator is reused for tables users create themselves.

Excel habits, kept on purpose

Marketing people think in grids, so the budget is still a grid: year tabs, plan / actual / deviation, cell formulas, paste straight from Excel or Sheets, a running sum of the selection, export back to .xlsx. The difference is what's underneath - each cell is a row in a table with an audit trail, not a coordinate in a file.

One development was split into two phases that were often budgeted jointly. Rather than force a choice, the phases became separate projects joined by a group. In group mode the matrix shows each line item per phase plus a total; edit the total and it's distributed back across the phases by their current proportions, rounded so the parts always add up to the exact figure.

The existing workbooks were imported by an idempotent script - safe to re-run, with a dry run first - preserving the original cell formulas: nearly two thousand budget entries, digital spend, brand queries and sales targets, on day one.

Fields the client adds without a developer

The client can add a column, or a whole table, from the admin panel - text, money, percent, date, reference, or a formula written with plain labels like [Spend, AED] / [Leads]. These aren't JSON blobs: each becomes a real column in Postgres, and a formula becomes a generated column computed by the database itself, so the BI tool sees it too.

Formulas
Labels are translated to SQL and only then validated: forbidden-keyword scan, tokens
limited to the table's numeric columns plus a short whitelist (COALESCE, NULLIF, ROUND,
CASE...), and a dry parse against the table before the column is created. A new table
arrives with audit triggers, generated security policies and a row in the access matrix
set to "none" for every role but the superadmin - closed until someone decides otherwise.

Analytics in the app, BI outside it

Inside the console, the analytics section shows budget execution, spend by platform, the lead funnel against plan, source cohorts, cost per lead, sales against plan and refusal reasons - and each block appears only if the viewer's role can read every table behind it.

For the BI team there's a separate read-only schema of twelve flat views, not exposed through the app's API, read by a dedicated database login that can see that schema and nothing else. Dashboards and the app work from one source of truth instead of an export that went stale the day after it was made.

Spend arrives on its own: connectors for Meta Ads and Google Ads pull monthly spend overnight, and a CSV upload covers the property portals that have no API. Connector credentials are encrypted in the application and hidden from every API role at the column level.

Results

  • Two workbooks replaced by one database: 33 tables, every write audited
  • An access matrix the client edits themselves, enforced by row-level security rather than by screens
  • Agencies enter their own spend and see only their projects
  • User-defined fields and formula columns computed in the database and visible to BI
  • A read-only BI schema of 12 views on a dedicated login
  • Ad-platform connectors plus CSV, on a nightly schedule
  • Bilingual interface (RU/EN), 58 automated tests including row-level security run against the live database
  • From design spec to handover in a single working day
Stack
Next.js 16 (App Router, Server Actions) + React 19 + TypeScript · Postgres with
row-level security, generated columns and security-definer functions · next-intl ·
exceljs · AES-256-GCM for connector credentials · nightly cron · BI tool connected
directly to a read-only schema.