Custom portals backed by HubSpot: member and partner portals

Sector Growth

  • Professional services
  • Canada
  • Portal, live since 2026
Portals on one engine
3
Fork to live portal
5 days
Referral pipeline stages
7
Cached read window
60 seconds

A referral partner cannot see a deal that lives in our CRM, and we did not want a second system where referrals could go stale. So we built a portal: a partner signs in, submits a referral in about two minutes, then watches it move through the delivery pipeline on a stage tracker. Every referral is a native deal, worked in HubSpot by the same people who work every other deal. It runs on the portal engine behind a member portal demo we shipped a month earlier.

01 The Challenge

Partners send us work. The referral becomes a deal in our CRM, and from that moment the partner is outside the system that holds the answer. They ask by email, someone opens HubSpot, someone replies when they get to it. It is a small tax on every referral, and it falls hardest on the partners who send the most.

The obvious routes were closed. A CRM seat for each partner exposes every other record in the portal. A second database of referrals creates a second record of the same thing, and within a quarter the two disagree about which stage a deal is in. A gated content page can carry a resource library, but it cannot show one partner the stage history of their own referral and nobody else’s.

Two constraints shaped what was left. HubSpot rate limits are counted per app rather than per user, so every partner on the portal spends one shared budget. A portal that reads the CRM on each page view will exhaust it. And access has to be revocable by the delivery team in the tool they already have open, not in an admin screen inside the portal that nobody would think to check.

02 The Solution

Identity is a contact. A property on that contact decides whether the person can sign in at all, and the backend checks it at sign in rather than trusting a session issued earlier. Turning a partner off is a property change made in the CRM by whoever notices.

Submitting the form writes a deal into a referral pipeline and associates it to the partner’s contact. The partner never retypes their own details: identity comes from the session, so the form asks only about the company being referred, the use case and the notes. Each referral gets a short code at write time, which is how a partner and our team name the same record without either of them quoting a record id. The partner’s cached list is dropped on the way out, so the new referral is visible at once.

The tracker is the part partners actually use. We read the deal’s stage history rather than its current stage, which gives a date for every step. It renders as a stepper: the current stage pulses, a won referral lights the whole track, a lost one stops at the stage where it died. Nobody on our side feeds it. The team moves the deal in HubSpot the way they always have, and the portal shows the change on its next read.

Two more object types carry the rest. Resources are a custom object, published by the team and identical for every partner, so the catalogue is one cached entry shared by the whole portal rather than one read per person. Notifications are a custom object as well, but whether a partner has read one is not CRM data, so read state sits in the portal’s own database next to the login audit.

03 The Outcome

The partner portal went from a fork to a live deployment reading real records in five days in August 2026. Most of it was already built: the security model, the session handling and the cache layer came across unchanged. The work was the object model, the screens and the brand, which is the part that genuinely differs each time.

Three portals now sit on the same engine. It began as a borrower portal, was reshaped into a member portal demo in July, and became our own partner referral portal in August.

Running one ourselves is the cheapest way to find what is wrong with it. A partner checking on a referral opens the tracker, and the answer is at most a minute out of date. The member portal demo does the same job in a sales conversation. A prospect sees a portal reading and writing a real CRM on synthetic data, rather than a screenshot of one.

How the lifecycle runs now

  1. Partner signs in

    Partner, with a session cookie

    1. Source: The partner is a HubSpot contact Next: Access property allows sign in?.
    2. Decision: Access property allows sign in? Yes: Browser holds a session cookie, nothing else. No: Partner cannot sign in.
    3. Browser holds a session cookie, nothing else Next: Form asks for the company, use case and notes, in Referral submitted.
    4. Outcome, stops: Partner cannot sign in
  2. Referral submitted

    Partner, in about two minutes

    1. Form asks for the company, use case and notes Next: Deal in the referral pipeline, tied to the partner.
    2. In HubSpot: Deal in the referral pipeline, tied to the partner Next: Short code set at write time.
    3. Short code set at write time Next: Cached list dropped, referral visible at once.
    4. In Custom portals: Cached list dropped, referral visible at once Next: Team moves the deal the way it always has, in Deal worked.
  3. Deal worked

    Sector Growth team, in HubSpot

    1. In HubSpot: Team moves the deal the way it always has Next: Stage history keeps a date for every stage.
    2. In HubSpot: Stage history keeps a date for every stage Next: Portal reads stage history, cached 60 seconds, in Progress tracked.
  4. Progress tracked

    Partner, on the stage tracker

    1. In Custom portals: Portal reads stage history, cached 60 seconds Next: Current stage pulses on the stepper.
    2. Current stage pulses on the stepper Next: Referral won or lost?.
    3. Decision: Referral won or lost? Won: The whole track lights up. Lost: Track stops at the stage where it died.
    4. Outcome, done: The whole track lights up
    5. Outcome, stops: Track stops at the stage where it died
The map follows one referral from a partner's sign in to the stage tracker, where it ends won or lost. Our team works it as a deal in HubSpot, the only record of the referral, and the portal reads it.

04 Stack

Cloudflare Pages serves the app and Pages Functions run the API. A KV namespace holds the cache. A D1 database holds what the CRM should not: portal accounts, the login audit and notification read state. The frontend is React and TypeScript. The HubSpot token exists only in the function environment, never in the browser, and every CRM call goes through one server side client with backoff on rate limit and server errors.

Operations is a CRM job rather than a deployment job. Publishing a resource, sending a notification, moving a stage and revoking access are all record edits made by the team in HubSpot. The only reason to deploy is a change to the portal itself.

There is also a fixture mode. With no token set, the API serves an equivalent world from the local database under the same property names, so every screen and the whole submit and track flow runs offline.

05 Architecture and data model

The browser holds a session cookie and nothing else. Every call goes to a server side function, which checks the access property on the contact, then reads HubSpot through the cache. A cache miss is the only thing that reaches the CRM, which is what stops one shared rate limit budget being spent by whoever signs in first. A submitted referral is written as a deal and associated to the partner’s contact. Edits the team makes in HubSpot travel back out along the same path, so the portal owns no write path for a stage change.

Each row below exists because the CRM already had a place for the thing, or because it deliberately does not:

Portal conceptHubSpot representationWhy
PartnerContact, with a portal access propertyAccess is granted and revoked where the team already works, and no partner needs a CRM seat
ReferralDeal in a referral pipelineA referral is a deal we work, not a copy of one, so it sits in the pipeline with everything else
Referral progressStage history on that dealHistory carries a date per stage, so the tracker needs no event table of its own
Partner company and tierContact propertiesThey describe the person rather than a separate record, and they decide what the portal shows
Resource libraryCustom object, one list for everyoneA catalogue identical for every partner caches as a single entry and stays editable in the CRM
NotificationCustom object written by the teamThe team sends it from the CRM. Whether it has been read stays out of the CRM
Read state and login auditPortal database, not HubSpotNeither belongs on a contact timeline, and the audit has to survive a rate limited CRM
Portal activityCustom behavioural eventsLogins, record views and submissions land on the contact timeline with the rest of the person’s history

Every property the portal adds carries a portal prefix, so a portal field is obvious in a property list that marketers also use.

The same rows reshape for a different domain. In the member portal demo the referral becomes an event plus a registration object joining it to the member. Invoices are deals in their own pipeline with a pay link out to a payment provider, and member messages are a ticket per member with each message a note. Documents sit beside the resource library rather than replacing it. The library is published by the team and identical for everyone, while a document is uploaded by one member and carries a review state and a private file. The mapping changes with the domain, and the engine under it stays the same.

A partner browser holds only a session cookie. Server side functions check an access property on the contact, then read HubSpot through a cache with sixty second entries, so a miss is the only call that reaches the CRM. A submitted referral is written as a deal in the referral pipeline and associated to the partner. Stage moves made by the team in HubSpot appear on the next read.
Hosting
Cloudflare Pages
Runtime
React single page app, server side functions, a KV cache and a D1 database
Cadence
On demand per request, with HubSpot reads cached for 60 seconds
Pattern
Portal over the HubSpot API

What we would do again, and differently

What we would do again

  • Keep HubSpot as the only record of a referral. Nothing has to be reconciled later, and the team works the deal the way they work every deal.
  • Read stage history rather than the current stage. The per stage dates come with it, and the tracker needs no event table to keep in step.

What we would do differently

  • Package the engine instead of forking it. A fix made in the third portal does not reach the first two, and three copies of the same access check is two more than anyone wants to maintain.
  • Let HubSpot tell the portal when a stage moves, rather than waiting for a cache entry to expire. Sixty seconds is a short wait, but it is one we chose for simplicity rather than one the CRM forced on us.

Systems in this build

Partnerships and accreditations

  • HubSpot Elite Solutions Partner
  • HubSpot Custom Integration Accreditation
  • HubSpot CRM Data Migration Accreditation
  • HubSpot Solutions Architecture Design Accreditation

HubSpot accreditations

  • HubSpot Onboarding Accreditation
  • HubSpot Academy Accreditations

Industry specializations

  • HubSpot Healthcare Industry Specialist
  • HubSpot Financial Services Industry Specialist
  • HubSpot Manufacturing Industry Specialist
  • HubSpot Quote to Cash Capability

Partnerships

  • Google Partner
  • Anthropic Solutions Partner

Have a system in the same shape?

Send the systems, the direction and the record volume. We reply with a scope and an effort band within one business day.

Press Enter or comma to add another system.

Add more detail (optional)

Optional. A sentence or two about the workflow is enough.

We use these answers to scope the work and for nothing else. Privacy policy.

Prefer to talk it through first? Book a technical scoping call

Talk to the people who build these

  • Nijat
  • Sean
  • Prash

Book a scoping call