Turn off upstream telemetry today
self-host-telemetry.ts holds a hard-coded PostHog key for the upstream project and skips sending only in hosted mode. Our team-mode instance appears to send anonymous heartbeats to them unless opted out.
Turn our internal fork of OpenSEO into a product agencies buy instead of Semrush: client workspaces, white-label reports, usage-based billing. This page is the working plan for the team taking over.
What the product is, where it stands, and the one architectural fact that shapes everything.
src/client/styles/app.css)AUTH_MODE=team: owner-provisioned users, roles, shared workspacesrc/server/features/activity/)/p/$projectId/report)The codebase has four auth modes, and none is "an agency with many clients". team is a single shared workspace with no billing and no self-serve signup. hosted has orgs, roles, invitations and per-org credit billing, but it is wired to the upstream author's vendors (Autumn and Stripe, Loops email, PostHog, Cloudflare KV). The plan is to build the agency product on top of hosted mode, running it on our own accounts, and to run team-mode instances for pilot agencies while that work happens.
Read first: CLAUDE.md (engineering rules), specs/0002 (DataForSEO metering), specs/0011 (multi-user orgs), docs/SELF_HOSTING_TEAM_MODE.md, docs/LOCAL_DEVELOPMENT.md.
The plan below assumes the recommended answer. If any changes, the affected phase is named.
| Decision | Assumed answer | If it changes |
|---|---|---|
| Architecture | recommended Multi-tenant hosted mode, deployed to Cloudflare with Postgres. Per-agency team-mode instances only for the pilot. | Per-agency instances forever: drop P2.4–P2.6, add instance provisioning automation. |
| Branding depth | Agencies white-label the reports and client view. The app shell stays CrawlerX. | Full white-label (custom domain, app shell): add P3.1. |
| Upstream | Hard fork. Cherry-pick security fixes and DataForSEO changes only. | Track upstream: do P0.5 first and keep every change additive. |
| Pricing | Platform fee per agency, client seats, plus credit packs for data usage. | Pure usage: skip client seats in P2.4. |
| Team size | Planning assumption: two full-stack engineers, one part-time devops/QA. | Scale the timeline; the phase order does not change. |
Long-lead items (amber) start in week 0 because they gate launch, not because they are big.
Nothing customer-facing. Remove the things that would hurt us the moment a second customer appears.
self-host-telemetry.ts holds a hard-coded PostHog key for the upstream project and skips sending only in hosted mode. Our team-mode instance appears to send anonymous heartbeats to them unless opted out.
Customer support requests currently point at the upstream author's email. Links send users to the upstream product, Discord and docs. Create one src/shared/brand.ts (product name, support email, docs and terms URLs, crawler contact URL) and make every reference read from it.
Production today appears to run vite preview under pm2 (the start script), which emulates the Cloudflare runtime locally. That is fine for an internal tool, not for paying customers. package.json already has deploy:postgres (Alchemy, hosted-prod stage, Postgres). Prove it works end to end on our own Cloudflare account, in a staging stage.
Search Console and GA4 scopes are sensitive. Until Google verifies the app, external users see an unverified-app warning and are capped. Verification takes weeks and needs a live privacy policy, a homepage on our domain and a demo video. Separately, get written confirmation that DataForSEO allows us to resell their data in a paid product.
Every server function must scope by the caller's organisation (see specs/0001). Run /security-review, then write the test that would catch a leak: two orgs, and each server function called with the other org's projectId must fail.
Enough for three to five design-partner agencies to put their own clients on it. MetatronCube itself is design partner zero.
New table for per-organisation branding: display name, logo, primary colour, footer text. Logo stored in R2. Applied to reports and to the client view, not to the app shell.
ProjectReportService covers GSC, GA4 and the latest audit. Add rank tracking movement, a backlinks summary and AI visibility. Each section already degrades on its own when an integration is missing; keep that. Export: start with a print-optimised route (@media print, A4) since it is the cheapest path. Move to server-side rendering (Playwright is already a dev dependency) only if print output is not good enough.
Store a snapshot of the report JSON at share time so a client sees stable numbers and cannot trigger spend. Public route addressed by a random token (store only its hash), with expiry and revoke. Log create and revoke in the activity log.
One screen listing every client project with the few numbers an account manager scans: rank movement over 7 and 28 days, latest audit health, GSC clicks change, integration status (connected, expired, missing). Sort by "needs attention".
Invitations, password reset and verification currently go through Loops templates that belong to the upstream account. Put a small provider interface in src/server/email and implement it with Resend or SES. Replace billing/loops-sync.ts contact sync (remove, or point at our own tool).
Many client sites sit behind Cloudflare Managed Challenge or DataDome, which the crawler cannot pass. Give agencies something to allowlist: a stable egress IP for the crawler and a user-agent that names us and links to a page explaining it. Publish a short help page with per-WAF instructions. Keep the DataForSEO OnPage fallback as the paid option.
Until self-serve exists, provision each pilot by script: create org and owner, apply branding, set an invoice reference. Set up production basics: nightly Postgres backup with a tested restore, error tracking, uptime check and a status page, and a shared support inbox.
What turns a pilot into something agencies pay for monthly without hand-holding.
The member role exists but is not invitable: spec 0011 blocks it in beforeCreateInvitation, and every invitee is an admin. Add a client role that is read-only and limited to named projects. Model access as a project_member table (userId, projectId), not a JSON list. Enforce it in the project-auth middleware and the MCP layer.
Per-project schedule (weekly or monthly), recipients, and whether to attach the PDF or send the share link. A cron creates a snapshot (P1.3) and sends it. Cron hooks already exist in the scheduled handler in src/server.ts and wrangler.jsonc.
Notify on rank drops above a threshold, audit score regressions, GSC clicks falling, and expired Google connections. Hook into scheduledRankChecks.ts after each run. Preferences table per user and project. Email first, in-app feed second.
Credits are pooled per organisation. Agencies need to know what each client costs, and to stop one client burning the pool. Record projectId on every usage event, show usage by client on the billing page, and add an optional monthly cap per project checked in the same preflight as the balance check.
Create our own Autumn and Stripe accounts and define the agency plans (platform fee, client seats, credit packs). The 1.28 markup is a constant in src/shared/billing.ts: make it configuration, not a code edit, so pricing can change without a deploy. Stripe checkout already collects business name and tax ID.
Signup flow adapted for agencies (agency name, first client project, connect Google), a free-trial policy with a spend ceiling so trials cannot burn DataForSEO money, terms and privacy pages, a pricing page. Rework the onboarding chat copy from "personal SEO tool" to "agency workspace".
Do not start these until paying agencies have asked. Each is a real project.
| ID | Item | Note |
|---|---|---|
| P3.1 | Custom domain and full white-label | Per-org hostname (Cloudflare for SaaS), branded login and emails. |
| P3.2 | Agency MCP and API | MCP and skills exist in hosted mode. Publish our own skill pack and per-org API keys. |
| P3.3 | Looker Studio connector or CSV export | Agencies keep their own dashboards. Today only export to Google Sheets exists. |
| P3.4 | Security hardening | 2FA, SSO for larger agencies, audit-log export, DPA template. |
| P3.5 | Local SEO depth | Local rank grids and Business Profile data, if agencies serve local businesses. |
These come from CLAUDE.md. New work is reviewed against them.
npm run db:generate and keep the parity test green.fetch*Raw call in feature code is a billing bypass.pnpm ci:check (prettier, knip, tsc, oxlint) and pnpm test..agents/PAPERCUTS.md..greptile/, AGENTS.md, CLAUDE.md and .github/ need maintainer review.| Risk | Why it matters | Mitigation |
|---|---|---|
| DataForSEO cost overrun | We pay raw cost. Trials, rank tracking and large audits can burn money faster than revenue. | P2.4 caps, trial spend ceiling in P2.6, alert on account balance, weekly review of pnpm billing:usage. |
| Google verification delay | Blocks external GSC and GA4, which the report depends on. | Submit in week 1 (P0.4). Pilots can use their own Google Cloud client in the meantime. |
| Reseller terms | If DataForSEO disallows resale, the model needs a different arrangement. | Ask before building billing (P0.4). |
| Cross-tenant data leak | Fatal for trust, with agency clients' data. | P0.5 test suite, and a review of every new server function. |
| Blocked crawls | Site audit fails on many client sites. | P1.6 allowlist and identity, paid OnPage fallback. |
| Upstream drift | Upstream ships almost daily. Our rebrand touched 42 files. | Hard fork; brand constants in one file (P0.2); cherry-pick security fixes only. |
| Runtime not production-grade | Internal deployment runs a preview server under pm2. | P0.3 decision; do not put paying agencies on it. |
Access the new team needs on day one, and what they should read.
docs/LOCAL_DEVELOPMENT.md)CLAUDE.md, specs/0001, 0002, 0011pnpm test and pnpm ci:check green.agents/PAPERCUTS.md for known friction