The surface check looks at your app the way any visitor’s browser does — from the outside, without logging in — and turns what it sees into four scores and a short list of findings. It takes a few seconds and never touches your data.
What the check does#
- One address at a time, and only when the owner (or someone they authorise) asks for it.
- Only what is already public: the home page, the JavaScript files it loads, response headers, public DNS records and the TLS certificate.
- Nothing is changed and nothing we find is used: no logins, no keys tried, no data read from your database.
- Four separate scores: Security, Performance, Accessibility and SEO. We do not blend them into one number, so you can see where the problem is.
Who can ask for a check#
Anyone can run the surface check, but only on an app they own or are authorised to have analysed. Before the check starts, you tick a box confirming exactly that; without it, nothing runs. We record that confirmation, the version of the text you accepted and the time.
We keep the surface check deliberately light, so that a false declaration cannot cause harm: it reads only what any browser already receives, accepts at most 3 checks an hour against the same site, whoever asks (except the owner, once ownership is proved), respects the site’s robots.txt, and identifies itself openly. Anything that goes beyond the public surface needs more than a tick: the deeper check runs only after the owner has proven control of the site, and the full audit only with the owner’s explicit authorisation.
What happens during a check#
- Your request is validated. The address must be a public website on the standard web ports; private networks and internal addresses are refused.
- We read the site’s
robots.txtfirst. If it asks automated tools not to read the home page, we stop there and tell you. - We load the home page and the site’s own JavaScript files that the page declares — up to 40 files, within a total of 20 seconds and 20 MB for the whole check. Scripts from other companies are not downloaded.
- On your own domain, we look up public records: the TLS certificate and the DNS records that protect your email (SPF, DMARC, MTA-STS, TLS-RPT) and your certificates (CAA). On an app builder’s own subdomain these belong to the platform, so we skip them.
- We calculate your scores and show you the report. The check keeps only the results, never the content of your pages.
The surface check asks only for what a browser loads when it opens your home page for the first time. It never asks whether files such as /.env, /.git/config, your source maps, /sitemap.xml, /.DS_Store, /.htpasswd or your API documentation exist, and it never asks your server how it answers other websites (CORS): no browser asks for them, and ticking the box does not prove that the site is yours. Those questions belong to the deeper check, after the owner has proven control of the site. There we read only the answer code and headers, never the file itself, and if one of those files seems to be there we also ask for an address that cannot exist, so a site that answers every address the same way is not reported by mistake.
What we never do#
- Read, copy or store data from your database.
- Use a key, password or token we find. We report that it is visible; we never try it.
- Download configuration files, source code or source maps. The one exception is the deeper check on a site whose owner has proven control of it and ticked a separate box for it: there we read the source maps in memory, only to point to the file and line of an exposed key. We keep the key type, the file, the line and a shortened fragment of the key, never the code and never the whole key. If the owner ticks another separate box, we use the same in-memory code to measure unused code and libraries included more than once. There we keep file paths (without user names), library names and versions, and sizes in bytes, never the code.
- Guess file or table names, or crawl other pages. The one exception is the accessibility check on rendered pages, part of the deeper check on a site whose owner has proven control of it and ticked a separate box for it: there an isolated browser opens at most 10 pages of that exact hostname — the home page and the pages it links to, up to 2 links away, each one allowed by
robots.txt— and runs their JavaScript to see what a visitor sees. It reaches no other site, sends only page reads (no forms, no uploads, no messages), and we keep only page paths and measurements, never the content. - Record the network requests your pages make. The one exception is the network check, part of the deeper check on a site whose owner has proven control of it and ticked a separate box for it: there an isolated browser, carrying the same name, opens the home page of that exact hostname once, if
robots.txtallows it, runs its JavaScript and records the requests it makes in the first seconds after loading, without clicking anything. It reaches no other site — requests to other services start and stop there, and scripts from other companies do not load — and it sends only page reads (no forms, no uploads, no messages). We keep methods, addresses without their values, the names of parameters and headers, third-party hosts, and whether a value looked like personal data; never the values and never the content. - Log in, submit forms or change anything.
- Send details of your site to other companies. The one exception is the deeper check on a site whose owner has proven control of it, and only for the boxes they tick, each one separately: the address of the site to a public dataset of real-visitor speed, the domain name to the registry of its extension (registration and expiry), the hostname to the browsers’ public HTTPS list, the name and exact version of the libraries we recognise (today Next.js and Angular) to an open database of security advisories (known vulnerabilities), the domain name to a public database of known data breaches, and the domain name, or the full address on a builder’s subdomain, to a public code-hosting platform (keys in public code). Each one receives only that; the privacy policy says what we keep of each answer.
- Hide who we are: every request carries the name
Root92-MigrationScannerand a link to our “About the scanner” page.
Your scores explained#
| Score | What it tells you | How it is calculated |
|---|---|---|
| Security (0–100) | How much an outsider could exploit, and how tied the app is to its platform. | Starts at 100. Each problem found removes a fixed number of points (see the list below). It never goes below 0. |
| Performance (0–100) | How heavy and how fast your home page is. | Five measurements, each scored 0–100 and weighted: server response time 40%, JavaScript size 30%, number of JavaScript files 10%, HTML size 10%, server-side rendering 10%. |
| Accessibility (shown as issues) | Common barriers for people using screen readers or keyboards. | Six checks on the page markup, weighted by how severe the barrier is. The report shows how many failed. |
| SEO (shown as issues) | Whether search engines can index your home page and present it well. | Thirteen checks; each problem removes points from 100. The report shows how many were found. |
- “Score ≤ N” means we could not finish reading your app within the time or size limit. Anything we did not read may hide more problems, so your real score can only be lower.
- “Not measured” means the check could not run — usually because the page is built entirely in the browser, so its HTML is empty until JavaScript runs.
- The SEO checks are the exception: they read the HTML exactly as your server sends it. On a page built entirely in the browser, a missing
<h1>can therefore be reported even when the finished page shows one. - Apps on an app builder’s subdomain: domain, email and certificate settings belong to the platform, not to you, so we do not count them against you.
- No score is shown if we could not reach the site, or if its
robots.txtasked us not to read it. The surface check does not ask for your contact details, so we will not get in touch on our own: if you want us to look at it, write to us using the “Contact us” form on the home page.
What we check#
The surface report shows the most important findings straight away. Other findings are counted in your score and listed in the full audit.
Security — platform and backend
| Check | What we look at | Points | Shown |
|---|---|---|---|
| Built with a site builder | Signs of Lovable, Bolt, v0, Replit, Base44, Framer, Webflow, Wix, Squarespace, Shopify, WordPress, Bubble, Softr, Glide, Retool or another AI app builder | 12 | Surface report |
| Hosted on a platform subdomain | The app answers on the builder’s own address (counted instead of the previous line, not in addition) | 12 | Surface report |
| Supabase backend | A Supabase address in the page or its scripts | 16 | Surface report |
| Firebase backend | A Firebase address in the page or its scripts | 16 | Surface report |
Security — exposed keys and files
| Check | What we look at | Points | Shown |
|---|---|---|---|
| Database service key in the browser | A Supabase key with full administrative rights inside your public code | 45 | Surface report |
| Database key in the browser | Your public Supabase key. It is safe only if row-level security protects every table | 34 | Surface report |
| Other secret keys in the code | Secret keys for services such as Stripe, AWS, OpenAI, Anthropic, GitHub, Slack, SendGrid, Twilio or Google, or private certificates | 40 | Surface report |
| Database or service credentials in a URL | An address with a username and password (user:password@host) for a server reachable from the internet | 40 | Surface report |
| Email service key in the code | An API key for Resend, Mailchimp or Brevo | 40 | Surface report |
| Payment system token in the code | A Square access token or OAuth secret | 40 | Surface report |
| AI service key in the code | An API key for Hugging Face, Replicate, Groq, OpenRouter, xAI or Perplexity | 40 | Surface report |
| Code repository token in the code | A GitLab, npm or Bitbucket token | 40 | Surface report |
| Cloud credential in the code | A DigitalOcean token or an Azure storage account key | 40 | Surface report |
| OAuth secret in the code | A Google OAuth client secret | 40 | Surface report |
| Store admin token in the code | A Shopify admin or app token | 40 | Surface report |
| Work tool token in the code | An Airtable, Notion or Linear API token | 40 | Surface report |
| Chat webhook in the code | A Slack or Discord webhook, or a Telegram bot token: anyone could post as you | 12 | Full audit |
| Internal address in the code | A full address to a private IP or an internal name (localhost does not count: many libraries ship it by default) | 1 | Full audit |
| Unencrypted API or WebSocket | An API called over http:// or a WebSocket opened over ws:// | 2 | Full audit |
.env file reachable | Whether your environment file can be downloaded by anyone. Checked only in the deeper check, after you prove the site is yours | 40 | Full audit |
Plain http:// not sent on to https:// | Your home page still answers over unencrypted http:// instead of redirecting. Checked only in the deeper check, after you prove the site is yours | 3 | Full audit |
.git folder reachable | Whether your code history can be downloaded by anyone. Checked only in the deeper check, after you prove the site is yours | 40 | Full audit |
| Public source maps | Whether the original source of your scripts can be downloaded. Checked only in the deeper check, after you prove the site is yours | 8 | Full audit |
Password file reachable (.htpasswd) | Whether your web server password file can be downloaded by anyone. Checked only in the deeper check, after you prove the site is yours | 12 | Full audit |
Folder metadata file reachable (.DS_Store) | Whether a macOS file listing your folders can be downloaded by anyone. Checked only in the deeper check, after you prove the site is yours | 2 | Full audit |
| API documentation publicly reachable | Whether an OpenAPI or Swagger file describing your API answers at its default address. Checked only in the deeper check, after you prove the site is yours | 2 | Full audit |
Security — browser protections
| Check | What we look at | Points | Shown |
|---|---|---|---|
| Insecure cookies | Cookies set without the Secure, HttpOnly or SameSite flags | 4 | Full audit |
| Content-Security-Policy missing | The header that limits which scripts may run | 2 | Full audit |
| HSTS missing | The header that forces browsers to use HTTPS | 2 | Full audit |
| Clickjacking protection missing | Protection against your page being embedded in another site | 2 | Full audit |
| CORS open to any site | Responses readable by scripts on any other website | 2 | Full audit |
| CORS trusts any website with credentials | On your home page, your server copies any website’s address into its CORS answer and allows the visitor’s cookies. We test the home page only, not your API routes. Checked only in the deeper check, after you prove the site is yours | 4 | Full audit |
| Mixed content | An HTTPS page loading images or scripts over plain HTTP | 2 | Full audit |
| X-Content-Type-Options missing | The header that stops browsers guessing file types | 2 | Full audit |
| Referrer-Policy missing | Control over how much of your addresses is shared with other sites | 1 | Full audit |
| Permissions-Policy missing | Limits on camera, microphone, location and similar features | 1 | Full audit |
| Script from a compromised domain | A script loaded from a domain known to have served malicious code, such as polyfill.io | 40 | Surface report |
| Password sent without encryption | A password field on a page or in a form that is not on HTTPS | 8 | Full audit |
| Unsupported jQuery | jQuery 1.x or 2.x, which no longer get security fixes | 3 | Full audit |
| Third-party scripts without integrity check | Scripts from another domain without the integrity attribute | 2 | Full audit |
| Form sent without encryption | A form that sends its data to an http:// address | 2 | Full audit |
| Technical error shown in the page | A stack trace or a framework error page in the home page | 2 | Full audit |
| HSTS too short | An HSTS header that lasts less than six months | 1 | Full audit |
| Content-Security-Policy allows inline scripts | A policy whose script-src allows 'unsafe-inline' or 'unsafe-eval' | 1 | Full audit |
Content-Security-Policy without base-uri | A policy that does not restrict where a <base> tag may point | 1 | Full audit |
| Cross-Origin-Opener-Policy missing | The header that isolates your page from windows of other sites | 1 | Full audit |
| Software version in the headers | A version number in the Server or X-Powered-By header | 1 | Full audit |
| Cookie on a publicly cached page | The page sets a session or login cookie (by its name, or because it is HttpOnly) while letting shared caches (CDNs, proxies) store it | 4 | Full audit |
| Session ID in the page links | A link or form of your site with a session identifier in the address, such as PHPSESSID= or jsessionid=, or a form with a hidden PHPSESSID field | 3 | Full audit |
| Login page can be cached | A page with a password field that does not send Cache-Control: no-store, so browsers and proxies may keep a copy (no-cache, private or max-age=0 alone do not prevent that) | 2 | Full audit |
| Debug headers | A header from a debugging or profiling tool, such as X-Debug-Token | 2 | Full audit |
| HSTS without subdomains | An HSTS header on your main domain without includeSubDomains. Not counted under .app, .dev and the other extensions browsers already force to HTTPS, or on an address of a hosting platform. Judged only when the scan ends on the main domain itself: on www. we do not read the main domain’s answer, so the check is not examined | 1 | Full audit |
| Content-Security-Policy trusts public code hosts | A policy whose script-src allows a host where anyone can publish or call code: jsDelivr, unpkg, cdnjs, esm.sh, Skypack, JSPM, ajax.googleapis.com, www.google.com or apis.google.com. Not counted with strict-dynamic, which makes browsers ignore hosts | 1 | Full audit |
| Cookie shared with every subdomain | A cookie whose Domain attribute covers your whole domain, so every subdomain can read it. Not counted on an app builder’s subdomain or an address of a hosting platform, and the bot-protection cookies of CDNs do not count | 1 | Full audit |
| Internal address in the headers | A private IP address or an internal server name in a response header | 1 | Full audit |
Security — certificate, domain and email (your own domain only)
| Check | What we look at | Points | Shown |
|---|---|---|---|
| Outdated TLS or expiring certificate | Encryption older than TLS 1.2, or a certificate that expires within 30 days | 12 | Full audit |
| SPF record missing | Which servers may send email for your domain | 6 | Full audit |
| DMARC record missing | Your policy against email impersonation | 6 | Full audit |
| DMARC in monitor-only mode | A DMARC policy that watches but does not block (p=none) | 3 | Full audit |
| Domain does not receive email | No mail server published for your domain | 2 | Full audit |
| No CAA record | Which certificate authorities may issue certificates for your domain | 1 | Full audit |
| SPF lets anyone send | An SPF record that ends with +all or ?all | 3 | Full audit |
| No MTA-STS | The record that makes mail servers deliver to your domain only with encryption, when the domain receives email | 1 | Full audit |
| No TLS-RPT | The record that asks mail servers to report failed encrypted deliveries to your domain, when the domain receives email. More than one record, or one without a usable rua address, counts as missing | 1 | Full audit |
| SPF record invalid | An SPF record that needs more than 10 DNS lookups, counting the records it includes, so mail servers treat it as broken | 1 | Full audit |
| Old TLS versions accepted | The server still accepts TLS 1.0 or 1.1. Not active yet: the result stays “not examined” and the points are never deducted. The deeper check already shows the accepted TLS versions separately, without points (see below) | 8 | Full audit |
| Certificate chain problem | An incomplete, self-signed or untrusted certificate chain. Not active yet: the result stays “not examined” and the points are never deducted. The deeper check already shows the chain separately, without points (see below) | 8 | Full audit |
| Weak TLS ciphers | TLS 1.2 suites without forward secrecy, or in CBC mode. Not active yet: the result stays “not examined” and the points are never deducted. The deeper check already shows the cipher suites separately, without points (see below) | 2 | Full audit |
| Domain without DNSSEC | The domain is not signed with DNSSEC, read from its public registration. Not active yet: the result stays “not examined” and the point is never deducted | 1 | Full audit |
- In the deeper check, if the owner ticks its box and the server is theirs rather than a shared platform, we look at the encryption more closely: up to 7 more TLS connections to that server, one after the other, within 20 seconds, without requesting any page. They show which TLS versions the server accepts, whether it still allows cipher suites without forward secrecy or in CBC mode, whether the certificate chain is complete and whether the server attaches its revocation status. The report shows them separately; they do not change the security score. Two of them are also named among the owner’s findings, without points: a server that does not accept TLS 1.3 when the connection that tries only TLS 1.3 is refused, and a revoked certificate only when the revocation status the server attaches says so, carries your certificate authority’s valid signature and is still current — without it we do not ask the certificate authority, and we do not say.
Performance
| Measurement | What we measure | Good → poor |
|---|---|---|
| Server response time (40%) | Time until your server starts answering | under 200 ms → over 800 ms |
| JavaScript size (30%) | Total size of your own scripts | under 300 KB → over 1.5 MB |
| JavaScript files (10%) | How many of your own script files load | 3 or fewer → 20 or more |
| HTML size (10%) | Size of the home page markup | under 15 KB → over 100 KB |
| Server-side rendering (10%) | Whether content is already in the HTML | yes = 100 · no = 60 |
Accessibility
| Check | Why it matters | Weight |
|---|---|---|
| Images have alternative text | Screen readers can describe images | High ×3 |
| Form fields have a label | Users know what to type | High ×3 |
| Buttons have a name | Icon-only buttons are announced | High ×3 |
| Links have readable text | Users know where a link goes | Medium ×2 |
| The page declares its language | Text is read with the right pronunciation | Medium ×2 |
| Heading levels do not skip | The page structure is navigable | Low ×1 |
- In the deeper check, if the owner ticks its box, the same six checks also run on up to 10 pages as an isolated browser shows them, together with three that only a browser can measure: text contrast, keyboard focus order and the state of controls. The report shows them separately, with the number of pages covered; they do not change the accessibility result above.
SEO
| Check | Points |
|---|---|
| The page asks search engines not to index it | −40 |
| Title missing | −15 |
| Mobile viewport setting missing | −12 |
| Meta description missing | −10 |
| No main heading (H1) | −10 |
| No canonical link | −5 |
| More than one H1 | −5 |
| No social preview (Open Graph) | −5 |
| Title shorter than 10 or longer than 60 characters | −4 |
| Description shorter than 50 or longer than 160 characters | −4 |
robots.txt missing or empty | −3 |
| No sitemap declared or found | −3 |
| Broken language alternates (hreflang) | −3 |
Checks for your technology (no score)
When we recognise the technology your site is built with, and only when we are sure, a few more checks run on the same files we already downloaded — no extra requests. They appear in their own section of the report and do not change the security score. When we are not sure, the report says so and these checks do not run. A version we cannot match to our tables is reported as unknown, never as up to date.
| Check | Technology | Severity | Shown |
|---|---|---|---|
| WordPress version with a known vulnerability | WordPress | Critical | Surface report |
| WordPress plugin with a known vulnerability | WordPress | Critical | Surface report |
| Outdated WordPress | WordPress | High | Surface report |
| Outdated WordPress plugin | WordPress | High | Surface report |
| WordPress version declared in the page | WordPress | Medium | Full audit |
| WordPress XML-RPC advertised | WordPress | Medium | Full audit |
| Outdated WordPress theme | WordPress | Medium | Full audit |
| Outdated WooCommerce | WooCommerce | High | Surface report |
| Cart reloaded on every page | WooCommerce | Medium | Full audit |
| Too many apps loading scripts in the store | Shopify | Medium | Full audit |
| Heavy Shopify page and theme | Shopify | Medium | Full audit |
| Magento 1, end of life | Magento | Critical | Surface report |
| End-of-life Drupal | Drupal | High | Surface report |
| Drupal version declared | Drupal | Medium | Full audit |
| End-of-life Joomla | Joomla | High | Surface report |
| Joomla version declared | Joomla | Medium | Full audit |
| Next.js version with a known vulnerability | Next.js | Critical | Surface report |
| Outdated Next.js | Next.js | High | Surface report |
| Next.js running in development mode in production | Next.js | High | Surface report |
| Next.js public variables with secret-like names | Next.js | High | Surface report |
| Page with a cookie stored in a shared cache | Next.js | High | Full audit |
| Next.js X-Powered-By header | Next.js | Medium | Full audit |
| Next.js static files without long caching | Next.js | Medium | Full audit |
| Nuxt 2, end of life | Nuxt | High | Surface report |
| Nuxt running in development mode in production | Nuxt | High | Surface report |
| Nuxt public config with secret-like names | Nuxt | High | Surface report |
| Unsupported Angular | Angular | High | Surface report |
| Angular in development mode | Angular | High | Surface report |
| Angular app in a single chunk | Angular | Medium | Full audit |
| Vue 2, end of life | Vue | High | Surface report |
| Vue development build | Vue | High | Surface report |
| React development build | React | High | Surface report |
| Development server open to the Internet | React | High | Surface report |
| Environment variables with secret-like names in the bundle | React | High | Surface report |
| React app in one heavy chunk | React | Medium | Full audit |
| Laravel debug tools visible from the home page | Laravel | Critical | Surface report |
| ASP.NET version in the headers | ASP.NET | Medium | Full audit |
| Very heavy ASP.NET ViewState | ASP.NET | Medium | Full audit |
| Session identifier in the addresses | Spring | Medium | Full audit |
Privacy and technology (no score)
- Trackers before consent — advertising and analytics trackers (Google, Meta, Hotjar, TikTok, LinkedIn, X, Pinterest, Snapchat, Microsoft Clarity, Yandex) that load before a visitor could agree. Shown in the surface report.
- Cookie consent tool — whether a known consent banner is present.
- Privacy policy link — whether the home page links to one.
- Technology overview — framework, hosting provider, analytics tools and number of external services. Context for the migration, not a problem in itself.
What an automatic check cannot see#
The surface check looks only at your home page from the outside. It cannot see logged-in areas, other pages, your database rules, slow queries or how your code handles data. Where a check depends on content that only appears after JavaScript runs, we say “not measured” rather than making assumptions. The full audit covers what the surface check cannot: it is carried out by our team, with access you grant, table by table — and we still never read your data.
Frequently asked questions#
Does the check slow my site down?
No. It is one short visit with a fixed time and size budget, comparable to a single visitor loading your home page.
You found my database key. Has my data been accessed?
Not by us. We report that the key is visible in your public code; we never use it. Whether it is a real risk depends on your row-level security settings, which the full audit checks.
Why is my score lower than I expected?
Each finding removes points, and the serious ones remove many: one exposed secret key alone costs 40 points. Findings not shown in the surface report still count; the report tells you how many there are.
Can I stop the scanner from checking my site?
Yes. Add User-agent: Root92-MigrationScanner and Disallow: / to your robots.txt, or see the “About the scanner” page.
What do you keep after the check?
Only the results, never your page content. If you do not leave your email, we keep no contact record. We have put in place appropriate technical and organisational measures to keep your data secure. Details and retention periods are in the privacy policy.