Premium Membership Platform — User Guide
Premium Membership Platform — Proof of Concept | 9 Modules · 250 Tests · 3 Portals
Get oriented quickly — open these three URLs and follow the walkthrough to see the full platform in action.
https://specialoffer-demo.fly.dev/adminhttps://specialoffer-demo.fly.dev/https://specialoffer-demo.fly.dev/partnerWhen no member session is active (fresh browser or after clicking "Switch Member"), the portal shows the Member Directory — domain-grouped cards for all pre-seeded members. Each card displays the member's name, email, domain, tier badge, specialty, and profile completeness bar. Click any card to set a session and load their personalized dashboard with domain theming.
At the top, demo invitation links are displayed for walking through the "New Member" signup flow from scratch.
The "Switch Member" link in the navigation clears the current session and returns to the directory.
Both members come pre-seeded with behavioral profiles, career moments, life moments, and tier status to demonstrate the full platform without manual setup.
Follow these 8 steps for a complete platform walkthrough:
/go/{offerId}?m={memberId}, recording a PARTNER_VISIT signal, then opens the mock UpToDate page with embedded tracking pixel.
The member-facing portal at https://specialoffer-demo.fly.dev/ provides the full professional membership experience — from discovery through onboarding, offer redemption, partner visits, and personalized content.
The directory is the landing page when no session is active. Members are grouped by professional domain, each with distinctive accent colors:
Each member card shows: name, email, domain badge, tier badge, specialty, and a profile completeness progress bar. Clicking a card sets a session cookie and navigates to their domain-themed dashboard.
Demo invitation links at the top allow walking through the new member signup flow from scratch.
Members arrive via an invitation link (emailed in production, shown as demo links here). The landing page displays the invitation details and a prominent "Accept Invitation" button. Accepting triggers the InvitationAccepted event and shows the application form.
The application collects professional details specific to the member's domain:
Domain-specific fields are dynamically loaded from the Domain Configuration module. The form also captures geography (city, state, country) for future geographic targeting.
After submission, the member sees a status screen showing their application state. Non-ACTIVE members are gated from the dashboard — they see only the status screen until approved. Status flow: PENDING → APPROVED → ACTIVE. Admins approve members from the Admin UI, which triggers the ApplicationApproved and MemberActivated events.
The dashboard is the member's home after login, featuring:
The dashboard applies the member's domain theme — teal for Medical, navy for Legal, charcoal+gold for HNW. Offer cards show category, partner name, discount/benefit, and the affinity-based "Recommended" badge.
Clicking an offer card opens the detail page with:
Viewing the offer records an OFFER_CLICK signal. Dwelling 5+ seconds records an OFFER_DWELL signal. Redeeming records OFFER_REDEEMED. All signals feed back into the affinity scoring engine (when consent is granted).
The partner visit flow tracks outbound member activity through a multi-step redirect:
/go/{offerId}?m=/api/track/pixel.gifThe 5 configured mock partner pages, each with brand-appropriate styling:
| Partner | Domain | Page URL | Brand Style |
|---|---|---|---|
| Stryker | Medical | /partner-demo/stryker |
Deep blue (#003865) |
| UpToDate | Medical | /partner-demo/uptodate |
Bright blue (#0077c0) |
| J.P. Morgan | HNW | /partner-demo/jpmorgan |
Dark navy (#0a3d62) |
| Westlaw | Legal | /partner-demo/westlaw |
Red (#c41230) |
| NetJets | HNW | /partner-demo/netjets |
Black + gold |
Each mock page shows a "SpecialOffer Exclusive" badge, the offer details, and a "View Tracking Integration Code" disclosure that reveals the actual pixel HTML. A "Return to SpecialOffer" link navigates back to the member dashboard.
The profile page has three collapsible sections and a career timeline:
Career Timeline: A chronological view of career moments (domain-specific: board certification, partnership, fellowship, etc.) and life moments (platform-wide: marriage, home purchase, retirement planning, etc.). Members can record new career events directly from the timeline.
A persistent consent banner appears on every page until the member makes a choice. The banner is non-blocking — the member can continue using the portal while it's displayed.
Three options on the banner:
The Preferences Panel has three toggles:
Consent is stored server-side on the SignalCollection aggregate. Signals are only recorded when consent is granted (enforced at the domain layer). Revoking consent stops signal recording immediately.
The “Privacy” link in the member nav bar lets members access these preferences at any time — not just during the initial prompt. Members can also withdraw all consent via a red button in the preferences panel, which anonymizes their behavioral data and resets personalization. See Section 5.2 for full details.
When a member has granted consent and has behavioral signal history, offers are ranked by category affinity score (0-100 scale). The ranking is subtle by design:
SpecialOffer uses a tiered membership model with four levels:
| Tier | Access Level | How to Reach |
|---|---|---|
| Basic | Standard offers | Default on activation |
| Professional | Standard + professional offers | Subscription upgrade |
| Premium | All non-exclusive offers | Subscription upgrade |
| Founders Club | All offers including exclusives | Invitation only |
Tier-locked offers display a lock icon on the dashboard. Founders Club exclusives (e.g., Figure 1 Consult Network, Lexis+ AI Beta) are only visible to Founders Club members. Tier changes produce domain events that trigger cross-module projections.
All three portals are fully responsive. The member portal adapts to mobile viewports with stacked layouts, collapsed navigation, and touch-friendly controls. Offer cards stack vertically, the consent banner adjusts to screen width, and tables scroll horizontally when needed.
The Admin UI at https://specialoffer-demo.fly.dev/admin is the system-wide control center. Built with React + Vite, it communicates with the NestJS backend via typed API calls and receives real-time events via SSE.
The dashboard provides at-a-glance system statistics across the top, followed by detailed configuration tables. Current metrics:
Below the stats, a Configured Domains table displays each domain with completion status and specialty/career stage counts:
| Code | Name | Status | Specialties | Career Stages | Completeness |
|---|---|---|---|---|---|
| MEDICAL | Medical Professionals | ACTIVE | 11 | 6 | 100.00% |
| LEGAL | Legal Professionals | ACTIVE | 8 | 5 | 100.00% |
| HNW | High Net Worth Individuals | ACTIVE | 4 | 5 | 100.00% |
An Event Streams table follows, listing the distinct event stream IDs and their event counts, separated by aggregate type (ProfessionalDomain, Partner, Offer, Campaign, Membership, MemberProfile, TierProgression, SignalCollection).
The Domain Configuration page manages professional domains. A "+ Create Domain" button enables adding new domains. The domain table lists all configured domains with their type, status, completeness percentage, and action buttons.
Three domains are pre-configured with comprehensive career moments:
Clicking a domain opens its detail page with tabbed sections:
Each tab supports full CRUD operations — add, edit, and remove items. Changes produce domain events that update the read model projections.
The Members page has four tabs: All Members, Pending, Active, and Invitations.
The member list now includes a profile completeness column showing each member's profile progress as a percentage bar.
Clicking a member opens their detail panel with tabs:
Three tabs: Offers, Campaigns, and Partners.
The Platform Config page manages platform-level life moments — universal milestone types that apply to all members regardless of domain (unlike domain-scoped career moments).
The life moment table is grouped by category, with admin CRUD operations:
| Category | Count | Examples |
|---|---|---|
| Family | 5 | Marriage, New Child, Child's College, Empty Nest, Divorce |
| Financial | 4 | Inheritance, Major Investment, Business Sale, Retirement Planning |
| Health | 4 | Health Change, Wellness Program, Insurance Switch, Aging Parent Care |
| Housing | 4 | Home Purchase, Relocation, Renovation, Vacation Property |
| Lifestyle | 4 | Sabbatical, Philanthropy, Travel, Hobby Pursuit |
21 life moments are pre-seeded across these 5 categories. Admins can add, edit, or deactivate life moments. Life moments are available platform-wide at /api/member/life-moments.
The Event Stream page displays all domain events flowing through the system in real time via Server-Sent Events (SSE). Events are color-coded by aggregate type and display:
MemberActivated, OfferRedeemed, SignalRecorded)Events stream in automatically — no polling. The SSE connection listens on the domain-stream.all channel. Historical events can be browsed in the event log table with pagination and filtering by aggregate type.
Consent events: ConsentWithdrawn events appear in the stream with payload showing previous consent state, scope, and anonymization count. The admin behavioral profile tab shows affinities as empty and consent status as withdrawn after a withdrawal.
The Partner Portal at https://specialoffer-demo.fly.dev/partner provides analytics dashboards for approved partner organizations.
The landing page lists all 12 approved partners. Click a partner card to enter their analytics dashboard. Each card shows the partner name, domain focus, offer count, and activation status.
The partner dashboard displays aggregate performance statistics:
Below the stats, the Offer List shows all offers from this partner with individual performance metrics.
Each offer row shows: offer title, category, impressions, clicks, redemptions, click-through rate, and conversion rate. The table is sortable by any column to identify top-performing and underperforming offers.
Clicking an offer row opens a funnel visualization showing the conversion pipeline: Impressions → Clicks → Redemptions. The funnel uses proportional bar widths to visually represent drop-off at each stage. Percentage labels show stage-to-stage conversion rates.
The behavioral intelligence system collects member interaction signals, scores category affinities, and personalizes the offer experience — all with explicit consent gating.
The client-side useSignalTracker hook batches behavioral signals and flushes them to the backend every 5 seconds. The following signal types are collected:
| Signal Type | Weight | Strength | Description |
|---|---|---|---|
PAGE_VIEW |
0.5 | Visited a page | |
OFFER_IMPRESSION |
0.5 | Offer card appeared in the member's feed | |
OFFER_VIEW |
1 | Viewed an offer card in the feed | |
CATEGORY_BROWSE |
2 | Scrolled through an offer category | |
OFFER_CLICK |
3 | Clicked into an offer detail page | |
PARTNER_PIXEL |
4 | Loaded a partner page (pixel confirmation) | |
OFFER_DWELL |
5 | Spent 5+ seconds on an offer detail page | |
PARTNER_VISIT |
7 | Clicked through to a partner site | |
OFFER_REDEEMED |
10 | Redeemed an offer |
Consent is managed through a two-layer system:
A banner appears on every page until the member chooses. It's non-blocking — the member can navigate freely while the banner is displayed. Three actions:
Consent state is stored on the SignalCollection aggregate (server-side). The domain layer enforces consent — signals are silently dropped when consent is not granted. Revoking consent stops recording immediately.
Privacy Preferences access: The “Privacy” link in the member nav bar (visible for active members only) opens the consent preferences panel at any time — not just during the initial consent prompt.
Server-authoritative consent: Consent state is synced from the server on page load. Clearing browser data does not affect consent — it is not stored in localStorage.
Below the preference toggles, a red “Withdraw All Consent & Anonymize My Data” button allows members to fully withdraw consent. What happens on withdrawal:
ConsentWithdrawn event emitted to event stream (visible in admin)Re-consent: Members can grant consent again via the consent banner at any time. They start fresh with a clean behavioral profile.
GET /api/member/consent/:memberId — Returns consent status (behavioralTracking, geographicTracking, thirdPartySharing, isActive)
POST /api/member/consent-withdraw/:memberId — Withdraws consent. Body: { reason: "MEMBER_REQUEST", scope: "ALL" }
The affinity engine converts raw signals into category scores on a 0-100 scale:
For each offer category, the raw score is the sum of weighted signals:
raw_score(category) = SUM( signal_weight * time_decay_factor )
After computing raw scores for all categories, scores are normalized to a 0-100 scale where the highest-scoring category equals 100:
normalized_score(category) = (raw_score / max_raw_score) * 100
This means the member's strongest category always scores 100, and other categories are relative to it. The normalized scores are stored in the BehavioralProfileEntity as JSONB category affinities.
When serving the offer feed, the system enriches each offer with two fields:
affinityScore — The member's normalized affinity for the offer's category (0-100)isRecommended — Boolean, true when affinityScore ≥ 30The member portal sorts offers by affinityScore descending. Offers with isRecommended: true display a "Recommended" badge. Members without consent or without signal history receive the default ordering with no badges.
Partner tracking uses two mechanisms to capture high-intent signals:
When a member clicks "Visit [Partner]" on an offer detail page:
GET /go/{offerId}?m={memberId}
→ Records PARTNER_VISIT signal (weight 7)
→ 302 redirect to mock partner page
Each mock partner page embeds a 1x1 transparent GIF:
<img src="https://specialoffer-demo.fly.dev/api/track/pixel.gif?m={memberId}&p={partnerId}&c={category}&o={offerId}" width="1" height="1" />
The pixel endpoint records a PARTNER_PIXEL signal (weight 4) and returns a 1x1 transparent GIF. This confirms the member actually loaded the partner page (not just that they clicked the redirect).
Admins can see each member's full behavioral profile:
SpecialOffer is built as a CQRS/Event Sourcing platform using NestJS, with a React admin UI and server-rendered member/partner portals.
| Portal | Technology | URL Pattern | Purpose |
|---|---|---|---|
| Admin UI | React + Vite SPA | /admin/* |
Domain config, member management, offers/partners, platform config, event stream |
| Member Portal | Server-rendered + client JS | /, /dashboard, /profile |
Directory, onboarding, dashboard, offers, profile, career journey, consent |
| Partner Portal | Server-rendered | /partner/* |
Partner selection, analytics dashboards, offer performance |
apply(). Events are appended to an in-memory event store with optimistic concurrency control.
@OnEvent('domain.EventType') handlers update TypeORM read models in PostgreSQL. Read models are denormalized for fast queries. No foreign keys (CQRS pattern).
domain.{EventType} for projections. SSE catch-all listens on domain-stream.all. Cross-module reactions via event handlers.
| Module | Path | Aggregate(s) | Status |
|---|---|---|---|
| Shared Kernel | src/shared/ |
— | Complete |
| Domain Config (M9) | src/domain-config/ |
ProfessionalDomain | Complete |
| Membership (M1) | src/membership/ |
Invitation, Membership, Verification | Complete |
| Offers (M3) | src/offers/ |
Offer, Campaign | Complete |
| Partners (M4) | src/partners/ |
Partner | Complete |
| Tiering (M5-lite) | src/tiering/ |
TierProgression | Complete |
| Member Profile (M2) | src/member-profile/ |
MemberProfile, CareerJourney, SignalCollection | Complete |
| Tracking | src/tracking/ |
— (controller only) | Complete |
| Admin | src/admin/ |
— | Complete |
Domains are classified by type, which controls offer visibility:
| Domain Type | Domains | Offer Visibility |
|---|---|---|
| PROFESSIONAL | Medical, Legal | Members see only offers tagged to their domain |
| LIFESTYLE | HNW | Members see offers from all LIFESTYLE domains |
This distinction ensures Medical professionals see only Medical offers, while HNW members can see offers across all lifestyle-oriented domains.
Key cross-module event chains:
MemberActivated → triggers MemberProfileInitialized (creates profile + career journey)MemberActivated → triggers TierProgressionStarted (initializes at Basic tier)OfferImpression / OfferClicked / OfferRedeemed → auto-recorded as behavioral signals (when consent granted)ConsentGranted → enables signal recording on SignalCollection aggregateConsentWithdrawn → anonymizes signals, resets behavioral profile, deactivates consentSignalHistoryPurged → projection anonymizes signal log entries (memberId → hash)SignalRecorded → updates BehavioralProfile projection (affinity recalculation)TierUpgraded → updates membership projection (offer eligibility changes)@shared/*, @membership/*, etc.
admin-ui/. Vite dev server on :5173, proxies /api to NestJS on :3000. Dark theme with aggregate color coding.
synchronize: true for PoC. JSONB columns for flexible event data. No foreign keys.
domain.{EventType} for projections, domain-stream.all for SSE. Replaces SNS/SQS for PoC.
ops.sh script for deploy, seed, test, reset, and status operations.
250 automated tests verify the platform end-to-end:
| Suite | Type | Tests | Sections |
|---|---|---|---|
| Business Rules | API integration tests | 209 | 18 sections covering domain config, membership, offers, partners, tiering, member profile, signal collection, partner tracking, life moments, consent withdrawal |
| Redemption Flow | End-to-end flow test | 40 | 40 sequential steps: invite → apply → approve → activate → browse → redeem → partner visit → behavioral signals → consent withdrawal |
Run tests locally or against the remote deployment:
# All tests against local
npm run test:all
# Business rules only
npm run test:rules
# End-to-end flow only
npm run test:flow
# Against remote deployment
npm run test:remote