SubscribeGo ProYour plan Settings

Check it yourself

You do not need to believe any of this page. Three checks, each under a minute:

  • The badge. Every page carries a Private · verify badge. Press it and it lists everything that page has contacted since you opened it, grouped by where it went, and how many bytes of your content each request carried. Open a file in a tool and watch the list: it does not grow.
  • Your browser's network panel. The badge is this site's own code, so check it with something that is not. Verify walks through it: open the panel, use a tool with a large file, and see that nothing the size of your file goes up. It also hashes the code your browser was served, so you can compare it with what somebody else received.
  • Pull the plug. Load a tool, switch off your connection, and use it. It works, because everything it needs is already in the tab.

What you will see in the network panel: requests to obscuraos.com, including this site's page count (/api/hit, three words, described below), and Cloudflare Web Analytics, which Cloudflare adds to each page as it is served. Nothing else is permitted, by the policy in the next section.

The policy your browser enforces

Every page on this site is sent with this header. It is quoted here by the build from _headers, the file Cloudflare reads, so it cannot say one thing here and another on the wire:

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://static.cloudflareinsights.com; worker-src 'self' blob:; child-src 'self' blob:; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' blob: data: https://cloudflareinsights.com; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
DirectiveValueWhat it means
default-src'self'Anything not named below may come from this site only.
script-src'self' 'unsafe-inline' 'wasm-unsafe-eval' blob: https://static.cloudflareinsights.comCode may come from this site, from the page itself, and from workers and WebAssembly the tools build as they run. The one outside address is Cloudflare Web Analytics' script, which Cloudflare adds at the edge (see below). 'unsafe-inline' is there because the pages still carry inline script; it is a weakness against script injection, not a way for a file to leave.
worker-src'self' blob:Background workers may only be this site's own files, or ones a tool builds in memory.
child-src'self' blob:The same, for older browsers that read this instead.
style-src'self' 'unsafe-inline'Styles from this site and the page itself.
img-src'self' data: blob:Pictures from this site, or made in the page. No outside image can be used as a beacon.
media-src'self' data: blob:The same, for sound and video.
font-src'self' data:Fonts are this site's own files; no font service is asked.
connect-src'self' blob: data: https://cloudflareinsights.comWhere a page may send a request. This is the directive that makes “nothing uploaded” something the browser enforces: a page can talk to this site and to Cloudflare Web Analytics' collector, and the browser refuses anything else, whatever the code tries.
manifest-src'self'The install manifest is this site's.
object-src'none'No plug-ins, ever.
base-uri'self'An injected tag cannot make relative links point elsewhere.
form-action'self'A form can only post to this site.
frame-ancestors'self'No other site can put these pages in a frame.

Devices a page may use, from the same block:

Permissions-Policy: camera=(self), microphone=(self), geolocation=(self), payment=(), usb=(), serial=(), bluetooth=(), interest-cohort=()

Replies from the server's own routes under /api/ carry a stricter policy, since none of them is a page: default-src 'none'; frame-ancestors 'none'.

6 files run under a policy of their own, because the library inside has to build code as it starts. None of them is a page, and none is given a wider connect-src than the page has:

  • /pro-code/cad-solve-worker.js: default-src 'none'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; connect-src 'self'; worker-src 'self'
  • /pro-code/eng-brep-worker.js: default-src 'none'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; connect-src 'self'; worker-src 'self'
  • /vendor/libraw-wasm/1.6.0/worker.js: default-src 'none'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; connect-src 'self'; worker-src 'self'
  • /vendor/libraw-wasm/1.6.0/libraw.js: default-src 'none'; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; connect-src 'self'; worker-src 'self'
  • /lib/occt-import-js/*: no policy at all. A worker is held to its own script's policy, and this one (the Engineering Suite's STEP reader) could not start under the page's. Nothing on the site loads a page from there.
  • /lib/replicad-opencascadejs/*: no policy at all. A worker is held to its own script's policy, and this one (the Engineering Suite's STEP reader) could not start under the page's. Nothing on the site loads a page from there.

Two things this does not cover, stated plainly. It permits requests back to this site, which is why the page count and the routes below are listed in full. And it cannot stop a change to the site's own code; what stops that is the code being small enough to read, the build's checks, and hashing what you were served.

Who else handles data

Three companies process data for this site. None of them ever receives a file you open in a tool, because no tool sends one.

WhoFor whatWhat they handle
CloudflareHosting: the pages, the server routes, the store of seats and waitlist records (Workers KV), the page count (a D1 database), and Cloudflare Web Analytics.Every request reaches Cloudflare's edge, with your IP address, as it would for any website. The records and counts described below, in this site's own Cloudflare account. Web Analytics reports that a page was viewed; it sets no cookie and keeps no identifier.
StripePayments, on Stripe's own checkout page.Your email address and payment details, under Stripe's own policy. This site never sees a card number. The server asks Stripe about a checkout or subscription by its reference, to make and renew your key.
ResendEmail, and only once email is switched on for this site.The address and the message: a key and its sign-in link, a seat invitation, or a waitlist confirmation. Whether email is on is shown here when this page can ask the server.

The server also asks a few public services for public data, on your behalf, so that your browser never contacts them. Every outside address in the server's code is here:

AddressWhoWhat it is sent
api.stripe.comStripeCheckout, subscription, invoice and customer references, to make and renew keys, credit an invitation and read the owner's revenue figures. Only ever with this site's own key.
billing.stripe.comStripe's billing pageA link written into the key email and the Pro page. The server never fetches it.
api.resend.comResendOnly when email is switched on (the RESEND_API_KEY secret): the buyer's address and their key, the sign-in link, a seat invitation, or a waitlist confirmation.
api.osv.devOSV.dev, Google's open source vulnerability databaseWhen you press the button in the Sovereign Suite's dependency check: each package's name, version and ecosystem, and nothing else.
api.pwnedpasswords.comHave I Been PwnedWhen you use PWNCHECK: the first five characters of the password's SHA-1 hash, with padding asked for. Never the password or the rest of the hash.
www.cisa.govCISAIts whole Known Exploited Vulnerabilities list, fetched at most once every six hours per data centre. Nothing about you or your findings.
epss.cyentia.comFIRST's EPSS scoresThe whole daily file, the same way. Nothing about you or your findings.
cdn.jsdelivr.netjsDelivrThe library files on the shelf (/vendor/), fetched once by the edge, checked against a pinned size or hash, and kept. Your browser never contacts it.
cdnjs.cloudflare.comcdnjsThe same, for the libraries published there.
huggingface.coHugging FaceThe speech model SCRIBE uses, fetched once by the edge in the same way.
tfhub.devTensorFlow HubThe face detection model VEIL uses, the same way.
github.comGitHub releasesOne pinned model file for the Photo Editor's subject selection, fetched once by the edge and checked against its hash.
gist.githubusercontent.comA GitHub gistThe record /status is drawn from, read by /api/status. Nothing is sent to it.

The terms for Team and Business are in the data processing agreement.

What the server sees

Every route the server answers, found from the files that answer them. A route not in this table does not exist.

RouteWho calls itWhat arrivesWhat is kept
/api/hitEvery page, once as it opens, and a tool when a file is opened, a save meets the paywall, a checkout button is pressed and the like.Three words: the page's address, what happened, and where the visit came from as a class (a search engine's name, “internal”, “other”, “none”). Nothing is sent when Global Privacy Control or Do Not Track is on.One is added to a count for that day and those three words (the table below). The address and user agent are not kept; the user agent is read only to leave robots out.
/api/licenceThe Pro page, when a checkout finishes, a key is renewed, a gift or seat link is opened, or someone asks for their key by email.A Stripe checkout reference, a key, an invitation or seat token, or an email address.Nothing of its own but a note that a checkout's key email has gone (pro-mail:) and when a gift pass was first opened (gift-start:). Stripe is the record of who paid.
/stripe-webhookStripe's servers, when a checkout completes, an invoice is paid, or a subscription changes or ends.An event naming a checkout, invoice or subscription. Only the reference is believed: the object is fetched from Stripe with this site's key.The key email's note, an invitation pairing (ref:), and on Team and Business a line in the seat record when the plan changes.
/api/seatsThe seats page, for the holder of a Team or Business subscription's own key.That key, and a person's name and email address when a seat is added.The seats and their record of changes (seat:, seatlog:, seattoken:, seatplan:, seatgen:, seatseen:), and a Business organisation's name and logo (seatorg:).
/pro-code/*The Sovereign Suite, the Photo Editor and the Engineering Suite, for their code.The Pro key, in a cookie sent to this path only.Nothing. The key is checked; whether the subscription is paid is cached at the edge (below).
/api/waitlistThe waitlist form, and the letter's sign-up at the end of each guide.An email address and which products it is waiting for.The address, the dates, and the products (waitlist:). No IP address, no browser details.
/api/unsubscribeThe unsubscribe page.An email address.The record is marked as unsubscribed and its products emptied.
/api/osv/*The Sovereign Suite's dependency check, when you press the button.Package names, versions and ecosystems.Nothing: it passes the question to OSV.dev and the answer back.
/api/pwned/*PWNCHECK.Five hexadecimal characters of a password's SHA-1 hash.Nothing: it passes them to Have I Been Pwned and the answer back.
/api/kev/*The Sovereign Suite's vulnerability tracker, when you ask it to fetch the lists.Nothing but which of the two public lists.Nothing: CISA's and FIRST's lists are fetched and kept at the edge for six hours.
/vendor/*Tools that need a library or a model.Which library file.Nothing about you. The file is fetched once by the edge and kept.
/api/status/status.Nothing.Nothing: it reads the record the scheduled check keeps.
/api/statsThe owner's /stats page, behind a key.The key and a date range.Nothing; it reads the counts. With STATS_PUBLIC on, anyone may see the daily totals and the thirty pages most often found from a search.
/api/revenueThe owner's /revenue page, behind the same key.The key.Nothing; it reads totals from Stripe.
/api/waitlist-exportThe owner, behind a key of its own.The key.Nothing; it reads the waitlist.

Every route under /api/ first passes a gate that refuses a write from another website, caps the size of what is sent, and limits how often one address can write. The gate keeps its counts in memory and writes nothing down.

The page count

One table, in a D1 database in this site's Cloudflare account. It has no column for an address, a browser, a cookie or any identifier, so none can be stored by mistake.

The table, from the schema the database was made with (.github/d1/hits.sql), with its primary key day, path, ev, src:

ColumnDeclared asWhat it holds
dayday TEXT NOT NULLThe date, in UTC.
pathpath TEXT NOT NULLThe page's address, such as /seal: lower case, no query, no fragment.
evev TEXT NOT NULLWhat happened, one of the words below.
srcsrc TEXT NOT NULLWhere the visit came from, one of the classes below.
nn INTEGER NOT NULL DEFAULT 0How many times. The only number in the table.

A beacon is refused unless each word is on a fixed list in functions/_lib/count.js. The events: view, 404, file, gate, unlock, handoff, share, waitlist, invite-land, paid; plan: and one of 8 plan names; via: and a guide's address; lang: and one of 12 languages or other, for a visit from a search engine; and lcp, inp, cls with a kind of page and good, ni or poor, never the measurement. The sources: google, bing, duckduckgo, ecosia, brave, yahoo, yandex, startpage, qwant, kagi, reddit, hn, github, mastodon, linkedin, x, share, other, internal, none.

Pages that never send one: /stats, /revenue, /seats.

The records

Everything the server keeps about people, in one Workers KV store. Found from the kinds of record the code writes; there is no other.

RecordWhat it holdsHow long
waitlist:…An email address, when it joined, and the products it is waiting for; marked when it leaves.Until you ask for it to be deleted.
seat:…A person on a Team or Business plan: the name and email address the admin typed, the dates they were added, joined and removed, and which admin key did each.While the plan has them; a removed seat for 400 days, so a returning key can be told it was taken back.
seatlog:…One line of the record of seat changes, for the admin's audit.Kept; never rewritten.
seattoken:…An invitation to a seat, stored only as the SHA-256 hash of its token, with the seat it opens.Fourteen days, then it expires by itself.
seatplan:…The plan and seat limit last seen, so a change of plan is recorded once.While the subscription exists.
seatgen:…Which generation of a Team or Business admin key is current.While the subscription exists.
seatseen:…When a seat's key was last renewed, as a date only, for the admin's “key last renewed” column.400 days after the last renewal.
seatorg:…On Business, the organisation's name and small logo the admin set, for the covers of its reports, with when and by which admin key.Kept; the admin can change it, and there is not yet a way to remove it.
pro-mail:…That a checkout's key email has been sent, with Stripe's checkout reference and the time. No address.400 days.
gift-start:…When a gift pass's link was first opened, by checkout reference.400 days.
ref:…A friend's subscription paired with the inviter's subscription and customer references, and whether the month has been credited. No names or addresses.Kept, so a month is credited once.

Nothing about what anybody opens, types or makes is in any of it, because none of that is ever in a request.

What “Pro, locked on the server” means

The code for the Sovereign Suite, the Photo Editor and the Engineering Suite is not in the pages. It is served one file at a time from /pro-code/, and only to a request that carries a Pro key. The key is a short statement of the plan and the date it runs to, signed with an Ed25519 key that only the server holds; the page checks the signature with the public half, and the server checks it again before it hands over any code. A check made in the browser could be edited out of the browser. This one cannot: without a key, the code is simply not there to run.

What /pro-code/ answers when the key is missing or not good enough, read from the route itself. The reason is in the reply's X-Obscura-Pro header:

AnswerReason
402no key
403key not valid
402key expired
402subscription ended
402seat removed
402key replaced

Whether a subscription is still paid is asked of Stripe and kept at the edge for 6 hours, so a cancelled or refunded subscription stops being served within 6 hours. On Team and Business, whether a seat has been taken back is read from the store (which the edge may answer from a copy up to 30 seconds old) and remembered by the server for 30 seconds; a store write takes about a minute to reach every data centre, so a seat taken back stops being served within about two minutes. If Stripe cannot be reached, a signed key that is in date is served: a paying customer is not locked out by somebody else's outage.

The key travels in a cookie that is sent to /pro-code/ and nowhere else, and it carries no name or email address. Status asks /pro-code/ for a real file with no key every thirty minutes, and records whether it was refused.

What has not been done

There has been no external audit. Nobody independent has reviewed this code or tested this site: there is no penetration test report, no SOC 2 and no ISO 27001. Everything on this page is evidence you can gather yourself, not a certificate. If your purchase needs one of those, we do not have it, and security says what we do have and how to report a problem.

The rest of the paperwork: the privacy policy, the terms, the data processing agreement, the threat model, tool by tool, and the accessibility statement.