Skip to main content
The application is a single Laravel deployment with three fairly distinct halves: an Inertia-rendered dashboard, a queue-driven data collection layer that talks to external providers, and a scoring layer that turns collected raw answers into the numbers shown in the UI.

Request layer

Every dashboard route sits behind ['auth', 'verified', 'onboarded']. The onboarded middleware is the important one — it forces any user with an incomplete profile back to /setup, so onboarding is mandatory rather than skippable. Pages are scoped to an active campaign held in the session under selected_campaign_id. The sidebar switcher (POST /campaigns/select) validates that the campaign belongs to the user via accessibleCampaigns(), sets the session key, and always redirects to /visibility — never back to the current page, since a page like a prompt detail view belongs to the campaign the user just switched away from.
Campaign::getRouteKeyName() returns uuid, so campaigns are addressed by UUID in URLs, never by autoincrement ID.

Service layer

app/Services holds one class per external system plus the domain logic:
Services/LLM has a provider class per model family: OpenAiProvider, GeminiProvider, ClaudeProvider, PerplexityProvider, GrokProvider. Google’s AI Overview and AI Mode surfaces are reached through SerpApi rather than a direct API.
Services/BrandSentiment runs as a small pipeline: TopicSelectorTopicDataRetriever (using RetrievalPromptWriter) → TopicSentimentScorer (using EvaluationPromptWriter) → BrandSentimentFinalizer.
Services/Onboarding contains the wizard’s generators — BrandNameGenerator, BusinessDescriptionGenerator, BrandVariantsGenerator, TopicGenerator, PromptRetrievalGenerator — plus MasterTaxonomy and LocaleMapper.
GoogleAnalyticsService, Google/SearchConsoleService, BingWebmasterService, PageSpeedService, SiteInfoService, Semrush and GoogleSearchScreenshotService.
IndustryVisibilityScorer, VisibilityDetector, CompetitorDiscovery and CompetitorClassifier.

Collection layer

No provider call happens in the request cycle. Controllers dispatch jobs and the UI polls for status — for example GET /api/sentiment/status backs the sentiment page’s polling. Jobs come in Fetch* / Process* pairs: FetchOpenAiSentimentJob collects, then ProcessOpenAiAnswersJob parses answers and records detections. TokenUsageTracker and the LlmUsage model record spend per call, and PromptRunFailureAlert surfaces batches that failed. Failures are also reported to Mattermost through a custom log channel when MATTERMOST_LOG_ENABLED=true.

Scoring layer

config/scoring.php defines the weighted formula:
Provider weights must sum to 1.0 and default to OpenAI 0.30, Perplexity 0.25, Gemini 0.20, Grok 0.10, AI Overview 0.10 and AI Mode 0.05. A rank_degradation_exponent of 1.2 penalises lower mention counts more steeply than a linear scale, min_normalization prevents dividing by very small maxima, and non_competitor_penalty halves the contribution of domains the classifier did not mark as direct competitors. Every weight is overridable via SCORING_* environment variables.

Frontend

React 19 + Inertia 2 with TypeScript. Controllers return props; there is no separate REST API for the dashboard, though several JSON endpoints exist for polling and report data (/campaigns/{campaign}/traffic/summary, the reports/* API routes, and so on). Ziggy exposes named routes to TypeScript.