
OnboardingEnum state on every auth check.RexAI's onboarding is a strictly ordered, gated pipeline. Each step in the OnboardingEnum acts as a gate — the frontend reads the user's onboardingStep field on every auth check and routes accordingly, preventing dashboard access until all steps are complete. The backend enforces the same order — any out-of-sequence request returns a 403..
After email verification, the user is directed to Stripe Checkout. The backend listens for the checkout.session.completed webhook event and advances onboardingStep only after Stripe confirms payment — client-side state is never trusted for subscription gating.
The BusinessInformation schema captures name, street address, city, state, zip, phone, website URL, and geo-coordinates sourced via Google Places API autocomplete. A pre-save middleware runs against a requiredFields array and sets isComplete: true only when all fields pass. This flag gates progression to the Google Ads connection step.
After business info, the user connects their Google Ads account via OAuth2. A single master MCC (My Client Center) account is configured at the platform level. The OAuth callback stores the user's access_token, refresh_token, and tokenExpiresAt in the GoogleAdsManager collection. A corresponding CustomerAccount document is then created, referencing both the master manager and the user's specific customerId.
The user's onboardingStep in MongoDB is the single source of truth. A user who drops off mid-onboarding resumes exactly where they left off on next login - the checkAccountStatus endpoint returns their current step and the frontend routes accordingly. No step can be re-entered or skipped once completed.
login_customer_id header while targeting the user's specific customer account ID. This is the standard MCC sub-account access pattern - one developer token, centralised quota management, full audit visibility across all user accounts.The core of RexAI is ClaudeAIService, which calls claude-sonnet-4-20250514 with an 8,000-token budget to generate a complete Google Ads campaign structure from business data. The system prompt designates Claude as a "master Google Ads expert and copywriter specializing in SKAG architecture" that must always respond in valid JSON.
The platform enforces Single Keyword Ad Group (SKAG) architecture as a hard constraint in the system prompt. Every campaign contains exactly one ad group per target keyword. Each ad group is named keyword (phrase) to make match type explicit. Campaign names follow the static convention: Search | {Service} | Phrase. This maximises Quality Score and simplifies performance attribution by ensuring each ad is written around one search intent.
The prompt runs a three-step process: Business Research (website analysis, USPs, CTAs, brand voice), Keyword Development (service-level campaigns with SKAG grouping), and Ad Copy Creation (all headlines, descriptions, and extensions per ad group). Character limits are enforced inside the prompt itself — if any field exceeds its maximum, Claude is instructed to truncate at a semantic break.
The prompt receives the user's existingUserCampaigns array — Claude will not regenerate campaigns for services that already have live entries, preventing duplicates and respecting the current account state across multiple generation runs.
max_tokens: 8000, this eliminates the need for any post-processing or sanitisation layer before the response is written to AdGeneration.pending in AdGeneration. No campaign is deployed to Google Ads until a human approves it.After Claude generates the campaign structure, every document is written to the AdGeneration collection with status: 'pending'. The Ad Copy Review dashboard page surfaces all generated content — every headline, description, keyword, extension, budget recommendation, and bid strategy reasoning — for user review before any Google Ads API call is made.
All fields are editable in-place. The adApprovalController handles granular field-level updates. Client-side validation enforces Google Ads character limits before each save — headlines cannot exceed 30 characters, descriptions 90 — mirroring the Google Ads API's own validation to prevent deployment failures from policy violations.
Once the user approves, the backend transitions AdGeneration.status to 'approved' and sets approvedAt. The campaignService then runs a sequential orchestration against Google Ads API v21: create campaign with budget and bid strategy → create ad group → create responsive search ad → attach callouts → attach sitelinks → attach structured snippets → attach call extensions. Each step creates a corresponding document (Campaign, AdGroup, Ad) in MongoDB with the returned Google resource IDs.
post('save') and post('findOneAndUpdate') middleware on AdGeneration calls MarketingInformation.syncFromAdGeneration() whenever an existing generation is modified. This keeps the marketing profile and campaign data in sync without additional reconciliation jobs.The frontend redirects to Google's OAuth2 consent screen via generateAuthUrl() with the https://www.googleapis.com/auth/adwords scope. The backend callback calls getTokensFromCode() to exchange the authorisation code for access_token and refresh_token, stored in GoogleAdsManager alongside a tokenExpiresAt timestamp. Before every API request, shouldRefreshToken() checks whether the token expires within the threshold — if so, refreshAccessToken() is called proactively, and the new token is written back before the API call proceeds.
The Reporting section executes GAQL (Google Ads Query Language) queries against the Google Ads API — pulling impressions, clicks, CTR, cost, conversions, and average CPC at the campaign, ad group, and keyword level. Date-range parameters are passed as GAQL WHERE segments.date BETWEEN clauses. Results are returned directly to the frontend with no intermediate cache layer.
login_customer_id header while targeting the user's specific customerId. This is the required pattern for MCC-managed sub-account access and enables the platform to operate across all accounts from a single developer token without per-user token escalation.Subscription management is handled exclusively through Stripe webhooks — the backend never trusts client-side state for subscription status. Every event is validated with stripe.webhooks.constructEvent() against the STRIPE_ENDPOINT_SECRET before processing. The Stripe webhook route is registered before any global express.json() middleware to preserve the raw body required for signature verification.
The Plan model defines available tiers with associated feature flags. When a user's subscription changes, the ActiveSubscription document is updated and downstream access-control gates (campaign limits, reporting access) reflect the new state immediately on the next protected request.
If a payment fails during onboarding or on renewal, the user is redirected to /payment-retry. A new Stripe checkout session is created for the same plan. The checkAccountStatus endpoint validates subscription and onboardingStep state on every protected page load, routing users to the retry page if their subscription is in a failed state.
express.json() middleware modifies the request body, which invalidates Stripe's HMAC signature. The webhook endpoint uses a dedicated express.raw() parser, registered before the global JSON middleware, to ensure the unmodified payload is available for stripe.webhooks.constructEvent().Beyond campaign creation, RexAI includes two automated optimisation layers running on schedule via Agenda.js — a MongoDB-backed job queue. Jobs are defined once and scheduled per-user on account activation. History, retry logic, and failure tracking live in the same Atlas cluster as application data.
The biddingService and budgetOptimizationService pull campaign-level metrics from the Google Ads API — impressions, CTR, conversion rate, cost-per-conversion — and pass them through the budgetOptimizer.js multi-factor scoring model. The bidStrategy.js utility maps AI-recommended BidStrategyType enum values to Google Ads API enum values and validates compatibility with the campaign's current settings before submitting any mutation.
NegativeKeywordService queries the last 30 days of search term data via GAQL, identifies terms with clicks but zero conversions, and adds them as phrase-match negatives at the campaign level. It first runs checkAccountConversionsLast30d() - if the account has fewer than 1 conversion in the period, negative keywords are not added, preventing premature exclusions on new accounts. Every addition is recorded in NegativeKeywordAudit with timestamp, campaign, term, and reason.
budgetOptimizer.js weights three factors per campaign: conversion rate, impression share lost to budget, and actual vs. target CPA. The resulting score drives reallocation recommendations surfaced in the dashboard and applied in one click.AdGeneration.login_customer_id. One developer token covers all user accounts, API quota is managed centrally, and all mutations are auditable from a single source. Users connect via OAuth2 and grant access — no credential sharing occurs between the platform and user accounts.shouldRefreshToken() compares tokenExpiresAt against a threshold before every Google Ads API request. Proactive refresh prevents mid-operation token failures during campaign deployment, which would otherwise leave campaigns in a partially created state with no clean rollback path.onboardingStep field is the single source of truth for funnel position. Frontend reads it on every auth check; backend validates it on every onboarding endpoint. No client manipulation can skip a step. A user who drops off mid-onboarding resumes exactly where they left off — zero re-entry of already-completed data.shouldRefreshToken() handles proactive token refresh — users never need to reconnect.login_customer_id. Centralised quota, one developer token, full audit visibility across every account.status: pending. Inline editing with Google Ads char-limit validation before approval. Zero unreviewed content ever reaches the API.NegativeKeywordAudit trail in MongoDB for complete transparency.onboardingStep as single source of truth. Frontend + backend both enforce step order. Fully resumable — drop-off and re-entry with zero data re-entry.