Security

Built like the honesty is the product. Because it is.

The same discipline that keeps our numbers honest keeps your data safe: every control below exists in the system today, described plainly.

Row level security is forced on every tenant table

Every tenant table carries PostgreSQL row level security with an org isolation policy, and forces it, so the policy binds whoever queries the table rather than everyone except the table's owner. Two tests hold that, and our honesty ledger records both by name: rlscoverage.test.ts reads the migrations on every commit, so a new tenant table cannot ship unforced, and rlsforce.live.test.ts reads the database catalogue on every live run and proves, on a scratch table it creates and drops, that forcing is the thing that binds an owner. Exactly one table is exempt and it carries its reason in the code: the beta grant ledger has no tenant policy at all, because the application role is revoked from it outright, which is the stricter lock. Reads and writes run inside an org transaction that takes the application role first, and the policies bind there: a missing WHERE clause inside it cannot reach another org's rows. The limit we would rather state than hide is the connection itself. It is still an owner connection with superuser rights, and a superuser reads past row level security whether or not it is forced, so today the isolation binds inside the transaction rather than on the connection. Forcing is what keeps it binding the day that connection is hardened, and the same test measures the connection and fails when it changes.

API keys are org-scoped bearer tokens

Keys are created in the dashboard, resolve server-side to exactly one org, and every read runs inside that org's row-level security context. A brand id from another org reads as not found. Programmatic access is a plan feature, gated at the endpoint.

Outbound fetches are SSRF-guarded

Every fetch of a customer-supplied host (robots checks, crawler probes, page audits) goes through one guarded client: DNS-pinned dispatch, private and link-local address ranges refused, redirects re-vetted hop by hop, response sizes bounded.

The system drafts, your workspace publishes

Nothing reaches your site on its own, and there is no code path that publishes to your site without an approval. The only code that writes to a connected CMS runs on a request from your workspace, and a live publish now has to pass four locks: the placement must have reached its approved status through the approval decision itself, so a status set by any other route authorises nothing; a fix you tell us you shipped elsewhere is refused by name, because it carries no text anyone approved; the body that goes live is read from the approved revision and hashed again and checked against the hash the approval was taken against, never taken from the request; and the request may not carry a title or a body at all. Only an owner or an admin can publish live, and only an owner or an admin can delete a client or a branch. Every push writes a receipt either way, and placements are idempotent with an atomic claim, so a retry cannot post the same thing twice. This paragraph said the opposite three weeks ago, and it was corrected rather than defended: recording a fix shipped elsewhere used to mint a placement a live publish accepted. Two tests hold what replaced it, on every commit and every live run: publishgate.live.test.ts walks each lane the audit found, against a stand in CMS that records every post, and rolematrix.test.ts fails when any route that can publish or delete ships without a role behind it.

Review text is never stored

Review analysis runs in flight: text enters the extractor and only derived numbers persist (text share, term density, recency, response rate). Google Business Profile reads persist a completeness diagnosis, field flags and gap names, never profile content.

Spend has a hard ceiling

Every metered operation writes to a per-org cost ledger, and a monthly budget kill switch halts new spend at your plan's cap. Reads stay open when spend is paused.

Rate limiting on every public surface

Authentication endpoints run a database-backed limiter that survives restarts and scales across instances. Public tools and the funnel run per-IP sliding windows. The MCP endpoint bounds body size before parsing and rate-limits before key resolution.

Secrets stay out of the codebase

Credentials live in the deployment environment, never in code or config files. Provider keys are least-privilege, and the git history has been audited for leaked material.

Sub-processors

The vendors we pay to run Ansengine, and what each one is for. The accounts you connect yourself are in the second table, because data reaches a third party there too.

RailwayAPI, worker and PostgreSQL hosting
VercelWeb application hosting
InngestDurable job orchestration
Anthropic, OpenAI, Google, xAIThe AI engines measured; prompts contain your tracked questions, never your customer data
OpenRouterRoutes the Perplexity engine we measure
DataForSEOSearch, crawl and business panel data, and the consumer answers we read by scrape rather than by API
SearchAPISearch results and consumer answers (fallback provider)
FirecrawlRendered page extraction for readability checks
MillionVerifierChecks that an outreach address is deliverable before anything is sent to it, so it receives that address
ComposioBrokers the OAuth grants you make to your own accounts, listed below
ResendTransactional email
PolarBilling and payments
GitHubRuns the nightly database backup and keeps that dump as a build artifact for seven days
Cloudflare R2Holds the same nightly database dump off GitHub, so one blocked account cannot take the backups with it
SlackWhere our own error alerts are posted by webhook; an error message can carry a value it interpolated

Accounts you connect

Your own grants, made by you and revocable from Settings at any time. An outgoing webhook is not on this list because it has no fixed vendor: point it at Zapier, at Make or at your own endpoint and the event goes where you pointed it.

Google Search Console, Google AnalyticsRead only: demand, impressions and AI referral sessions from your own properties
Google SheetsAppends your lead ledger to your own spreadsheet when you ask it to
GmailSends outreach from your own mailbox, so the From line is yours; every send is an explicit action
HubSpot, SalesforceWrites a lead into your own CRM, carrying the AI source it reported
Reddit, LinkedIn, X (Twitter)Disclosed placements posted from your own account, after your approval
WordPress, Webflow, ShopifyPublishes an approved draft into your own site or store; Shopify needs an OAuth app we have not published yet
SlackYour own incoming webhook, pinned to hooks.slack.com, for lead alerts and the daily brief in your channel

Report a vulnerability or ask a security question: kailesk@houseofmvps.com. We respond fast and we credit researchers who report responsibly.