Skip to main content
bisibility has two main processes:
  • The web app serves the dashboard, REST API, Model Context Protocol (MCP), and live notifications.
  • The Temporal worker runs scheduled rank checks and maintenance jobs.
Both processes use PostgreSQL. Valkey ships by default, while any Redis-compatible endpoint can handle rate limits, idempotency, and notifications. Rank data comes from a SERP provider that you connect yourself.

Runtime components

The MCP endpoint uses the same router as REST. Manual and scheduled checks also use the same rank-check runner. These shared paths keep behavior consistent.

Request and data flow

In words: the web process authenticates browser, REST, SDK, CLI, and MCP traffic, then reads or writes durable project state in PostgreSQL. Redis-backed services enforce cross-process rate limits and idempotency and fan out live notifications. A manual check can call the shared rank-check runner from the web process. Scheduled work starts through Temporal, whose task queues are polled by the bisibility worker. Worker activities call the configured provider, persist results and audit rows in PostgreSQL, and publish resulting notifications through Redis. Temporal keeps workflow state; it is not the source of truth for projects, keywords, or rank history.

The worker is not the Temporal server

Temporal is split into two independent pieces, and only one of them ships in the bisibility images. The Temporal server stores workflow state, history, schedules, and task queues. It runs no bisibility code and knows nothing about rank checks. It is infrastructure that the stack connects to. The Temporal worker is a bisibility process. It hosts the workflow and activity code, polls the server for due work, and executes it. This is the bisibility-worker image. Two consequences follow. Starting the worker on its own does nothing. It needs a reachable Temporal server, which is why the Temporal topology needs TEMPORAL_ADDRESS, a dedicated TEMPORAL_NAMESPACE, and both task queues. The web app needs the same values, not to manage schedules, but to start workflows. SCHEDULER_DRIVER=none keeps manual checks inline and disables scheduled execution in the core topology. The Temporal server is replaceable. Docker can add the bundled Temporal overlay, but the same worker image runs unchanged against a self-managed cluster or Temporal Cloud. See Temporal Server or Temporal Cloud.

Data model

A Project owns its keywords, provider connections, alert rules, notifications, and audit entries. Each keyword is unique by text, location, and device. Scheduling is stored as intent, not as jobs: ProjectDefaults holds the project-wide schedule and market, and an optional per-keyword KeywordSchedule overrides it. Each executed check is a RankCheck row recording status, position, ranking URL, provider, and cost. Alerts connect AlertRule to TriggeredAlert and DeliveryAttempt rows.

Rank data retention by deployment

Each completed check can store a normalized SERP snapshot in rank_checks.raw. The adapters reduce provider responses to normalized organic results and SERP feature labels before storage. That snapshot is separate from the rank-history fields that remain on the row. The dashboard reads the snapshot on the keyword detail page and in the checks table, and it is also available through direct PostgreSQL access to rank_checks.raw; the REST API rank-check resource does not expose this field. Once a configured window expires, an older check can therefore have a null raw value while its row, position, and organicRanks remain available. See Database growth for worker configuration and purge behavior.

Rank-check lifecycle

The app never creates Temporal Schedules directly. It only writes schedule intent to Postgres; a singleton reconciler in the worker periodically converges per-keyword Temporal Schedules to match that intent, creating, updating, and pruning them as keywords change. Postgres stays the single source of truth, and the worker is safe to restart or temporarily disable. Manual checks go through the same runner and persistence (lib/rank-check/) as scheduled checks. When Temporal is unreachable, interactive manual checks fall back to inline execution in the app process; the REST API can also start a check asynchronously and return 202.

Extensibility: providers

Providers are the main extension point, under lib/providers/. Kinds. Every provider has a kind. serp providers answer “where does this keyword rank” and are tried in a user-configured fallback chain; analytics providers add owned-data context such as traffic and clicks, and never set positions. A new category of data source - social media, for example - would be a new kind following the same pattern. Registry. PROVIDER_CATALOG in lib/providers/registry.ts is the single source of truth for which providers exist, their kind, and the credentials they require. The user-facing roster and setup details live in Integrations. Adapters. Each provider is one adapter module implementing a small interface: identify itself, test a connection, fetch a rank. Adding a search engine such as Bing means writing one new serp adapter and registering it in the catalog - the scheduling, fallback, cost, and alerting machinery is provider-agnostic and does not change. Connected credentials are stored encrypted per project. Optional environment fallbacks for local development are documented in Integrations.

Alerts and notifications

After a successful check persists, the alert evaluator applies each enabled rule’s conditions to the new result. Matching rules create a triggered alert, which is delivered to the configured channels - email and signed webhooks - with every delivery attempt recorded. Slack tenant delivery is available as an API-only preview. Workspace installation and channel management are not yet exposed in the dashboard. Successful deliveries and completed checks also create in-app notifications, pushed live to the browser over SSE.

Operational notes

Valkey backs the cross-process concerns by default: API and outbound provider rate limits, idempotency replay for non-GET API requests, and pub/sub fan-out for realtime notifications. Local development degrades to in-memory fallbacks when Redis is absent. Temporal workflow code stays deterministic. Workflows contain no database, network, or Node-specific calls; all side effects live in activities. This standard Temporal boundary is what makes scheduled checks replayable and safe across worker restarts. Code layering - which modules may import which - is lint-enforced; see CONTRIBUTING.md in the repository.