beyondcode/fathom-api
A Laravel-friendly PHP client for the Fathom Analytics API. Query sites, page views, events, and other analytics data directly from your app, with a clean wrapper around Fathom's endpoints for dashboards and reporting.
Architecture fit. beyondcode/fathom-api is a thin fluent adapter over Fathom's v3 HTTP API, not a data layer. It contributes four capabilities — /sites, /aggregations, /events, /current_visitors — and returns untyped PHP arrays. That means it fits cleanly into one architectural role and only one: a read-only reporting edge for a Laravel app that has already committed to Fathom as its analytics backend. It fits poorly anywhere the application needs a domain model for analytics (sessions, funnels, retention, user journeys). The architecture decision this package actually implicates is not "do we adopt this library" but "do we standardize on Fathom," because the library is trivially replaceable and the backend is not.
Integration feasibility. Low effort, high ambiguity. Installation is composer require beyondcode/fathom-api:^1.1, a published config, and one env token; Laravel auto-discovery registers the provider and Fathom facade. A first successful Fathom::sites() call is achievable in well under a day, and a dashboard widget is a day or two more. The ambiguity is in what you're integrating against: 1.1.0 is the only release that speaks v3, and there is no documentation stating whether v3 coexists with the v2 path behind config or replaces it. Budget an explicit spike — not a feature sprint — to answer that empirically against a live token before committing scope.
Technical risk. Five risks, ranked by what they would cost us:
"1.1.0" exactly, and keep the facade behind our own AnalyticsReport interface so replacement is a single container binding.null and nothing raises. This is the risk most likely to reach a stakeholder before it reaches engineering — a client report showing zero traffic. Mitigation: DTO mapping at the boundary plus a validation assertion that logs a warning on missing keys.Key questions to resolve before approval.
1.1.0 additive or breaking versus v2, and did the config schema change? (Answer determines whether this is a dependency bump or a migration project.)Stack fit. Native fit for Laravel 8+ with package discovery; Lumen requires manual provider and alias registration in bootstrap/app.php, and a missed registration fails as an opaque null-reference rather than a useful exception — call that out for any Lumen service in the estate. The package sits on Laravel's HTTP client, which means timeout, retry, and Http::fake() all work through the framework's existing mechanisms and our existing observability. No new infrastructure, no queue worker, no migration — unless we adopt the snapshot table, which we should.
Migration path. Assume the package is the easy part and Fathom is the hard part. Three entry scenarios:
Compatibility. Two hard constraints. First, v2→v3 surface change is unconfirmed — verify returned array keys against a live v3 response before trusting any of them, and treat the published base_url/version config key as the highest-risk value in the file. Second, config:cache is the dominant failure mode: a stale cached token or base URL keeps hitting the old endpoint, and the exception message contains nothing that identifies the cause. Any runbook for this integration must lead with config:clear / config:cache.
Sequencing. Recommended four-stage rollout, each stage independently valuable and independently abandonable:
Fathom::sites() against a real site. Produce a one-page answer to the v2/v3 ambiguity and a sample of every response shape we intend to use. Decision gate: proceed or stop.AnalyticsReport interface with a Fathom-backed implementation, site ID resolved once into config or DB — never a firstWhere('name', ...) lookup on a request path, which fails silently on rename and throws on absence. Bind per-environment so local, staging, and production point at distinct sites with no code change.SnapshotFathomDaily per site into an analytics_daily table, withoutOverlapping(). This is the load-bearing stage: it converts an unreliable live API into a dependable internal data source, gives us history beyond Fathom's retention, enables backfill and repair without touching the vendor, and is the precondition for any external-facing report.Only after stage 3 should we expose numbers to anyone outside the internal team. Anything shown to a client before the snapshot exists is being served by a live rate-limited call on a render path.
Maintenance. Low code volume, disproportionate process overhead. Plan for: exact version pin; a watching brief on the repo and on Fathom's v3 changelog; and a quarterly dependency review line item with an explicit trigger — 12 months with no release plus a Fathom v4 announcement means fork or replace, not "hope." Keeping the vendor behind our AnalyticsReport interface reduces the replacement cost to a one-line binding, which is the single highest-return piece of discipline in this entire integration.
Support. Support load concentrates in two places. First, "the number is wrong" — which with untyped arrays and no vendor-side error signalling is genuinely hard to triage, and is why DTO validation with structured warnings is a support tool, not just a code-hygiene preference. Second, auth and config — the debugging order is fixed and should be written into the runbook verbatim: sites() first (auth + base URL), then a wide 90-day range (isolates date-argument bugs from endpoint bugs), then HTTP logging for endpoint paths, then config:cache if it works locally but not in production. Publishing that order saves more engineer-hours than any refactor.
Scaling. Scale is not the constraint; freshness is. Every additional widget, portal, or client site multiplies HTTP calls against a throttled vendor. The architectural answer is fixed: cache per-site, per-metric, per-range — not per-dashboard — so five widgets over the same pageview query cost one call, and dashboards read the local snapshot rather than the API. Explicit timeout, retry with backoff and jitter, and a circuit breaker in front of non-critical reporting paths. Multi-site and client-portal use is a config loop plus per-site fetch; it scales linearly in storage and near-zero in vendor calls if stage 3 is done properly.
Failure modes to design for, and the product response to each.
Fathom::sites() health check raises it before a stakeholder does.Ramp-up. Effectively zero for a Laravel developer — the public surface is two files, and a working integration is measurable in hours. The real ramp-up is organizational: stakeholders must internalize that this is traffic reporting, not product analytics, and that "today" on a dashboard means yesterday's full aggregate rather than the current partial day. Both are expectations problems rather than engineering problems, and both are cheaper to fix in the spec than in a steering meeting after launch. Timezone mismatch between the Fathom site and the application is the same class of issue — decide it explicitly, once, and document it next to the site ID.
Recommendation. Approve as a time-boxed spike with an explicit go/no-go, targeting internal dashboards only, with an exact version pin and the adapter seam in place before any widget code is written. Do not treat it as a strategic dependency, and do not put it on a client-facing surface until the nightly snapshot exists and has run clean for a full reporting cycle. The 0.3 package score and the 6.59 opportunity score are not in conflict — they describe a small, low-maturity wrapper around a genuinely useful need. Buy the wrapper, own the interface, and keep the exit cheap.
How can I help you explore Laravel packages today?