24 KiB
CI Pivot MVP — Umbrella Plan
Master roadmap for the Competitive Intelligence pivot. Each phase becomes its own subplan saved in this folder (
plans/backend/).
This is the high-level roadmap. It is sequenced to match the agreed order: rename first, then connector restructure, then Pipelines.
SCOPE: This umbrella currently covers the BACKEND only (
surfsense_backend). Frontend (surfsense_web) and client apps (desktop, Obsidian, browser extension) will get their own umbrella/subplans LATER, once the backend is fully working as expected. Frontend-facing decisions (URL segment, TS types, i18n copy) are recorded below where relevant but are out of scope for the active phases.
Positioning
"NotebookLM for Competitive Intelligence" — each WorkSpace acts as a workspace for setting up competitive-intelligence-optimised notebooks.
Target architecture
flowchart TD
WS[WorkSpace] --> CONN[Connectors]
WS --> PIPE[Pipelines]
WS --> KB[(Knowledge Base: documents + chunks)]
CONN --> T1[Type 1: Data Sources - pull]
CONN --> T2[Type 2: MCP Tools - act]
T1 --> WEB[Universal WebURL Crawler - functional]
T1 --> PLAT[Platform connectors - coming soon]
T1 --> UP[File Upload]
PIPE --> RUN[PipelineRun history]
PIPE -->|"save_to_kb + destination folder"| KB
RUN -->|"manual or cron"| T1
T2 --> CHAT[Chat / Automations]
CHAT --> DELIV[Deliverables: audio/video/report/image]
RUN -->|"read-only context"| CHAT
Decisions locked
- Full rename SearchSpace -> WorkSpace across DB, API, URLs, code, satellite apps.
- Canonical names (proposed defaults): DB table
workspaces, columnworkspace_id, RBAC tablesworkspace_roles/workspace_memberships/workspace_invites, API base/workspaces(consolidating today's/searchspacesvs/search-spacessplit), URL segment[workspace_id], settings folderworkspace-settings, TS typeWorkspace. - Connectors split into two types via a STATIC code registry (
connector_type→ category/availability), NOT a DB column. Type 1 =DATA_SOURCE(pull → pipelines/KB): WebURL crawler, Google Drive (native + Composio), OneDrive, Dropbox, file uploads, plus deferred platform connectors. (YouTube is DEFERRED — no backend connector exists today.) Type 2 =MCP_TOOL(act in chat): only the genericMCP_CONNECTORis functional for MVP. All branded natives are deprecated (MIGRATING, turned off) pending a post-MVP MCP re-point — they are NOT functionally moved to MCP in this MVP. Artifacts stay in the existingdeliverablesagent system (not routed through MCP). (Full model: Phase 4 below /04a.) - Web search APIs (SearXNG, Linkup, Baidu) are repurposed as a SOURCE-DISCOVERY helper: they suggest URLs the user can add to the Universal WebURL Crawler when setting up pipelines (they are not a standalone connector type and do not index data). NOTE: Tavily and Serper are being REMOVED from the search infra and are not part of this set.
- Obsidian and Circleback (push/webhook sources) are DISABLED for the MVP.
- MCP-availability audit complete: BookStack (community MCP servers), Elasticsearch (official Elastic Agent Builder MCP), and Luma (community MCP servers) all have MCP available, so none are
DISABLED— they're taggedMIGRATING(turned off for MVP like the other branded natives, pending the post-MVP MCP re-point). PipelineandPipelineRunare new first-class tables. A Pipeline references a connector + config + schedule + KB destination. File upload creates/uses a pipeline and registers a run; uploads always save to KB.- The chat agent gets read-only access to pipeline run history (pipelines + their recent runs/status) as context, so it can reason about what was fetched, when, and whether runs succeeded — even for data not saved to the KB.
- Deferred (post-MVP): platform scraper implementations, public pay-as-you-go API for Type-1 connectors, public MCP server exposing the KB.
Platform connector research list (deferred build, MVP = "coming soon")
- LinkedIn — people profiles (discovery by keyword/company), company info, job listings.
- Amazon — product (ASIN), search (keyword), pricing; reviews secondary.
- Google — Web Search (organic SERP), AI Overviews, Maps/Local (discover by location).
- Instagram — profiles first, then posts; discover profiles by username/keyword.
- Zillow / Redfin — full property listings (discover by search URL/filters); Zillow price history.
- Walmart — product, search; zipcode-localized pricing premium variant.
- eBay — search by keyword/category; price-comparison/resale feeds.
- Crunchbase — company info, search by keyword (B2B lead-gen / investor research).
- TikTok / YouTube — profiles/channels, posts/videos; discover by keyword/hashtag; TikTok Shop.
- Indeed / Glassdoor — job listings (discover by keyword in location), company reviews.
Backend phases (active — this umbrella)
Phase 1 — Rename foundation (DB) [subplan: 01-rename-db.md]
- Alembic migration: rename
searchspaces->workspaces; renamesearch_space_id->workspace_idon ~20 child tables; rename RBAC tables and their FKs; rename indexes/constraints (uq_searchspace_*,idx_documents_search_space_id, etc.); update Rocicorp Zero publication column lists (backend-ownedpublicationdefinition; frontend Zero schema rename happens in the later frontend umbrella). - Decide transition strategy: hard cutover (simplest for MVP) vs temporary API aliases for clients.
- Key files:
surfsense_backend/app/db.py,surfsense_backend/alembic/versions/(new migration).
Phase 2 — Rename backend (code + API) [subplan: 02-rename-backend.md]
- Rename models/schemas/services/routes/agents/tasks identifiers:
SearchSpace*->Workspace*,search_space_id->workspace_id. - Consolidate API to
/workspacesand fix the/searchspacesvs/search-spacesinconsistency. - High-touch files:
routes/search_spaces_routes.py,routes/rbac_routes.py,utils/rbac.py(check_search_space_access),schemas/search_space.py, plussearch_space_idthreading through agents/Redis keys/storage paths (documents/{id}/...).
Phase 3 — WebURL Crawler & Crawl Billing (backend) [subplans: 03a–03d]
The Universal WebURL Crawler is the flagship Type-1 data source (the moat). This phase hardens it on a single framework (Scrapling), generalizes proxy support, introduces pay-as-you-go crawl credits, and (deferred) adds opt-in captcha solving. It is broken into focused subplans:
03a-crawler-core.md— Standardize the fetch layer on Scrapling. Remove Firecrawl entirely (no other frameworks). Define crisp per-URL success/empty/failure semantics, keep Trafilatura extraction, and expose a single billable "successful crawl" signal (one unit per URL that yields usable content, regardless of how many internal fallback tiers ran).03b-proxy-expansion.md— Add a BYOCustomProxyProvider(the only new provider — no branded vendors) alongsideanonymous_proxies, selectable via a single, app-wideConfig.PROXY_PROVIDER. Add bounded client-side rotation+retry via Scrapling'sProxyRotator/is_proxy_erroronly when the active provider is pool-backed (CUSTOM_PROXY_URLS); single-endpoint providers (incl.anonymous_proxies) stay the default and no-op the retry. No per-connector/per-crawl selection (one provider app-wide); a per-pipeline override is left as a no-op seam for Phase 5/6.03c-crawl-billing.md— Charge crawl credits at $1 / 1000 successful requests = 1000 micro-USD per successful crawl, drawn from the existing credit wallet (credit_micros_balance), gated by a newWEB_CRAWL_CREDIT_BILLING_ENABLEDflag (off for self-hosted). Two surfaces: connector/pipeline crawls billed to the workspace owner via a dedicatedWebCrawlCreditService(mirrorsEtlCreditService's gate →check_credits→charge_credits, notbillable_call); chat scrapes fold their crawl cost into the chat turn's existing bill (turn accumulator). No DB migration (uses the existing free-formweb_crawlusage_type).03d-captcha-solving.md(DEFERRED — sequenced last, non-MVP-blocking) — Covers the captcha types Scrapling does not (reCAPTCHA v2/v3, hCaptcha, image) viacaptchatools.captchatoolsis itself the provider registry (new_harvester(solving_site=…)across capmonster/2captcha/anticaptcha/capsolver/captchaai), so we do not rebuild a provider hierarchy — our layer is thin: config resolution + a StealthyFetcherpage_actionthat detects the sitekey, harvests a token, and injects it. Scrapling already handles Cloudflare Turnstile (03a). Flags the billing asymmetry (solvers charge per attempt,03cbills per success) for resolution at build time. Requires a paid solver account.
Phase 4 — Connector two-type restructure (backend) [subplans: 04a–04b]
Split into two independently testable subplans:
04a-connector-category.md— Connector taxonomy + availability gating + the MCP routing-gap fix. Introduce a static registry keyed byconnector_type→ (category=DATA_SOURCE|MCP_TOOL,availability=AVAILABLE|MIGRATING|DISABLED), exposed as computed fields on the connector schema/API. Keepis_indexable(orthogonal capability flag — still gates the real/index+ periodic machinery; NOT replaced). Gate creation/index/subagent-build/pipeline-eligibility off the registry. Fix theMCP_CONNECTORsubagent routing-map gap. No DB migration (registry is code).04b-source-discovery.md— Web-search repurposing. Drop all 5 search-APIconnector_typeenum values (SERPER_API,TAVILY_API,SEARXNG_API,LINKUP_API,BAIDU_SEARCH_API) + their connector paths; relocate survivors to platform/env config (SearXNG already env-based; Linkup + Baidu keys move from per-connectorconfigto platform env); rewire the chatweb_searchtool onto platform providers; add a backend source-discovery endpoint suggesting candidate URLs for the WebURL Crawler. Has a (destructive) migration to delete the 5 connector types' rows.
Locked model (MVP):
- Functional Type-1 (Data Sources — can create pipelines + feed KB): Universal WebURL Crawler (
WEBCRAWLER_CONNECTOR), Google Drive (native + Composio), OneDrive, Dropbox, plus file uploads. YouTube is deferred (no backend connector exists today — net-new work; reserve the slot). Deferred "coming soon" platforms: LinkedIn, Amazon, Google, Instagram, Zillow/Redfin, Walmart, eBay, Crunchbase, TikTok, Indeed/Glassdoor. - Functional Type-2 (MCP Tools — act in chat, NO pipelines): ONLY the generic
MCP_CONNECTOR(bring-your-own MCP server). TheMCP_CONNECTORrouting-gap fix (04a) is what makes it work. - Deprecated for MVP ("migrating to MCP soon",
availability = MIGRATING): every branded native — current indexers (Notion, GitHub, Confluence, BookStack, Elasticsearch) AND act-only ones (Slack, Teams, Linear, Jira, ClickUp, Airtable, Discord, Gmail, Google Calendar, Luma, + Composio Gmail/Calendar). Behaviour: block new creation, disable/index+periodic + their chat subagents; KEEP existing rows + already-indexed KB docs searchable (via the always-on knowledge-base subagent). Real MCP migration is post-MVP. - Disabled for MVP (
availability = DISABLED): Obsidian (plugin push) and Circleback (meeting webhook) — distinct from MIGRATING (not MCP-bound). - Frontend connector UI restructure is DEFERRED (frontend umbrella).
Phase 5 — Pipelines data model [subplan: 05-pipelines-model.md]
- New tables:
pipelines(workspace_id,created_by_idnullable SET-NULL [creator metadata, not owner], connector_id nullable, name, config JSONB,schedule_cron+schedule_timezone+ enabled + next_scheduled_at,save_to_kbbool,destination_folder_idnullable) andpipeline_runs(pipeline_id, status, trigger = manual/scheduled/upload, timestamps, doc counts,crawls_*,charged_micros, error, optional raw-result blob ref). - Models + Pydantic schemas + Alembic migration + backend Zero publication entry.
- Pipelines API routes: CRUD + manual run trigger + list runs.
Phase 6 — Pipeline execution + scheduling [subplan: 06-pipelines-exec.md]
- Run engine: pipeline run -> invoke connector fetch (WebURL crawler for MVP) -> if
save_to_kb, persist to the destination folder (the crawler writesDocuments directly viaindex_crawled_urls, extended with afolder_idparam; it does not useIndexingPipelineService, which is the file/Composio path) -> writePipelineRunrecord. (06 verified the crawler indexer is its own KB-write path.) - Crawl billing wiring (carry-over from
03c):03cmeters crawls insidewebcrawler_indexer. A pipeline run that crawls but hassave_to_kb=falsemust NOT bypass billing — wire the pipeline fetch through the sameWebCrawlCreditService(pre-check + charge oncrawls_succeeded) regardless of the KB-save branch, ideally recordingcharged_microson thePipelineRunfor idempotency. Otherwise non-KB pipeline crawls are free by accident. - Scheduling: a Celery Beat tick over
pipelines.next_scheduled_at, modeled on the automations cron selector (automations/triggers/builtin/schedule/selector.py— cron +FOR UPDATE SKIP LOCKED+ self-heal, reusing thecroniterutil), which fitsschedule_cronbetter than the connectorfrequency_minuteschecker. Plus the de-dup guard (a pipeline over a connector disables that connector's own periodic indexing) so the two minute-level scans never double-crawl/bill. - When
save_to_kbis off, persist the raw fetch result on the run (blob viafile_storage) so it is retrievable without indexing. - Chat agent context: expose pipeline run history to the
multi_agent_chatagent (read-only) — via a tool (e.g.list_pipelines/get_pipeline_runs) and/or a context middleware injection (similar toKnowledgeTreeMiddleware). Scope strictly to the active workspace. Gives the agent awareness of recent runs, statuses, schedules, and last-fetched timestamps.
Phase 7 — File upload as a pipeline + KB-save-secondary [subplan: 07-upload-pipeline-kb.md]
- Wire file upload (
documents_routes.pyfileupload+folder-uploadflows) to lazily get-or-create a singleton "Uploads" pipeline per workspace (connector_id = NULL,save_to_kb = true) and register aPipelineRun(trigger=upload); uploads alwayssave_to_kb = true. - Key design fact: uploads are route-recorded, not engine-executed — Phase 6's engine fails
connector_id IS NULLruns, so the upload routes themselves write a terminal auditPipelineRun(the existing upload code stays the executor). The run is an upload-event record; per-file ETL outcomes stay onDocument.status(accurate per-file roll-up needs the deferreddocuments.pipeline_run_idprovenance column). - A small migration adds a partial unique index
ON pipelines(workspace_id) WHERE connector_id IS NULL(enforces the singleton + race-safe get-or-create). - KB-save-secondary: the opt-in
save_to_kb+ destination folder for connector pipelines already shipped in Phases 5–6; Phase 7 only records the inverse invariant (uploads always KB) and guards the connector default staysFalse.
Deferred — Frontend & client phases (separate umbrella, planned LATER)
These are recorded for continuity but are NOT planned in this umbrella. They start once the backend phases above are working.
- Frontend rename + i18n: route segment
[search_space_id]->[workspace_id],search-space-settings/->workspace-settings/, TS types, api services, Jotai atoms, components, cache keys, and "Workspace" copy across 5 locales (messages/{en,zh,es,pt,hi}.json), plus frontend Zero schema rename. - Satellite/client apps + docs rename:
surfsense_desktop,surfsense_obsidian,surfsense_browser_extension,surfsense_evals, README/docs. - Connector two-type UI: restructure
connector-popupandconnector-constants.tsinto the two labeled types. - Pipelines UI + positioning: Pipelines section (list/create/configure/run-history/manual run), WebURL source-discovery UX, file-upload-as-pipeline surfacing, "coming soon" platform cards, "NotebookLM for Competitive Intelligence" copy.
Open items to confirm during subplanning
Rename transition: hard cutover vs temporary API aliasesRESOLVED: HARD CUTOVER (see resolved log + 02-rename-backend.md). The frontend is rebuilt against the corrected backend in its own umbrella; backend is verified via tests/OpenAPI, not the old UI.Whether existing connector periodic-indexing config is migrated into Pipelines or coexists during MVP.RESOLVED (Phase 5): COEXIST — connector periodic path stays untouched; pipelines add a siblingnext_scheduled_atscan. Phase 6 owns the de-dup guard (a pipeline over a connector disables that connector'speriodic_indexing_enabled) to avoid double crawl/bill. See05-pipelines-model.md.Chat agent run-history access: tool vs middleware injection vs both (default: tool).RESOLVED (Phase 6): tool — a read-only main-agentget_pipeline_runstool (registry pattern), workspace-scoped via its build-timesearch_space_id, opening its ownshielded_async_session()(likeKnowledgeTreeMiddleware). Always-on<pipeline_activity>middleware injection is deferred (per-turn token cost). See06-pipelines-exec.md.Type-2 MCP migration depthRESOLVED (Phase 4): branded natives are taggedMIGRATINGand turned OFF for MVP (not re-pointed to MCP yet); only the genericMCP_CONNECTORis a functional Type-2. Real MCP re-pointing is post-MVP.
Resolved decisions log
- Web search APIs (SearXNG/Linkup/Baidu): repurposed as source-discovery helper for the WebURL Crawler (suggest URLs for pipelines); not a standalone connector type.
- Tavily and Serper: REMOVED from the search infra. They are dropped as search providers entirely (not repurposed). Phase 4's source-discovery endpoint must build only on the remaining providers (SearXNG, Linkup, Baidu).
- Obsidian + Circleback: disabled for MVP.
- MCP-availability audit: BookStack, Elasticsearch, Luma all have MCP available -> eligible for Type-2 (so deprecated as
MIGRATING, notDISABLED). For MVP they are turned off pending the post-MVP MCP re-point, like the other branded natives. - Phase 4 connector taxonomy: modeled as a STATIC code registry (
connector_type-> category/availability), NOT a DB column — no migration in 04a;is_indexableis KEPT (orthogonal). Only the genericMCP_CONNECTORis a functional Type-2 for MVP; all branded natives (indexers + act-only, incl. Composio Gmail/Calendar) areMIGRATING(new-create blocked,/index+periodic+their subagents off, existing KB docs stay searchable). TheMCP_CONNECTORsubagent routing-map gap (constants.py) is fixed in 04a. - Phase 4 search APIs: all 5 enum values dropped (
SERPER_API/TAVILY_API/SEARXNG_API/LINKUP_API/BAIDU_SEARCH_API) in 04b. Survivors (SearXNG/Linkup/Baidu) become PLATFORM providers keyed by env (Linkup/Baidu keys move from per-connectorconfigto env — app-wide, not per-workspace). 04b carries a destructive migration deleting the 5 connector types' rows. - Phase 4 structure: split into 04a (taxonomy/gating/MCP-fix, no migration) and 04b (search repurposing + source-discovery endpoint, with migration); intended order 04a -> 04b (both orders safe).
- Rename transition policy: HARD CUTOVER of the external API (paths + JSON field names) in Phase 2 — no backward-compat aliases. Rationale: the frontend is (re)built against the corrected backend later, so there is no old client to keep alive; backend correctness is verified via the test suite + OpenAPI rather than the existing UI.
- WebURL Crawler framework: STANDARDIZE on Scrapling; remove Firecrawl entirely (no other scraping frameworks now or planned). Scrapling's
StealthyFetcherhandles Cloudflare; captcha-tools (deferred) covers the rest. - Crawl billing: reuse the existing credit wallet (
credit_micros_balance) with a newweb_crawlusage_type. Price: $1 / 1000 successful requests (1000 micro-USD per success). Connector/pipeline crawls bill the workspace owner; chat scrapes fold their crawl cost into the already-billed chat turn. Gated byWEB_CRAWL_CREDIT_BILLING_ENABLED(off for self-hosted); no DB migration required. - Billable unit: one unit per URL that returns usable extracted content, regardless of how many internal fallback tiers were attempted (not per HTTP fetch, not per URL-processed).
- Captcha solving (captcha-tools): DEFERRED to the last Phase-3 subplan (
03d); non-MVP-blocking. - Roadmap: WebURL Crawler & Crawl Billing inserted as the new Phase 3; connector two-type → Phase 4; pipelines → Phases 5/6/7.
- Phase 5 pipelines data model: two new tables
pipelines(mutable) +pipeline_runs(append-only), modeled onautomations/automation_runs; ORM lives indb.pynext to connectors/folders.connector_idnullable (NULL = Phase-7 Uploads), eligibility enforced at create via 04a'sis_pipeline_eligible. Schedule =schedule_cron+schedule_timezone(default UTC) +next_scheduled_at(cron, matching automations).pipeline_runspre-includescharged_micros/crawls_*/result_blob_keyso Phase 6 needs no extra migration. Both tables published to Zero full-row (like folders/connectors). Routes reuseCONNECTORS_*permissions. Phase 5 ships the data model + API surface only; the/runendpoint enqueues a Phase-6 task stub. - Phase 7 uploads-as-pipeline: a singleton "Uploads" pipeline per workspace (
connector_id NULL,save_to_kb=true), lazily get-or-created (race-safe via a partial unique indexON pipelines(workspace_id) WHERE connector_id IS NULL). Eachfileupload/folder-uploadrequest writes a terminal auditPipelineRun(trigger=upload, status=succeeded, documents_indexed=<accepted count>)— uploads are route-recorded, not engine-executed (Phase 6 fails NULL-connector runs by design; existing upload code stays the executor). Best-effort via an inner try/except (never 5xx the upload — the route's outer handler would otherwise 500 an already-committed upload). No crawl billing (uploads aren't crawls;charged_microsNULL). Per-file ETL truth stays onDocument.status; accurate roll-up needs the deferreddocuments.pipeline_run_idprovenance. Connectorsave_to_kbdefault staysFalse(opt-in for connectors, mandatory for uploads). Phase 7 also guards Phase-5's generic CRUD against the system Uploads pipeline:POST /pipelinesrejectsconnector_id=None(supersedes Phase 5's permissive create),/runand schedule-PUTreject NULL-connector pipelines, and Phase 6's scheduler_claim_duefiltersconnector_id IS NOT NULLas a backstop (so the Uploads pipeline can never be manually-run or scheduled into perpetually-failing runs). See07-upload-pipeline-kb.md. - Phase 6 pipeline execution: run engine mirrors automations (thin Celery
run_pipeline(run_id)→execute_pipeline_run; PENDING-gated, idempotent terminal no-op;pending→running→succeeded/failedwith timing/counts/error). MVP executor = WebURL crawler only (other types fail cleanly).save_to_kb=truereusesindex_crawled_urlsextended with afolder_idparam (lands in the destination folder);save_to_kb=falseruns a fetch-only loop and persists one JSON blob viafile_storage(result_blob_key). Crawl billing is owned by the run engine for the pipeline path (pre-check onlen(urls)+ chargecrawls_succeeded+ idempotentcharged_micros), calling the crawler with a newbill=Falseseam (the connector/index+periodic paths keep03c's in-indexerbill=True) — so non-KB runs are billed identically. Scheduler = apipeline_schedule_selectBeat tick modeled on the automations cron selector (cron +FOR UPDATE SKIP LOCKED+ self-heal, using the existingcroniterutil), plus the de-dup guard (a pipeline over a connector disables that connector'speriodic_indexing_enabled). Chat context = theget_pipeline_runstool. Carries a small additive05amendment: aschedule_timezonecolumn (cron util needs a tz). See06-pipelines-exec.md.
Subplan index (backend)
| Phase | Subplan file | Status |
|---|---|---|
| 1 | 01-rename-db.md |
drafted |
| 2 | 02-rename-backend.md |
drafted |
| 3 | 03a-crawler-core.md |
drafted |
| 3 | 03b-proxy-expansion.md |
drafted |
| 3 | 03c-crawl-billing.md |
drafted |
| 3 | 03d-captcha-solving.md |
drafted (deferred — last) |
| 4 | 04a-connector-category.md |
drafted |
| 4 | 04b-source-discovery.md |
drafted |
| 5 | 05-pipelines-model.md |
drafted |
| 6 | 06-pipelines-exec.md |
drafted |
| 7 | 07-upload-pipeline-kb.md |
drafted |
Frontend & client subplans will be added under a separate umbrella later (see "Deferred — Frontend & client phases").