// saas

Agents ship the pull request. Nobody tests it. We test what it breaks for your customers.

Coding agents open pull requests faster than any team can review them. We test the parts that cost you customers: who can see whose data, what gets billed, who still has access.

Start a two-week pilot
ISO 27001:2022clients include Vulog, Collaboard, Any4m4.9 Clutch, 70 reviews$25-45/hour
On an invoicing platform the bugs that matter are in the maths: a total that ignores a second tax, a client balance at zero with unpaid invoices out. The page looks fine and the number is wrong. I check every total against my own calculation first.
Raluca Lenghel, QA engineer on a B2B SaaS platform since 2025
// a bug I still remember
“One feature let you limit how many requests a user could make. Once they reached it, they were blocked and saw a message. There were built-in ways to raise the limit, so I wanted to see how the app handled that. After the limit went up, the user was still blocked. It was fixed soon after.”
Rucsandra Brun, QA engineer, SaaS
// what keeps you up at night, and what you get
"Our coding agents write code faster than anyone can test it."
Every pull request tested before your users see it, without hiring a QA team first.Agentic pipelines
"A release breaks something customers use every day, and they churn."
Regression that repairs itself when the UI changes, and tests you keep if you leave.Flows
"An enterprise buyer sends a security questionnaire we can't answer."
A security report an enterprise buyer will accept.AI Security Toolkit
// four places SaaS products break
01 · tenant isolation

Customer A reads customer B's invoice

The login check passes: A has a valid token for org A. The endpoint then loads invoice 9041 by id alone, with no filter on the caller's org. Changing one number in the URL returns another customer's data. Agent-written handlers repeat this when the filter lives in each query instead of one shared place.

OWASP API1:2023, broken object level authorization
org Aapidblogin as user of org AJWT, org=AGET /invoices/9041SELECT ... WHERE id=9041row found, org=B200 + invoice of org B
FAILorg A read org B's invoice 9041
how we test itSeed two orgs, create an object in B, then call every verb on it as A. Expect 404, not 403, so the id's existence is not confirmed either.
const [a, b] = await seedOrgs(2)
const inv = await b.api.post('/invoices', fixture)
for (const verb of ['get', 'patch', 'delete'])
  expect((await a.api[verb](`/invoices/${inv.id}`)).status).toBe(404)
expect(ids(await a.api.get('/invoices'))).not.toContain(inv.id)
02 · billing webhooks

A late payment event reopens a cancelled plan

Stripe delivers webhooks at least once and does not guarantee order. A customer cancels, and customer.subscription.deleted lands first. Then a delayed invoice.paid for the last invoice arrives and the handler sets the status back to active. A redelivery grants the seats a second time.

Stripe retries a failed delivery for up to three days, so the late event can land long after the cancel
createdevt_298
deletedevt_302
paidevt_301
paidevt_301
SEATS x2redelivered, no dedupe
how we test itReplay the same events in every order and twice. The final state must depend on the events, not on the order they arrived in.
const evts = [created, deleted, paid]
for (const order of permutations(evts))
  for (const times of [1, 2]) {
    await resetAccount(); await deliver(order, { times })
    expect(await account.status()).toBe('canceled')
    expect(await account.seats()).toBe(0)
  }
03 · proration

An upgrade is charged for a day that did not exist

A customer upgrades near midnight. The credit for the old plan counts the days left in UTC and the charge for the new plan counts them in the customer's timezone, so the customer pays for a day of the new plan with no matching credit. Every line adds up, the total is 197 too high, and nothing errors.

also: month ends, leap days, seat changes on the renewal day
old plan / month2900new plan / month5900days left (UTC)12days left (customer tz)13credit 2900 x 12/301160charge 5900 x 13/302557invoice total1397expected 12 days both1200delta+197
one clock for both sidesinteger centsround once, at the end
FAILcredit 12 days, charge 13 days
PASSsame interval on credit and charge
how we test itRun the upgrade at every boundary: last second of the day, month end, leap day, across timezones. Assert credit and charge cover the same interval and the total matches an independent calculation.
for (const t of boundaryInstants(['UTC', 'Pacific/Auckland'])) {
  clock.set(t)
  const inv = await upgrade(customer, 'basic', 'pro')
  expect(inv.total).toBe(expectedProration(customer, t))
  expect(inv.credit.days).toBe(inv.charge.days)
}
04 · offboarding

A deprovisioned employee is still signed in

HR removes someone in the identity provider and the SCIM PATCH sets active to false. The app marks the user inactive but never revokes the refresh token or the personal API key. The person keeps access until the token expires, or for as long as the key lives.

SCIM, RFC 7644; the question an enterprise buyer asks first
"userName": "ana@acme.example" "active": false session: refresh token valid 30 days API key sk_live_...: still valid SSO re-login: refused
STILL SIGNED INoff in the IdP, alive in the app
how we test itDeprovision through the IdP, then try the old session, refresh token and API key. All must fail inside your stated window, not at expiry.
const s = await app.login(user)
const k = await app.createApiKey(user)
await idp.scimPatch(user.id, { active: false })
await waitUntil(() => app.synced(user), { timeout: 60_000 })
expect((await app.get('/me', { session: s })).status).toBe(401)
expect((await app.get('/v1/data', { apiKey: k })).status).toBe(401)

Illustrative examples built from defect patterns we test for. Not client data.

// on every pull request

Your code is written in hours. Our pipeline keeps up.

Pull requestfrom your coding agent
Flowsjourneys and fault injection
Security Toolkit15 scanners + 20 own checks
QA engineerconfirms every finding
BugBoardrelease report, Jira sync
// the regulation, in testing terms
regimewhere it actually breakswhat we run
SOC 2Change management evidence for every release (we hold no SOC 2 report; this is what your auditor samples)release test reports traced to each change, kept as evidence
GDPRDeletion that reaches search indexes, exports and backups, not one tabledelete-then-search tests across stores, DPA data-flow checks
ISO 27001Security testing during development and acceptance (Annex A 8.29)security checks inside the same pass as functional tests
Security questionnairesA buyer asks for a recent penetration test summarya scoped penetration test or security scan, named as which it is, with confirmed findings and retest evidence
// proof

What we found, and what we confirmed

VulogCollaboardAny4m
  • A beauty and wellness marketplace: we found public endpoints that exposed the home addresses of unlisted providers, before anyone else did.
  • A B2B SaaS: our security scan confirmed one customer cannot reach another customer's data by swapping IDs.
  • A roofing-design platform, tested since 2025.
  • Salesforce and Jira operations for a technology vendor, since 2024.

Vulog, Collaboard and Any4m are clients. The marketplace and the B2B SaaS are not named.

BUG-2208 · billing / webhooks · examplehigh

Cancelled subscription turns active again after a late payment event

  1. Cancel the subscription in the customer portal
  2. Deliver invoice.paid for the last invoice after the cancellation event
  3. Account status returns to active and seats are restored

Fix: Ignore events older than the stored state, and key handling on the event id.

CWE-841REQ-BILL-12TC-2208 addedJira SUB-884

Two weeks on your pull requests. Free if you're not happy.

Start a pilot