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
-
Partner signs in
Partner, with a session cookie
- Source: The partner is a HubSpot contact Next: Access property allows sign in?.
- Decision: Access property allows sign in? Yes: Browser holds a session cookie, nothing else. No: Partner cannot sign in.
- Browser holds a session cookie, nothing else Next: Form asks for the company, use case and notes, in Referral submitted.
- Outcome, stops: Partner cannot sign in
-
Referral submitted
Partner, in about two minutes
- Form asks for the company, use case and notes Next: Deal in the referral pipeline, tied to the partner.
-
In HubSpot: Deal in the referral pipeline, tied to the partner Next: Short code set at write time.
- Short code set at write time Next: Cached list dropped, referral visible at once.
-
In Custom portals: Cached list dropped, referral visible at once Next: Team moves the deal the way it always has, in Deal worked.
-
Deal worked
Sector Growth team, in HubSpot
-
In HubSpot: Team moves the deal the way it always has Next: Stage history keeps a date for every stage.
-
In HubSpot: Stage history keeps a date for every stage Next: Portal reads stage history, cached 60 seconds, in Progress tracked.
-
-
Progress tracked
Partner, on the stage tracker
-
In Custom portals: Portal reads stage history, cached 60 seconds Next: Current stage pulses on the stepper.
- Current stage pulses on the stepper Next: Referral won or lost?.
- Decision: Referral won or lost? Won: The whole track lights up. Lost: Track stops at the stage where it died.
- Outcome, done: The whole track lights up
- Outcome, stops: Track stops at the stage where it died
-
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 concept | HubSpot representation | Why |
|---|---|---|
| Partner | Contact, with a portal access property | Access is granted and revoked where the team already works, and no partner needs a CRM seat |
| Referral | Deal in a referral pipeline | A referral is a deal we work, not a copy of one, so it sits in the pipeline with everything else |
| Referral progress | Stage history on that deal | History carries a date per stage, so the tracker needs no event table of its own |
| Partner company and tier | Contact properties | They describe the person rather than a separate record, and they decide what the portal shows |
| Resource library | Custom object, one list for everyone | A catalogue identical for every partner caches as a single entry and stays editable in the CRM |
| Notification | Custom object written by the team | The team sends it from the CRM. Whether it has been read stays out of the CRM |
| Read state and login audit | Portal database, not HubSpot | Neither belongs on a contact timeline, and the audit has to survive a rate limited CRM |
| Portal activity | Custom behavioural events | Logins, 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.
- 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 accreditations
Industry specializations
Partnerships
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.
Thanks, we will reply within one business day
Prash or Sean reads every one of these and comes back with a scope, or with the one question we need answered first.