Raphael Napoleon Atikpo
Project 2 of 10 — Marketing Operations & Automation portfolio

A marketing database that can prove it's allowed to be used.

A consent-capture, storage, and suppression system for a fictional B2B SaaS company — cookie consent gated with Google Consent Mode v2, layered email consent captured in HubSpot as five distinct signals instead of one flag, and suppression logic verified against a live, shared HubSpot portal rather than a clean sandbox. Built entirely on free-tier tools.

Cookie Banner→ GTM→ Consent Mode v2→ GA4 / HubSpot Forms→ HubSpot CRM→ Segments→ Suppression enforced
The actual pipeline this page describes — every arrow below is backed by a real screenshot, not a diagram of an intention.
6
custom consent properties
5 / 5
test scenarios matched prediction
4
segments enforcing suppression
$0
total budget

The problem

Lumen Metrics, a small e-commerce analytics SaaS company, had spent eight months growing its contact database with no consent discipline: every demo request, ebook download, and imported conference lead was emailed the same way, with no record of who had actually agreed to marketing contact — and no cookie consent on the website at all, with analytics tracking firing unconditionally on every page load. As the company's first Marketing Operations hire, the brief was to answer one question with evidence, not assumptions: can this database actually be used legally and safely for marketing?

What I set out to build

How the pieces fit together

The system runs as two deliberately separate layers, meeting only at the moment a real send happens.

Track A — Platform-enforced

HubSpot's native subscription status and legal-basis property. This layer is unconditional — no custom logic built on top of it can override it, which is exactly why it's the layer this project's suppression segments were checked against.

Track B — Custom-built

Six purpose-built properties and four rule-based segments, engineered specifically to represent what HubSpot's free tier has no native way to model — category-level marketing consent chief among them.

Consent is modeled as five distinct signals rather than one flag:

Layer 1
Cookie/tracking consent

Can this anonymous browser be tracked — held in localStorage, signaled to GTM's dataLayer.

Layer 2
Marketing consent, by category

Did this known contact agree to Product Updates, Promotions, or Sales Outreach — specifically, not generically.

Layer 3
Legal basis

Why we may process this person's data at all, which can differ from why we may email them.

Layer 4
Subscription status

HubSpot's native, unconditional enforcement layer — the layer that actually blocks a send.

Layer 5
Data subject rights

Has this person asked to see, correct, or delete their data — captured, not assumed.

Stack

GitHub Pages Google Tag Manager Google Analytics 4 Google Consent Mode v2 HubSpot CRM (Free) HubSpot Forms

What's actually running

Cookie consent banner

Hand-built in vanilla JavaScript rather than a packaged CMP. Stores the visitor's choice in localStorage, re-announces it to the dataLayer on every page load, and stays reopenable anytime via a persistent "Cookie Settings" control in the footer.

The live cookie consent banner on the Lumen Metrics site, showing Necessary Only and Accept Analytics options
The banner live on lumenmetrics.dev — Necessary Only or Accept Analytics, with no pre-checked assumption either way.

Google Tag Manager & Consent Mode v2

A Consent Initialization tag sets Google Consent Mode v2 defaults to denied before anything else runs. A custom trigger, reading the banner's own consent event through a Data Layer Variable, is the only trigger available to the GA4 tag — meaning GA4 is structurally incapable of firing before consent, not merely delayed.

GA4 Realtime report showing 0 active users before analytics consent was granted
Before consent: GA4 Realtime shows 0 active users, despite the page having fully loaded.
GA4 Realtime report showing 1 active user after analytics consent was granted
Seconds after clicking "Accept Analytics": 1 active user appears — tracking begins exactly when consent is given, not before.

HubSpot CRM data model

Six custom properties in a dedicated Consent & Privacy group, modeling tracking consent, category-level marketing consent, and suppression reason as separate fields — deliberately not collapsed into one. Getting there required first auditing and archiving four unused properties from an earlier portfolio project, after hitting HubSpot Free's 10-property, portal-wide cap.

HubSpot Properties settings showing the Consent and Privacy property group with six custom properties, fill rates, and creator
The Consent & Privacy property group as configured — six purpose-built fields, each with its own fill rate, none of them collapsed into a single flag.

HubSpot Forms

Three forms — Demo Request, Newsletter & Ebook, Manage Preferences — each unbundling "submit this form" from "agree to marketing" as two separate, optional actions.

The live Demo Request form showing unbundled consent language and unchecked marketing category checkboxes
The Demo Request form, live: requesting a demo and opting into marketing are visibly, explicitly two different actions — none of the three category boxes are pre-checked.

Preference management & real unsubscribe enforcement

A native HubSpot GDPR consent element on the Manage Preferences form ties directly to real subscription status, layered on top of the custom audit property rather than replacing it. Verified with an actual send-and-unsubscribe test, not a simulated one — HubSpot's own reporting captured the unsubscribe reason natively.

HubSpot report titled Unsubscribe Reason showing a donut chart at 100 percent for one contact who selected I'm no longer interested
A real unsubscribe event, captured and reported natively by HubSpot — proof the preference layer is wired to the platform's actual enforcement mechanism, not just a form field.

Suppression segments

One segment, Suppressed - Consent Not Valid, built as the single source of truth; two "Valid Marketing Contacts" segments reference its membership rather than re-implementing the same rules — a redesign made mid-build once HubSpot's actual segment-membership filter made the cleaner pattern possible.

Proving it works, not just building it

QA happened in two layers: five test contacts engineered to sit right on the boundary of the rules rather than safely inside them, and three genuine free-tier platform constraints hit and resolved mid-build — all tested against a live, shared HubSpot portal already carrying contacts and properties from an earlier project, not a clean sandbox.

HubSpot segment builder showing the four OR-grouped suppression conditions and a resulting list of 15 contacts, 93.75 percent of the database
The Suppressed - Consent Not Valid segment, built exactly as specified — four conditions joined by OR, matching 15 of 16 contacts in the portal.

An honest read on that 93.75%

That figure looks alarming out of context, and it's worth explaining rather than hiding: this portal is shared with an earlier portfolio project, so most of that 15 is sample contacts and legacy leads that simply never had Lumen Metrics' consent properties set at all — which correctly matches the "Marketing Consent Categories is unknown" condition. It's the rule working exactly as designed on data it was never scoped to, which is itself a real lesson: a segment built to run in a production portal would need an additional filter scoping it to this project's own contacts, and I've noted that in the limitations below rather than quietly cropping the screenshot to hide it.

  1. 01Portal-wide property cap — HubSpot Free's 10-custom-property limit blocked the sixth property mid-build; traced to four unused properties from an earlier project and archived them to free a slot. Fixed, verified.
  2. 02Mid-build product rename — HubSpot renamed "Lists" to "Segments" across the entire product; every segment-related step had to be re-verified against the live interface rather than trusted from documentation written days earlier.
  3. 03Silent archive failure — attempting to archive an old property failed with no clear cause at first, blocked by an existing asset still referencing it; resolved by locating and clearing that reference before archiving.
  4. 04Segment scope check — reviewing the Valid Marketing Contacts segment surfaced one contact needing manual exclusion via HubSpot's individual-contact exclusion control, alongside the rule-based logic — a reminder that automated segmentation still benefits from a human spot-check before a real send.

What's actually verifiable

Measured: the "Valid Marketing Contacts - Product Updates & Tips" segment, filtered by category consent and membership exclusion from the Suppressed segment, resolved to exactly the two contacts predicted at design time — out of a shared portal containing sixteen. Consent Mode v2 was confirmed live: zero analytics activity before consent, tracking beginning within seconds of it being granted.

Full configuration, all documentation, and the complete test log are in the case study and documentation in the project repo.

HubSpot segment showing exactly two contacts, Sarah Chen and David Osei, matching the predicted eligible contacts
The eligibility proof: of everyone in the portal, exactly two contacts — Sarah Chen and David Osei — resolve into this segment, matching the design's prediction exactly.
On business impact: this is demonstrated capability, not measured business impact — there's no real company or customer base behind Lumen Metrics. What's genuinely demonstrated is a system that, applied to a real company's database, would keep marketing sends restricted to people who've actually and specifically agreed to them, and make that restriction provable on demand rather than assumed.

What I'd tell a hiring manager directly

Challenges

The most instructive snags weren't compliance questions at all — they were free-tier platform mechanics: a property cap that had nothing to do with this project specifically, and a mid-build product rename that made previously-correct instructions wrong within the same week. Working around both, rather than upgrading past them, is most of what this project actually demonstrates.

Limitations

What I'd build next