# AltoRank: open-source, self-hosted AI SEO content writer (Outrank alternative) **An open-source alternative to Outrank and Jasper.** AltoRank researches a keyword, writes the article, scores it and fact-checks it: a figure that cites a page is looked for on that page, and one with no source is flagged for the reviewer. Every draft can be copied as Markdown (front matter included) or HTML, or downloaded as a `.md` file, for you to paste wherever you publish. Approve it, paste it, then **Mark as published** records the live URL, and the article is tracked from there: indexing and rank. AltoRank never publishes to your site without an approval recorded under a named person. Approve each draft by hand, or set a rule that approves drafts after a hold unless you hold them (the rule publishes through a CMS connection, which is built but not yet verified end to end; see [Status](#status)). Agents can research and draft through the MCP server and the agent API. Neither can approve, and neither can publish anything a person has not already approved (the one publish-side call retries a failed publish of an approved article). Self-host it free under AGPL-3.0, or use the hosted app at `app.altorank.co`. Open source, the whole product. No feature-gated tier, no `ee/` directory. --- ## Status The hosted app at `app.altorank.co` is this repository, deployed from `main` ([docs/deploy.md](docs/deploy.md)). A self-hosted install runs the same code against your own Supabase and your own API keys. Research, drafting, scoring, fact-checking, audits, rank tracking and Search Console / Bing sync run today; the table below has the detail. What is not finished yet: - **No paying customers, no case studies.** Nothing here has a growth figure attached to it, and that is on purpose. - **CMS publishing is built but not yet verified end to end.** The thirteen adapters are in `apps/web/lib/cms/` and the connection form covers all of them, but none has been watched publishing to a live site yet, so the platform tiles on `/connect` ask you to request an integration instead of offering Connect. Every draft can be copied as Markdown or HTML today. Details under [What works today](#what-works-today). - **AI visibility tracking is built but not switched on.** The probe, the daily cron and the `/geo` page exist, but nothing in the app turns tracking on for a workspace or adds the prompts it measures, so the sidebar marks it "soon" (`apps/web/lib/constants.ts`). - **The CLI is publishable, not yet on npm.** `packages/altorank-cli` bundles `apps/web/scripts/cli.ts` into an `altorank` binary (`npx altorank auth whoami` once it is published); until then, `npm run cli` from `apps/web` drives the same agent API from a checkout. `skills/altorank/SKILL.md` is the skill file a coding agent reads, and it does install with `npx`; see below. Whether you want to read how it works, run it yourself or send a patch, everything is here. ## The publishing decision One gate, two ways through it. `lib/publishing/core.ts` refuses any article that is not `approved` or `scheduled` with `approved_by` set. Only the server writes those columns: a signed-in user's own database client cannot make an article approved, scheduled or live (migration 109), and an edit to approved text sends it back to review. What can write an approval: - **A person**, from the editor (`approveArticle`), recorded as `approved_by`. The button checks for an active plan and refuses a bare, unsourced figure. - **A rule a named member of the account set** (`auto_approve`, migration 079): the publish cron runs stricter checks than the Approve button - active plan, somewhere to publish, no unsourced figure, no named reviewer still to sign off, no failing audit item, SEO score at or above the floor - after a hold window (default 24h) during which the drafted email carries a Hold link. `approved_by` is whoever turned the rule on; `approval_kind` says `auto`. Held drafts carry the reason on their own row. See `lib/publishing/auto-approve.ts`. `live` is written after an approved publish, by **Mark as published** on an approved article, and by one thing that is not a publish: the nightly found-on-site check (`lib/found-on-site/detect.ts`) marks a draft live when its text is already on the customer's own site. That records something done outside AltoRank, sends nothing to a CMS, carries no approval, and can be undone from the editor. What cannot write an approval: an agent. **No MCP tool or agent API call approves an article or publishes a draft** - not disabled, absent. The one publish-side call, `altorank_retry_publish` (`POST /articles/{id}/retry-publish`), re-runs a failed publish of an article a person already approved, and is refused for anything else. The MCP tools are all registered in `apps/web/lib/mcp/server.ts`, and its server instructions say so to the agent. Competing tools ship the words "you stay in control" as copy. The difference is checkable here: every `live` row is an approved article (by click or by rule) or one found already published on the customer's own site. ## What works today | | | |---|---| | Keyword research + SERP analysis | DataForSEO | | Domain audit | 9 readiness checks, crawl, PageSpeed | | Article generation | research → draft → score → fact-check | | Brand voice | per-workspace voice profiles | | Publishing | Markdown and HTML export of every draft; **13 adapters** (below), built but not yet verified end to end | | Locales | **35** (`apps/web/lib/seo/locales.ts`) | | Rank tracking | scheduled SERP checks | | Search analytics | Google Search Console; Bing Webmaster Tools (clicks and impressions per day) | | AI visibility | **not switched on yet**: built (`lib/geo/`, the `/api/cron/geo` sweeps, the `/geo` page) to sample whether AI answers name you and who they name instead, but nothing in the app turns it on for a workspace yet | **Publishing destinations** (`apps/web/lib/cms/adapter.ts`) — thirteen adapters covering **ten CMS platforms**; WordPress is reachable two ways, and `git` and `webhook` are publishing targets rather than CMSs. Framer, Ghost, git, HubSpot, Magento, Notion, Shopify, Webflow, webhook, Wix, WooCommerce, WordPress, and the WordPress plugin — a second, recommended route to WordPress that installs a plugin and takes a per-site token instead of an application password (`apps/web/lib/cms/wordpress-plugin.ts`), which is why WordPress appears twice. None of the thirteen has yet been verified end to end against a live site, so `CONNECTABLE_CMS` in `apps/web/lib/cms/connectable.ts` is empty on purpose and every CMS tile on `/connect` offers "Request integration" instead of Connect; adding an id to that set brings that tile's Connect button back. The connection form itself is unchanged and still covers all thirteen: the page's "New connection" button, a draft's publish panel and `/connect?connect=` all open it. Without a connection, the editor copies any draft as Markdown (front matter included) or HTML, or downloads it as a `.md` file. ## Install the plugin Claude Code. From a terminal: ```bash claude plugin marketplace add AltoRank/altorank claude plugin install altorank@altorank ``` Or inside a Claude Code session: `/plugin marketplace add AltoRank/altorank`, then `/plugin install altorank@altorank`. `plugins/altorank/` bundles the hosted MCP server (`https://app.altorank.co/api/mcp`, OAuth, no API key to copy) with a skill written for those tools. The same folder carries the portable `plugin.json` + `mcp.json` that ChatGPT and Codex read, and `.agents/plugins/marketplace.json` lists it for them. ## Install the skill ```bash npx skills add AltoRank/altorank ``` That puts `skills/altorank/SKILL.md` where Claude Code, Cursor, Codex, Copilot, Windsurf, Gemini CLI, Cline and the other agents the [skills CLI](https://github.com/vercel-labs/skills) supports will read it. The skill teaches an agent to drive the agent API (`/api/agent/v1`): preflight the key, check a site's readiness, list or suggest keywords, generate a draft into a human's review queue, edit a draft by find-and-replace, move the content plan. It never approves, deletes or publishes a draft; the API has no such calls. Its one publish-side call retries a failed publish of an article a person already approved. It needs an API key, created at `/settings/api-keys` in the dashboard and exported as `ALTORANK_API_KEY`. Self-hosted installs also set `ALTORANK_BASE_URL`. `skills/altorank/commands/` holds five thin slash commands for the common calls; the skills CLI does not register slash commands, so copy them into `.claude/commands/` (Claude Code) or your agent's equivalent if you want them. ## Running it Requires Node 22+, and a Supabase project (local via Docker, or hosted). The full deployment story, hosted and self-hosted, is in [docs/deploy.md](docs/deploy.md); the Cloudflare assessment is in [docs/deploy-cloudflare.md](docs/deploy-cloudflare.md). ```bash npm install cp docker/.env.example apps/web/.env.local # every variable, with what each one enables npm run dev ``` Required before a self-hosted install works end to end: - your Supabase URL, its anon key and its `SUPABASE_SERVICE_ROLE_KEY` - `NEXT_PUBLIC_APP_URL`, the address people open the app at. Every emailed link (signup confirmation, invites, password reset) is built from it, and the Docker build bakes it into the image, so set it before `docker compose up --build`; compose refuses to build without it. - `ANTHROPIC_API_KEY` - `ENCRYPTION_KEY` and `CRON_SECRET` (`openssl rand -hex 32` for each). `docker compose` refuses to start without `CRON_SECRET`, and without it no scheduled job runs. - `RESEND_API_KEY` and `RESEND_FROM_EMAIL` (a sender on a domain verified in your Resend account). Signup confirms by email, so without them nobody can create an account and nobody can reset a password. Add `DATAFORSEO_LOGIN` / `DATAFORSEO_PASSWORD` for keyword volume and rank tracking; without them those steps skip rather than fail. Everything else in the example file is optional and each one disables exactly one feature. The file says which. Product analytics (the two `NEXT_PUBLIC_POSTHOG_*` variables, project token and host) are optional too: without them the browser console says analytics are off and nothing else changes. Other entry points: ```bash npm run test # vitest, unit tier: mocks only, needs no services npm run test:db # vitest, db tier: *.db.test.ts against the local Supabase only npm run smoke # research → prompt → model → fact check, against real APIs ``` and from `apps/web`: ```bash npm run mcp # MCP server over stdio, for driving the engine from an AI assistant npm run cli -- --help # the agent API from a shell; auth with ALTORANK_API_KEY npm run readiness -- # the agent-readiness checks on their own ``` ### Hosted MCP endpoint The same tools are served at `/api/mcp` (Streamable HTTP) by the running app, so an MCP client adds one URL as a connector instead of running the stdio server: `https://app.altorank.co/api/mcp` on the hosted install, or your own origin when you self-host. Authentication is a bearer token, either an API key from `/settings/api-keys` or the token the built-in OAuth flow issues: - discovery at `/.well-known/oauth-authorization-server` and `/.well-known/oauth-protected-resource/api/mcp` - dynamic client registration at `/api/oauth/register` (public clients, PKCE S256 only) - consent at `/oauth/authorize`, owner or admin only, write scope opt-in - token at `/api/oauth/token`, authorization-code grant, no refresh token The token a connector receives is an `api_keys` row (migration 080), so it is listed, expires and is revoked on `/settings/api-keys` like any other key. Claude Code registers it with `claude mcp add --transport http altorank https://app.altorank.co/api/mcp`; ChatGPT and Claude.ai take the same URL as a custom connector. Neither the CLI nor the MCP server can approve an article or delete anything — not by configuration, but because the agent API has no such call and no `DELETE` handler at all. Nor can either of them publish: the single endpoint that reaches a CMS is `POST /articles/{id}/retry-publish`, which re-runs a publish that **a human already approved** and that then failed. An article in draft or review is refused, and the refusal tells the caller to hand it to a person (`apps/web/app/api/agent/v1/articles/[id]/retry-publish/route.ts:13-35`). ### Database `apps/web/supabase/` carries a `config.toml` and the numbered migrations, which apply in order. With the [Supabase CLI](https://supabase.com/docs/guides/cli): ```bash cd apps/web supabase start # local Postgres + auth; applies the migrations, prints the URL and keys for .env.local ``` `supabase start` applies every migration to the local database on first start (`supabase db reset` re-applies them from scratch). Do not run `supabase db push` for a local stack: it targets the linked remote project. Against a hosted project, `supabase link --project-ref ` first, then `supabase db push`. The app will not get far without the migrations: every query targets tables they create. `docker/` has a container setup if you would rather not run Node directly. ## Layout ``` apps/web/ the engine and dashboard (Next.js) app/(dashboard)/ dashboard routes lib/audit/ domain analysis, readiness checks lib/seo/ recommendations, scoring, locales, topical profile lib/content/ generation (one implementation, shared by route + cron) lib/ai/ fact checking lib/cms/ 13 publishing adapters lib/geo/ AI-answer visibility scripts/mcp.ts MCP server scripts/cli.ts CLI over /api/agent/v1 plugins/altorank/ Claude / ChatGPT / Codex plugin: hosted MCP server + skill skills/altorank/ the skill file a coding agent reads (`npx skills add AltoRank/altorank`) packages/altorank-cli/ npm package `altorank`: bundles scripts/cli.ts into a bin docker/ container setup for self-hosting tools/agent-readiness/ standalone agent-readiness scanner ``` ## Licence **GNU AGPL-3.0-only.** Full text in [LICENSE](LICENSE). In practice: self-host it, run it for your own clients, commercially, and you owe nothing and publish nothing. The one obligation only reaches people rebuilding the product. If you modify the source and offer that modified version to others over a network, your changes have to be shared back under the same licence. Running an account on it is free and unencumbered. Relaunching a modified copy as a closed competing service is not. ## Contributing Issues and pull requests are welcome. Two things that will get a patch rejected regardless of how good the code is: - Adding a way to publish that is not attributable to a person - a click, or a rule a named member set and can veto. An agent-triggered publish is the canonical example. - Adding a claim the repository cannot support. If `grep` cannot find the feature, the README does not get to mention it. The code conventions a patch is read against, and how this repository relates to the marketing site, are in [CONTRIBUTING.md](CONTRIBUTING.md).