Humantik checklist, version 1.0
This checklist has 30 items. Each one lists the reviews that include it, how the reviewer checks it, and the public source behind it. Use it on your own app; it is free. Every item is checked on staging or in a testing window agreed with the builder, with test accounts only, never against real users. A review covers the build it names; anything added to the app after that build is not covered. Item numbers are never reused, so a number dropped while the checklist was drafted stays unused.
Access rules
AR-01 · One user cannot read or change another user's records by changing an id
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
If the server trusts whatever record id the browser sends, any logged-in user can view or edit everyone else's data.
How the reviewer checks it
- Log in as sample user A, open a record A owns, and capture the request (REST/RPC call or Supabase query) that loads and saves it.
- Log in as sample user B, replay the same read and update requests with A's record id using B's session token.
- Repeat for each record type that belongs to a user (profiles, orders, messages, documents) and for list endpoints with filters removed.
- Pass only if every cross-user read returns no rows or 401/403/404 and every cross-user write changes nothing.
Evidence attached to the report
- Request/response pairs for B's attempts on A's records (tokens redacted)
- Screenshot or query output showing A's record unchanged after B's update attempt
- List of record types and endpoints tested
A pass or fail on this item is accepted only with:
- a request and response
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Deleting another user's records (only reading and changing are tried); records the reviewer could not reach from the app's screens, such as exports or shared links; and records that belong to a team rather than one person, which AR-04 covers.
Sources
AR-04 · Team or workspace members cannot see or change another team's data
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
In apps with teams, organisations or workspaces, one missing team filter exposes every customer's data to every other customer.
How the reviewer checks it
- Ask the builder for two sample teams, each with its own member account; skip with a note if the app has no team concept.
- As a member of team 1, replay reads, writes, invites and file requests for team 2 using team 2's ids.
- Remove yourself from team 1 (or have the builder remove you) and confirm team 1's data is no longer reachable with the same session.
Evidence attached to the report
- Request/response pairs for cross-team attempts, all refused or empty
- Policy or code excerpt showing how team membership is checked on the server
- Note stating whether the app is multi-team, and which tables are team-scoped
A pass or fail on this item is accepted only with:
- a request and response
- a settings screenshot or a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Apps with no teams (the item is skipped with a note), and what different roles inside one team can do, such as whether an ordinary member can act as the team's owner.
Sources
AR-02 · Ordinary users cannot reach admin pages or admin functions
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Hiding an admin button in the interface does not stop a normal user from calling the admin function directly.
How the reviewer checks it
- As an admin sample account, list the admin screens and capture the requests they make (e.g. list all users, change roles, refunds, edge functions).
- As an ordinary sample account, open each admin URL directly and replay each captured admin request with the ordinary user's token.
- In the repo, confirm the role check happens on the server (RLS policy, edge function, API route), not only in front-end code.
- An admin page that loads its layout but receives no admin data passes.
Evidence attached to the report
- Request/response pairs showing ordinary-user calls to admin functions refused (401/403 or no effect)
- Screenshot of direct navigation to admin URLs as an ordinary user
- Code excerpt or policy listing showing where the server checks the role
A pass or fail on this item is accepted only with:
- a request and response
- a screenshot
- a settings screenshot or a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Admin functions that no admin screen uses; roles between ordinary user and admin, such as moderators; and how well admin accounts themselves are protected, such as by strong passwords or two-step login.
Sources
AR-03 · Users cannot write rows as someone else or change protected fields
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
A table can block reading other users' rows yet still let a user create rows as someone else or hand themselves an admin role or paid plan.
How the reviewer checks it
- List RLS policies for every table the app writes to and check that each INSERT policy has a WITH CHECK, and each UPDATE policy has a WITH CHECK or a USING clause (Postgres reuses USING as the check), tying the row to the current user or team. The result is decided by steps 2 and 3.
- As a sample user, try to insert a row with another user's user_id and try to update one of your own rows to change its owner.
- As a sample user, try to update protected columns on your own profile or account (role, is_admin, verified flags, owner columns). Plan and credit fields are checked under PM-03.
Evidence attached to the report
- Policy listing (SQL or dashboard screenshot) for each written table showing USING and WITH CHECK clauses
- Request/response pairs for the forged insert, owner change and protected-field update, each refused or without effect
A pass or fail on this item is accepted only with:
- a settings screenshot or a code excerpt
- a request and response
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Tables the app does not normally write to, which a user might still be able to write to directly; protected fields beyond the usual ones (role, admin and verified flags, owner); and plan or credit fields, which are checked under PM-03.
Sources
Payments and money math
PM-01 · Payment webhooks are rejected unless the Stripe signature checks out
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Without signature checking, anyone can post a fake 'payment succeeded' event and get goods or access for free.
How the reviewer checks it
- In the repo, find the webhook handler and confirm it verifies the Stripe-Signature header against the endpoint's whsec_ secret using the raw request body (e.g. constructEvent) before acting.
- Confirm the signing secret comes from server-side configuration, not code or browser variables, and that a non-zero timestamp tolerance is used.
- Against staging, send an unsigned or wrongly signed test event to the webhook URL and confirm it is refused (4xx) and nothing changes.
Evidence attached to the report
- Code excerpt of the signature check
- Request/response for the forged event, showing refusal
- Screenshot showing no order/entitlement created by the forged event
A pass or fail on this item is accepted only with:
- a request and response
- a screenshot
- a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Payment providers other than Stripe, and webhooks from other services, such as email or sign-in providers.
Sources
PM-03 · A paid feature cannot be unlocked without a confirmed payment
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Apps that grant access from the success page or a browser-side flag let users skip paying entirely.
How the reviewer checks it
- As a free sample user, open the checkout success/return URL directly (with a made-up or reused session id) and check whether paid features unlock.
- Send a request that updates your own profile row to set the plan, subscription or credits field (for example a PATCH to the profiles table through the database API). Confirm it is refused and that no paid feature unlocks after reload.
- In code, confirm access is granted only after the server confirms payment (webhook or server-side retrieval with payment_status checked).
Evidence attached to the report
- Screenshots of the success URL visited without payment, showing no unlock
- Request/response for the direct entitlement write attempt, refused
- Code excerpt of where entitlement is granted
A pass or fail on this item is accepted only with:
- a screenshot
- a request and response
- a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Calling a paid feature's server directly as a free user (the reviewer tries to switch paid access on, not to use each paid feature), and whether access is removed after a refund, a chargeback or a cancelled subscription.
Sources
PM-04 · A repeated payment event does not grant credit or create an order twice
Severity: high Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Stripe can deliver the same event more than once and out of order; naive handlers double-credit accounts or send duplicate orders.
How the reviewer checks it
- In code, confirm the handler records processed event ids (or checkout session ids) and skips ones already handled, ideally with a unique constraint.
- In Stripe test mode on staging, complete one purchase, then resend the same event from the Stripe dashboard or CLI.
- Confirm credits, orders, emails and entitlements were created exactly once.
Evidence attached to the report
- Code excerpt of the de-duplication logic and the table/constraint it uses
- Stripe event delivery log showing the resend
- Database rows or screenshots showing a single resulting order/credit
A pass or fail on this item is accepted only with:
- a screenshot
- a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Events arriving in the wrong order, which the steps do not try; event types other than the one purchase the reviewer resent, such as renewals or refunds; and effects in outside systems, such as a fulfilment partner, beyond what the builder can show.
Sources
PM-02 · Prices and totals are set on the server
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
If the browser sends the amount, a user can edit it and pay one cent; if money is held in floating-point numbers, totals drift by rounding.
How the reviewer checks it
- Capture the request that starts checkout and change any price, amount, currency, discount or quantity field it carries; confirm the Stripe Checkout total does not follow the edit.
- In code, confirm the checkout session is created server-side from Stripe Price ids or a server-side price table.
- Fail if a money column is float, double or real, or if an amount sent to Stripe comes from floating-point arithmetic on non-integer values.
Evidence attached to the report
- Edited checkout request and the resulting Checkout page/total (test mode)
- Code excerpt creating the checkout session
- Schema listing of money columns with their types
A pass or fail on this item is accepted only with:
- a request and response
- a code excerpt
- a screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Whether the prices set up in Stripe are the right amounts; tax and currency conversion; and charges made outside the checkout, such as manual invoices or plan changes.
Sources
Data exposure
DX-01 · Data cannot be read or changed without logging in
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
If the database is not locked down, anyone on the internet can read or change the app's data without an account.
How the reviewer checks it
- Log out and record every data request the app makes, using the browser's network tab.
- Repeat each request with no login token.
- For Supabase apps, query each table in an exposed schema directly with the public anon key (views are checked under DX-02, database functions under DX-06).
- For Firebase apps, read and write each Firestore or Realtime Database path with the web config and no sign-in.
- Pass only if no request without a login token returns rows or changes data, apart from tables the builder listed in writing as public before the review.
Evidence attached to the report
- For each request, the request sent, the response status and the number of rows returned.
- A screenshot of at least one response.
A pass or fail on this item is accepted only with:
- a request and response
- a screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Tables the builder listed in writing as public before the review, which anyone can read; views (DX-02), database functions (DX-06) and file storage (PD-03), which are checked separately; and server routes the app only calls after login, which are not retried without a login unless they are Supabase or Firebase tables.
Sources
DX-02 · Database views follow the same access rules as their tables
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
In Supabase, views skip row-level security by default, so a view can leak rows its table protects.
How the reviewer checks it
- List every view in an exposed schema.
- Query each view logged out and as an ordinary user.
- Pass if each view has security_invoker = true, or (on Postgres before 15) is not granted to anon or authenticated, or sits in a schema that is not exposed.
Evidence attached to the report
- The list of views.
- For each view, the query sent and the response.
A pass or fail on this item is accepted only with:
- a request and response
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Whether the tables under each view are themselves locked down (DX-01, AR-01), and views in schemas that are not exposed to the browser.
Sources
DX-03 · Responses carry only the fields the screen needs
Severity: high Included in: Launch Check, Signed Review, Sale & Raise Attestation.
A screen can show three fields while its data request returns twenty, such as other users' emails or internal notes.
How the reviewer checks it
- Log in as an ordinary user.
- For each main screen, compare the fields in the network response with the fields shown on screen.
- Main screens are those on the builder's screen list. Fail only for another user's personal data or fields the builder marks internal; extra fields from the user's own rows are noted.
Evidence attached to the report
- The response for each main screen, with extra fields marked.
A pass or fail on this item is accepted only with:
- a request and response
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Screens not on the builder's screen list; internal fields the builder did not mark as internal; and extra fields from the user's own records, which are noted but do not fail.
Sources
DX-04 · Web addresses carry no keys, tokens or personal data
Severity: high Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Web addresses end up in browser history, server logs and analytics tools.
How the reviewer checks it
- Use the main flows, including login, password reset and file downloads.
- Record every address and query string visited or requested.
- Single-use codes in sign-in, reset or magic links and signed storage URLs with an expiry are allowed. Fail if a path or query string carries an API key, a session or refresh token, an email, a phone number or an ID number.
Evidence attached to the report
- The list of addresses, with anything sensitive marked.
A pass or fail on this item is accepted only with:
- a request and response
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Single-use sign-in and reset codes and expiring file links, which are allowed in addresses; other personal details such as names or home addresses, which do not fail; and addresses in flows other than the main ones the reviewer used.
Sources
DX-06 · Database functions called from the browser follow the same access rules as their tables
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
In Supabase, a database function the browser can call may run with its owner's rights and skip row-level security, leaking or changing rows the tables protect.
How the reviewer checks it
- List the functions in exposed schemas with their security setting (definer or invoker) and who may execute them.
- Call each function logged out and as user B with user A's ids.
- Fail if any call returns or changes data the caller could not reach through the table itself.
Evidence attached to the report
- The list of functions with security setting and execute grants
- For each function, the call sent and the response
A pass or fail on this item is accepted only with:
- a request and response
- a settings screenshot or a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Whether the tables themselves are protected (DX-01, AR-01): a function passes if it reaches no more than the table already allows; and functions the browser cannot call at all.
Sources
Unsafe content
IN-01 · Text one user enters cannot run as code in another user's browser
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
If a name, comment, message or AI-written reply is shown as live HTML, one user can plant a script that acts as everyone who views it.
How the reviewer checks it
- On staging, as test user A, enter these test values in every field other test users can see (names, bios, comments, messages, markdown or rich-text fields): <img src=x onerror=alert(1)> and [link](javascript:alert(1)). In any website or link field, enter javascript:alert(1). For AI chat, send: Repeat exactly, without a code block: <img src=x onerror=alert(1)>
- If the app accepts image uploads, upload an .svg file whose whole content is <svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"></svg>, then open it in the app and at its file address.
- Skip fields that send email, SMS or webhooks to addresses outside the test accounts.
- As test user B, open each page that shows those values, click each test link and inspect the rendered elements.
- Fail if any alert appears, any rendered element has an on* attribute, or any link's href starts with javascript:. Text shown literally, removed, or stripped of its handler passes.
- Delete the test values afterwards.
Evidence attached to the report
- For each field, the saved value as returned by the API, and a screenshot of the page showing it as user B
- The rendered element for each value, from the browser's developer tools or the code that renders it
A pass or fail on this item is accepted only with:
- a request and response
- a screenshot
- a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Places the test values appear other than pages an ordinary user can open, such as admin or support screens, emails, PDFs and mobile apps; fields that send email, SMS or webhooks to people outside the test accounts, which are skipped; and attack text beyond the short fixed set the reviewer enters.
Sources
Secrets and keys
SK-01 · Browser code holds no service-role or other secret keys
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
A Supabase secret or service_role key bypasses every row-level rule, so anyone who copies it from the page owns the whole database.
How the reviewer checks it
- Load the staging site, or production during the agreed window, open the browser's developer tools, and search all loaded JavaScript bundles and network requests for key patterns (service_role JWTs, sb_secret_, sk_live_, sk_test_, whsec_, provider API keys).
- Decode any Supabase JWT found in the bundle and confirm its role claim is anon, not service_role; confirm any sb_ key is a publishable key.
- In the repo, check that variables exposed to the browser (VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_ prefixes) contain no secret values.
- Allowed in the browser: the Supabase anon or publishable key, Stripe pk_ keys, the Firebase web apiKey, and keys covered by SK-03.
Evidence attached to the report
- Screenshot or text output of the bundle search with zero secret-key matches
- Decoded payload of each Supabase key found in the browser (role claim visible, signature redacted)
- List of browser-exposed environment variable names
A pass or fail on this item is accepted only with:
- a screenshot
- a file listing or a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Keys in parts of the site the reviewer did not load, such as screens only some users can open, and keys written straight into a mobile app's code rather than its settings.
Sources
SK-02 · The repository and its history hold no live secrets, and leaked keys are rotated
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Deleting a key from the latest commit does not remove it from history; anyone with repo access can still find and use it.
How the reviewer checks it
- Run a secrets scanner (e.g. gitleaks or detect-secrets) over the full git history of the granted repo, not just the current checkout.
- Check that .env files are git-ignored and that no .env, key file or config with credentials is committed.
- For every live-looking hit, ask the builder to confirm it was revoked or rotated, and check the provider dashboard (or builder screenshot) shows the old key inactive.
- Fail if any hit is a real credential (not a placeholder) without proof that it was revoked or rotated.
Evidence attached to the report
- Scanner command and summary output covering all commits (secrets redacted)
- File listing of .gitignore and root config files
- Rotation evidence for each finding (dashboard screenshot with key prefixes only)
A pass or fail on this item is accepted only with:
- a file listing
- a screenshot or a settings screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Repositories, branches or forks the reviewer was not given; secrets kept outside git, such as chat logs, shared drives or hosting settings; secrets the scanner does not recognise; and whether a leaked key was used by someone before it was replaced.
Sources
SK-03 · Third-party keys that must sit in the browser are restricted to this app and the APIs it uses
Severity: high Included in: Launch Check, Signed Review, Sale & Raise Attestation.
An unrestricted browser key (maps, AI, email, analytics) can be copied and used by anyone, running up the builder's bill or abusing their account.
How the reviewer checks it
- List every third-party key visible in browser code or network requests and identify its provider.
- For each, ask the builder for a screenshot of the provider's key settings showing website/referrer (or app) restrictions and an API restriction list.
- Fail if a provider offers website, referrer or app restrictions and none are set. Secret server-side keys found in the browser are failed under SK-01, not here.
Evidence attached to the report
- Inventory of browser-visible third-party keys (prefixes only) and their providers
- Provider key-settings screenshots showing application and API restrictions
A pass or fail on this item is accepted only with:
- a settings screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Keys from providers that offer no website or app restriction, which pass even though anyone can copy them; limits on which of the provider's services a key may use, which are recorded but do not fail; and whether a restriction actually stops misuse, since the reviewer sees the builder's screenshot of the setting, not a test.
Sources
Authentication and sessions
AU-01 · Password reset links are single-use, expire, and do not reveal which emails have accounts
Severity: high Included in: Launch Check, Signed Review, Sale & Raise Attestation.
A weak reset flow lets someone take over an account or learn who uses the app, without ever knowing a password.
How the reviewer checks it
- Request a reset for a sample account and for an unregistered email; compare the on-screen message and response body for both.
- Use the reset link once, then try the same link again; also try a link older than the configured expiry if the builder can supply one.
- Fail if no link expiry is configured, if a used link works a second time, or if the wording differs between registered and unregistered emails.
Evidence attached to the report
- Screenshots or responses for registered vs unregistered email (showing identical wording)
- Screenshot of the reused reset link being refused
A pass or fail on this item is accepted only with:
- a request and response
- a screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Whether a link really stops working at its expiry when the builder cannot supply an old link (the reviewer then relies on the configured setting); whether the sign-up or login pages, or response times, reveal which emails have accounts; and other ways in, such as magic links or social login.
Sources
AU-02 · Logging out revokes the session on the server, and any copied access token stops at its expiry
Severity: high Included in: Launch Check, Signed Review, Sale & Raise Attestation.
If logout only clears the browser, a copied token keeps working on a shared or stolen device.
How the reviewer checks it
- On staging, log in as a sample user and copy the refresh token and access token from the session.
- Log out through the app's own logout button, then try to exchange the copied refresh token for a new session.
- Call a data endpoint with the copied access token and note how long it keeps working (it should stop at expiry); check in code which sign-out scope the app uses.
- Pass if the refresh token is refused after logout and the old access token stops working no later than its exp claim, which must not exceed the project's configured JWT expiry.
Evidence attached to the report
- Request/response showing the refresh token refused after logout
- Request/response with the old access token and its exp claim, noting when it stopped working
- Code excerpt of the logout call
A pass or fail on this item is accepted only with:
- a request and response
- a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Sessions on other devices (it checks the device that logged out) and tokens stolen before logout and used before they expire.
Sources
AU-03 · Sessions and access tokens expire on a defined schedule
Severity: medium Included in: Signed Review, Sale & Raise Attestation.
Long-lived tokens give a thief or a forgotten shared computer far more time to act as the user.
How the reviewer checks it
- Decode a sample access token and record the gap between its iat and exp claims.
- Ask the builder for a screenshot of auth session settings (JWT expiry, inactivity timeout, time-box) or the equivalent config for non-Supabase stacks.
- Compare the values with the builder's stated policy and note any session with no upper limit.
- Fail if there is neither a time-box or inactivity setting nor a written maximum session lifetime from the builder.
Evidence attached to the report
- Decoded token header/payload (signature redacted) showing iat and exp
- Auth settings screenshot showing token expiry and any inactivity or maximum session limits
A pass or fail on this item is accepted only with:
- a settings screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Whether the limits are short enough for this app, and whether a written limit is enforced: a written maximum session length from the builder passes without a matching setting. Differences between the settings and the builder's stated policy are noted, not failed.
Sources
AU-04 · Repeated login and code attempts are rate limited
Severity: high Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Without limits, attackers can try leaked passwords or guess one-time codes until they get in.
How the reviewer checks it
- Ask the builder for a screenshot of auth rate-limit settings (Supabase: Authentication > Rate Limits) or the equivalent for their stack.
- With the builder's agreement and against staging only, make 10 wrong-password attempts on a sample account, and on staging with a reviewer-owned email address at most three password-reset requests in a row; record whether a limit or delay appears. Do not load test.
- If the app has its own login or OTP endpoint (edge function, API route), check in code that it applies a limit rather than bypassing the platform's.
- Fail if neither the settings nor the attempt series shows a limit.
Evidence attached to the report
- Rate-limit settings screenshot
- Response log of the limited attempt series (e.g. 429 or lockout message), or a note that no limit appeared
- Code excerpt for any custom login endpoint
A pass or fail on this item is accepted only with:
- a settings screenshot
- a request and response
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Guessing spread across many devices or accounts, heavy traffic, and the live site, since the attempts run on staging only (ten wrong passwords and up to three reset requests). A limit shown in the settings passes even if those attempts met none. Guessing one-time codes is checked from settings and code, not by trying codes.
Sources
Backups and recovery
BR-01 · Database backups are switched on and a restore has actually been tried
Severity: high Included in: Launch Check, Signed Review, Sale & Raise Attestation.
A backup nobody has restored is a guess; free-tier projects may have no automatic backups at all.
How the reviewer checks it
- Ask the builder for a screenshot of the database backup page showing plan, schedule and most recent backup date.
- If the project has no automatic backups (e.g. Supabase free tier), ask for evidence of a regular export routine (e.g. scheduled db dump stored off-platform).
- Ask the builder to restore a recent backup or dump into a separate project or staging database and show the row count of one table in the restored copy beside the builder's count for the same table at backup time; no row contents.
- Pass needs automatic backups or an off-platform export at least weekly, plus a restore done within the last 90 days.
Evidence attached to the report
- Backup settings screenshot with dates
- Restore evidence: screenshot or log of the restored database and matching sample data, with the restore date
A pass or fail on this item is accepted only with:
- a settings screenshot
- a screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Uploaded files, which a database backup does not include; whether everything came back in the restore (the reviewer compares the row count of one table); how long a full restore takes; and data added since the last backup, which can be up to a week old.
Sources
Error handling and logging
EL-01 · Error messages shown to users do not reveal internals
Severity: medium Included in: Signed Review, Sale & Raise Attestation.
Stack traces, SQL errors and raw provider responses show attackers table names, policies and keys to aim at.
How the reviewer checks it
- Trigger ordinary failures on staging: submit malformed form data, request a missing record, call an endpoint without a token, and break a required field.
- Inspect both the on-screen message and the raw network response for stack traces, SQL text, file paths, keys or tokens.
- In the repo's production configuration (env files, framework config), confirm debug and verbose error modes are off.
- Fail on stack traces, file paths, SQL text, keys or tokens. Standard Supabase or PostgREST error codes that name a table or policy are noted, not failed.
Evidence attached to the report
- Screenshots of the user-facing error messages
- Raw response bodies for the triggered errors (redacted where needed)
- Excerpt of the production configuration showing debug and verbose errors off
A pass or fail on this item is accepted only with:
- a screenshot
- a request and response
- a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Errors that only appear under load or in flows the reviewer did not trigger, and standard database error messages that name a table or access rule, which are noted, not failed.
Sources
EL-02 · Passwords, tokens, card data and personal data are not written to logs
Severity: high Included in: Signed Review, Sale & Raise Attestation.
Logs are copied widely and kept long; anything sensitive written there leaks to everyone with log access.
How the reviewer checks it
- Search the repo's source and server functions for logging calls (for example the pattern console.log|logger.|print( ) that output request bodies, headers, auth tokens, webhook payloads or user objects.
- Perform a login, a password reset and a test payment on staging, then ask the builder for the matching edge function / server log lines.
- Check those lines for passwords, full tokens, card details, or full webhook payloads or user objects containing an email or phone number.
Evidence attached to the report
- List of logging calls reviewed, with file paths
- Redacted log excerpts covering login, reset and payment
A pass or fail on this item is accepted only with:
- a file listing
- a screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Logs from flows other than login, password reset and payment, which are checked only by reading the code; logs kept by outside services the builder cannot show; and who can read the logs and how long they are kept.
Sources
EL-03 · Failed logins and refused access attempts are recorded
Severity: medium Included in: Signed Review, Sale & Raise Attestation.
Without a record of failures, nobody can tell whether accounts are being guessed or data probed, or reconstruct an incident.
How the reviewer checks it
- Make five failed logins on a sample account and one refused insert as another user (from AR-03) on staging, noting the times.
- Ask the builder to show where these appear (auth logs, API/database logs, app logs) and confirm entries carry time, account or IP, and outcome.
- Note how long these logs are kept.
- Fail if any of the failed logins or the refused insert has no log entry; a gateway log line showing the status and path counts as an entry.
Evidence attached to the report
- Redacted log entries for the failed logins and the refused request, with timestamps
- Screenshot or note of log retention period
A pass or fail on this item is accepted only with:
- a screenshot
- a settings screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Whether anyone reads or acts on the logs; it checks that the entries exist.
Sources
Third-party services
TP-01 · Dependencies have no known critical or high vulnerabilities left unaddressed
Severity: high Included in: Signed Review, Sale & Raise Attestation.
AI-built apps pull in many packages; a known-vulnerable one can open a hole the builder never wrote.
How the reviewer checks it
- In the repo, run npm audit --omit=dev (or the pnpm/yarn equivalent) against the lockfile and record counts by severity.
- Fail if a critical or high finding in a runtime package has a fixed version available and no written reason from the builder to defer it.
- If the platform offers a dependency scan (e.g. Lovable's quick scan), attach its latest result as well.
Evidence attached to the report
- Audit command and summary output with severity counts
- Table of critical/high findings with status (fixed, planned with date, not reachable with reason)
A pass or fail on this item is accepted only with:
- a screenshot
- a file listing
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Development-only packages; problems not yet published; medium and low findings; critical or high findings with no fix yet, or that the builder gave a written reason to put off (both listed in the report); packages loaded from other websites while the app runs; and code outside JavaScript packages, such as Python.
Sources
TP-02 · Personal data sent to analytics, trackers or chat widgets matches the privacy policy
Severity: high Included in: Signed Review, Sale & Raise Attestation.
Emails, names or record contents passed to trackers leave the builder's control and may breach their own privacy promises.
How the reviewer checks it
- Using a sample account, walk the main screens with the network tab open and list every third-party domain contacted.
- Inspect requests to analytics, session-recording, ad and support-widget domains for emails, names, user-entered content or full URLs carrying ids.
- Compare what is sent with what the privacy policy says is shared.
- Fail if any email, name or user-typed content reaches an analytics, session-recording or ad domain and the privacy policy does not name that sharing.
Evidence attached to the report
- List of third-party domains contacted
- Redacted request payloads to each tracker showing which fields are sent
A pass or fail on this item is accepted only with:
- a request and response
- a screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Data sent from the app's server to outside services; screens beyond the main ones; data sent to support or chat widgets and ids in web addresses, which are noted but do not fail; and whether the privacy policy itself meets the law.
Sources
Privacy and data deletion
PD-01 · Deleting an account removes the user's records, files and sessions
Severity: high Included in: Signed Review, Sale & Raise Attestation.
Users expect deletion to remove everything, yet uploaded files and child records are often left behind.
How the reviewer checks it
- On staging, create a sample user, add records and upload a file, and keep a copy of the session tokens.
- Delete the account through the app's deletion feature or the builder's documented admin process.
- Check that the user's rows in app tables (via foreign keys with ON DELETE CASCADE or explicit cleanup) and their storage objects are gone, the old refresh token is refused, and requests made with the old access token return none of the user's data.
- Fail if there is no deletion feature or documented admin process.
Evidence attached to the report
- Before/after query output or screenshots for the user's rows and storage objects
- Request/response showing the old session refused
- Code or SQL excerpt of the deletion process
A pass or fail on this item is accepted only with:
- a screenshot
- a request and response
- a code excerpt
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
Copies in backups, logs and outside services such as email, analytics or the payment provider, which the reviewer does not check, and content the user left in other people's records, such as messages.
Sources
PD-03 · Private uploads are not publicly reachable
Severity: critical Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Files in a public bucket open for anyone who has the link.
How the reviewer checks it
- List storage buckets and their public/private setting; flag any public bucket holding user documents, IDs, invoices or private photos.
- Upload a private file as sample user A, copy its URL, and open it logged out and as user B: both must be refused. A signed URL must stop working after its stated expiry.
Evidence attached to the report
- Bucket list with public/private settings and storage.objects policies
- Responses for the logged-out and user B file requests, and for the expired signed URL
A pass or fail on this item is accepted only with:
- a settings screenshot
- a request and response
- a screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
File storage outside the app's own storage buckets; links a user has chosen to share; and how long signed links stay valid, which can be set very long and still pass.
Sources
Security contact
SC-01 · The app publishes a working way to report security problems
Severity: medium Included in: Launch Check, Signed Review, Sale & Raise Attestation.
Without a published contact, a person who finds a flaw has no safe way to tell the builder, and may share it publicly instead.
How the reviewer checks it
- Request /.well-known/security.txt on the app's domain and record the response.
- Confirm it has a Contact field and an Expires date in the future.
- Ask the builder to confirm that a test message sent to the Contact address or form arrived.
- Fail if the file is missing, has no Contact field, has expired, or the test message did not arrive.
Evidence attached to the report
- The request and response for /.well-known/security.txt
- The builder's screenshot of the test message received
A pass or fail on this item is accepted only with:
- a request and response
- a screenshot
- steps anyone can re-run
What a pass does not show (main gaps, not a complete list)
How quickly or well the builder responds to a real report; it checks that a test message arrived.