Most SaaS landing pages have a security section. It mentions encryption, compliance logos, and something about enterprise-grade infrastructure. You cannot tell whether any of it is true without signing an NDA.
CastorStack's home page lists eight specific protections. Each one maps to code you can open, tests that fail the build if someone removes it, or a script that runs before the buyer ZIP leaves the repo. This post walks through all eight.
Authentication and RLS on Supabase tables got their own article (The cost of the first login). What follows is everything else on that list.
Signed payment webhooks
Stripe, Paddle and Polar all deliver subscription events over HTTP POST. Anyone on the internet can hit that URL. The only thing standing between your database and a forged "payment succeeded" event is signature verification.
In CastorStack each provider has its own verifier: Stripe uses the standard webhook signature helper with HMAC-SHA256 and constant-time comparison; Paddle parses the Paddle-Signature header the same way; Polar checks webhook-signature and webhook-timestamp. All three reject events older than five minutes, which closes the replay window. Mismatched signatures fail with FixedTimeEquals, not a string comparison that leaks timing information.
Events are stored with an idempotency key before any business logic runs. A provider retrying the same event does not create a second subscription row.
You can read the implementations in PaddlePaymentProvider.cs, StandardWebhookSignature.cs, and the Polar provider. The tests in PaddlePaymentProviderTests.cs and StripeWebhookEndpointsTests.cs cover the failure cases.
Admin routes gated end to end
The admin panel is a separate Vite app with its own deployment. That separation is useful, but it is not the security boundary. The boundary is the API.
Every endpoint under /api/v1/admin/* requires an authenticated user with the admin role. There is no "public admin read" shortcut and no endpoint that checks the role in one place but forgets it in another. The authorization attribute is applied route by route, and the tests assert that unauthenticated and non-admin callers get 401 or 403.
If you add a new admin endpoint and skip the attribute, you will notice quickly because the pattern is consistent across the existing controllers.
Encrypted PSP credentials
Payment provider secrets (Stripe secret key, Paddle API key, webhook signing secrets) are stored in the database because the admin panel needs to configure them at runtime. Storing them in plain text would mean anyone with read access to Postgres could charge cards on your behalf.
CastorStack encrypts them with ASP.NET Data Protection before persistence. The encryption keys live in a DataProtectionKeys table, and that table has RLS enabled like every other server-managed table. API responses never return the decrypted secret; the admin UI shows a masked placeholder after the initial save.
Rotating keys or moving between environments requires understanding Data Protection's key ring, which is documented in the infrastructure setup. It is more moving parts than environment variables, but environment variables do not survive "configure this in the admin panel" as a product requirement.
No raw SQL in the API
Entity Framework Core is the only database access path in the API project. There are no FromSqlRaw calls with interpolated strings, no Dapper queries, no hand-built SQL in controllers.
That is a deliberate constraint. Parameterized queries through EF eliminate the most common injection surface in a line-of-business API. When you need raw SQL, it belongs in a migration file where it is reviewed, versioned, and never built from user input.
RLS enforced by migration
This one almost slipped past us, and it deserves more than a bullet point.
Supabase exposes every table in public through PostgREST. The anonymous key ships in your frontend bundle. Anyone can read it. Row Level Security is the actual protection, and Postgres does not enable it automatically on tables your migrations create.
CastorStack has a HardenRowLevelSecurity migration that enables RLS on every server-managed table, revokes grants from anon and authenticated, and changes the schema's default privileges so the next CreateTable is born closed. A regression test (RowLevelSecurityCoverageTests) parses every migration source and fails the build if a new table appears without a matching ENABLE ROW LEVEL SECURITY statement.
That test is the part I would not have thought to write until after someone pointed a REST client at production.
Buyer ZIP scanning
When you package CastorStack for a buyer, scripts/package-release.mjs stages a ZIP and scans it before release. The scan looks for live Stripe keys, JWT-shaped secrets, hardcoded passwords, and personal email addresses that should not travel with the template.
If something matches, the script exits with an error and the ZIP does not ship. It is a last line of defense against "I committed a secret three months ago and forgot."
Strict CORS defaults
The API's CORS policy lists explicit allowed origins from configuration. Credentials are disabled. That combination means a browser on a random domain cannot make authenticated cross-origin requests to your API, which rules out a class of CSRF attacks against cookie-based sessions.
You configure the allowed origins per environment (Cors:AllowedOrigins in appsettings or the equivalent environment variable). Adding a new frontend domain is a config change, not a code change.
Checkout redirect allowlist
Checkout endpoints accept a returnTo URL so the user lands back on your app after payment. Without validation, that parameter becomes an open redirect: an attacker sends a victim through your checkout and out to a phishing page that looks like it came from you.
CastorStack validates returnTo against the same allowed origins as CORS. A URL on an unlisted domain returns 400. The tests in PublicCheckoutEndpointsTests.cs cover both the happy path and the rejection case.
What is not here
This is not a SOC 2 certification. There is no WAF, no rate limiting middleware in the template, no automated dependency scanning in CI beyond what you add yourself. Penetration testing is your responsibility after you deploy.
Two-factor authentication is not implemented. Session listing and revocation are not implemented. The audit log records API-side changes, not Supabase login events.
The list on the home page is what ships in the repo today, not a roadmap dressed as a feature matrix. I would rather you find the gaps here than during a security review you did not schedule.
Why list verifiable claims
Security marketing that cannot be checked erodes trust faster than no security section at all. A buyer who clones the repo and opens RowLevelSecurityCoverageTests.cs learns more about what they are getting than from any compliance badge.
If you are evaluating CastorStack, start with those eight items. Open the files. Run the tests. The home page is not asking you to take our word for it.