How the waitlist is protected.

Last updated .

This page is about the security of this website and the founding-cohort form. The application that reads shops' invoices does not exist publicly yet, and when it does it will need a longer page than this one.

What the form is allowed to do.

The founding-cohort form writes one row to one table. What protects that table is not where the form runs, but what the database permits.

Row level security is switched on and exactly one policy exists: an anonymous caller may insert. There is no policy permitting read, update or delete. The table's permissions were then narrowed further, so the anonymous role has no read grant to fall back on either.

For anyone arriving through this website, insert is the only operation available.

That was tested rather than assumed. A signup was submitted, and then the same anonymous credentials were used to read the table back. The database refused outright - HTTP 401, "permission denied for table waitlist_signups". Not an empty list. A refusal.

Who can read it.

I can, with a privileged credential that is never used by this website, never present in any page or bundle, and never leaves the server side. That is how I see applications at all. The distinction is the whole design: the website can write and cannot read, and the credential that can read is not in the website.

About the public key.

Browser-based forms usually carry a public key, and this site's JavaScript does contain one. It is public by design, and it is exactly what the permissions above exist to contain. Submissions themselves are handled by a server function rather than sent from your browser straight to the database. Either way the answer is the same: it can add a row, and it cannot read one.

The waitlist is not in the same database as anything that matters.

This site's database holds founding-cohort applications and nothing else. Shop financial data - invoices, costs, margins - will live in a different database, in a different project, under different credentials.

The reason is blast radius. The key in this page is public by design. If it is public, it should point somewhere that holds twenty-five names and no money. The separation means the marketing site cannot reach shop data because no path exists, rather than because every permission was configured correctly.

Secrets are not in the website.

  • The email service's API key lives only as an environment variable on a server function. It is not in the page, not in the JavaScript bundle, and not in the repository.
  • The database's privileged key is never used by this site at all.
  • The notification function rejects any request that does not carry the right credentials, and it logs failures without logging anyone's email address or the key.

What I am not claiming.

Security pages tend to imply more than they say. Here is what is not true yet:

  • There is no SOC 2 report, no ISO certification and no third-party audit.
  • No penetration test has been performed.
  • I am one person. There is no security team and no 24-hour monitoring.

All three are things a company handling shop financials should eventually have, and saying so now is cheaper than being asked later.

If you find a problem.

Email support@stemalytics.com and tell me what you found. I will reply, and I will tell you what I did about it. If you have found something serious, please give me a reasonable chance to fix it before publishing it.

Last updated .