Skip to main content

When: pre-delivery audit, pre-launch check, periodic audit; security+baseline+SEO in one run · Input: {{project_path}} {{live_url}} {{gate}} {{client_project}} {{fix}} {{jurisdiction}} · Output: PASS/FAIL + merged priority list · keywords: launch checklist, production readiness, release gate

0
Oct 30, 2026
v2

License: CC BY 4.0. The author shares the prompts of this repo under these terms.

Import with the CLI:ple import ple:zeynepaslierhan/ortak-web-en/bddee9df
117 lines
Role: You run the launch gate for a Next.js + Supabase web app. Run the security, production baseline and SEO audits, deduplicate the findings, and return one priority list and one PASS/FAIL verdict.

Inputs (below, P = project path, U = live URL):
- P = {{#if project_path}}`{{project_path}}`{{else}}`.`{{/if}} → first convert it to an absolute path with `P=$(cd <path> && pwd)` (subagents may run in another directory) · U = {{#if live_url}}`U="{{live_url}}"`{{else}}not given → record live measurements as "not measured (no live URL)"{{/if}}
- Gate = {{#if gate}}`{{gate}}`{{else}}`delivery`{{/if}} (delivery | launch | periodic)
- Backend = {{#if backend}}`{{backend}}`{{else}}`own`{{/if}} (own | shared: schema/RLS live in another repo, tenant key such as `site_key`). If shared, G3, FB2 and the schema/RLS/migration items of the sub-audits are not measured here and are not a FAIL reason: Owner = "backend repo (<name>)"; here you measure only tenant isolation (tenant filter on every query, no service-role client).
- Jurisdiction = {{#if jurisdiction}}`{{jurisdiction}}`{{else}}`global`{{/if}} (global | eu | uk | us | tr); it sets the legal baseline in FB9, G9 and G10 and is passed to SEC and BASE.
- {{#if client_project}}Client project: NO commit/push; report + patch proposal + client summary only.{{else}}Own project: commit only by explicit file path, one commit per item.{{/if}}
{{#if standards_dir}}- "Read index.md in {{standards_dir}} and also apply the relevant rule tables."{{/if}}

Fixed rules: never read or print a .env or key VALUE (key name + location + leak type only; use `-l` with grep) · record what you did not measure as "not measured (reason)" · never invent a threshold, version or CVE · give version facts with a date · no "not checked" state: every G/FB row ends as passed / failed / not measured / not applicable · `\|` in tables is a markdown escape; type `|` in the shell.

## Step 0 — Stack detection
- [ ] Package manager from the lock file (`package-lock.json` npm · `pnpm-lock.yaml` pnpm · `yarn.lock` yarn · `bun.lock*` bun) → `<pm>` below
- [ ] `next` version from the INSTALLED package: `node -p "require('$P/node_modules/next/package.json').version"` (otherwise the lock file; do not use `npx next --version`, it downloads the latest version if none is installed and gives a wrong answer)
- [ ] App Router (`app/` or `src/app/`) or Pages Router; request middleware is `middleware.ts`, or `proxy.ts` on Next 16+. The file sits at the SAME level as `app/`: with `src/app`, a root-level file is silently not compiled → protection that relies only on it = P0 (FB3). Verify in the build output: `proxy.ts` (Node only) or a Node `middleware.ts` → command 1 ≥ 1 (`middleware-manifest.json` stays empty, do not count it as evidence); edge `middleware.ts` (still the default in 16) → command 2 ≥ 1:
  `grep -c '"/_middleware"' "$P/.next/server/functions-config-manifest.json"` · `node -p "Object.keys(require('$P/.next/server/middleware-manifest.json').middleware).length"`
- [ ] `@supabase/*` packages (is `@supabase/ssr` present), auth method; hosting (`vercel.json`, `.vercel/`), i18n, payment provider, `test` and `build` script names, whether `supabase/migrations/` and `supabase/tests/` exist
- [ ] Put checks that do not apply under "Not applicable" with a reason.

## Step 1 — Repo ↔ live parity
| Check | Command | Threshold |
|---|---|---|
| Undeployed commits | `git -C "$P" fetch -q; git -C "$P" rev-list --count @{u}..HEAD` (no upstream → "not measured"); uncommitted changes: `git -C "$P" status --porcelain \| wc -l` | ahead = 0; dirty files do not count as live |
| Live build identity | Git SHA of the prod deployment (Vercel dashboard → deployment → Source, or `vercel inspect <prod-deployment-url>`; if the app exposes `VERCEL_GIT_COMMIT_SHA` on a version endpoint, `curl -s U/<endpoint>`) ↔ `git -C "$P" rev-parse origin/<prod-branch>` | SHA 1:1; SHA not found → not measured |
| Live truth | Measure every "done" claim on U with curl/headers/browser | "live" claims while ahead > 0 = 0 |

If ahead > 0 or the SHA differs, open a "live ≠ repo" P1 finding; do not count a fix that is not live as "present".

## Step 2 — Pull and run the three audits via PLE
1. `command -v ple` → if missing, go to Step 3 and record it under "Not measured" and in a "PLE feedback:" line.
2. Syntax (the pack's only form, `ple get --help`): `ple get "<full title>" --var key=value` — one `--var` per variable. Add to every command: if U exists `--var live_url="$U"` · if client project `--var client_project=1` · if Backend = shared, add `--var shared_backend="<backend repo>"` to the security call only{{#if standards_dir}} · `--var standards_dir="{{standards_dir}}"`{{/if}}. Do NOT pass `fix` to the sub-audits; fixes happen in one place in Step 6. `ple get` lists unfilled variables in an "Unfilled variables" warning; supply their values from Step 0.

| Order | Code | Command |
|---|---|---|
| 1 | SEC | `ple get "Web security audit (Next.js + Supabase)" --var project_path="$P" --var jurisdiction=<Jurisdiction>` |
| 2 | BASE | `ple get "Web production baseline audit" --var project_path="$P" --var product_type=<Step 0> --var jurisdiction=<Jurisdiction>` (add `--var has_sales=1` if a payment provider was found) |
| 3 | SEO | `ple get "Web SEO audit" --var project_path="$P"` |

3. If subagents are supported, run all three in parallel: give each the prompt text + Step 0/1 output + the Backend value + "report only, no file edits, no commits, do not start `next build`/`next dev`". Parallel builds collide in the same `.next` (observed): run the build once yourself (Step 5); subagents use that output and a single server (U or your `<pm> start`). Otherwise run them in order SEC → BASE → SEO. Collect the feedback lines from the sub-reports.
4. If `ple get` errors or the prompt is not found: first confirm the title with `ple search "Web"`, add `--project <repo>` if titles collide; if it still fails, copy the error verbatim under "Not measured" and apply the Step 3 fallback list for that audit.
5. Gate checks (G): G1-G3 belong to the gate; always measure them yourself. G4-G10 are owned by a sub-audit item: take the result from that item, do not rewrite the check here; if the sub-report says "not measured" and you have access, measure it yourself with the owning item's command.

| ID | Check | Verification | Threshold | Severity |
|---|---|---|---|---|
| G1 | Post-deploy smoke | `curl -sI` on `$U/`, /sitemap.xml, /robots.txt, login/checkout/main flow, favicon, manifest icons, og:image (URL taken from the meta tag), font (.woff2); is the smoke test automated in CI/deploy (workflow file) | public 200; protected 3xx/401/403; 5xx = 0; font 200 + `font/woff2`; not automated → P2 | P1 (font P2) |
| G2 | Protected API without a session (post-deploy, all routes) | Route list: `find "$P" -path '*/node_modules' -prune -o -path '*/app/api/*' -name 'route.*' -print`; for each protected route `curl -s -o /dev/null -w '%{http_code}' "$U/api/<route>"` | 401/403; 5xx = 0 | P0 |
| G3 | RLS test evidence | `supabase start && supabase db reset && supabase test db` (needs Docker); are there scenarios for anon cannot read, cannot see another tenant, cannot escalate role | green; no `supabase/tests/` → failed; "build green + types clean" is not evidence | P1 |

| ID | Check | Owning item |
|---|---|---|
| G4 | Restore drill | BASE I2 |
| G5 | Monitoring (errors 2/2 + uptime alert) | BASE H1 + H3 |
| G6 | Domain/TLS (expiry, CAA, transfer lock; 4 host variants) | BASE K1-K3 + SEO C |
| G7 | Search Console property + sitemap | SEO N |
| G8 | Core Web Vitals field p75 (lab data alone does not decide) | SEO J |
| G9 | Cookie consent (tracking requests before consent = 0) | BASE F3 |
| G10 | Accessibility + cross-border data transfer (+ European Accessibility Act for EU sales) | BASE D1-D6 + F6 (+ F17) |

## Step 3 — Built-in fallback list (if PLE is missing or a prompt is not found; max 15)
Without a sub-report, G4-G10 results also come from this list.
| # | Check | Verification | Severity |
|---|---|---|---|
| FB1 | Secret leak | `grep -rn "NEXT_PUBLIC_" "$P" --include='*.ts*' --include='*.js*' --include='.env.example' --exclude-dir=node_modules --exclude-dir=.next \| grep -iE "SERVICE_ROLE\|SECRET\|ADMIN"` = 0; history: `git -C "$P" log --all --format='%h %ad' --name-only -- '.env' '.env.*' ':!*.example'` empty (do NOT use `-p`, it prints values); if available `gitleaks detect --redact -s "$P"`; after build `grep -rlE 'sb_secret_\|zZXJ2aWNlX3JvbG\|c2VydmljZV9yb2xl\|NlcnZpY2Vfcm9sZ' "$P/.next/static"` = 0 files | P0 |
| FB2 | RLS | `select relname from pg_class where relrowsecurity=false and relnamespace='public'::regnamespace and relkind='r';` = 0 rows (locally via psql after `supabase db reset`; for prod, have the user run it in the SQL Editor) + Supabase Advisors "RLS Disabled in Public" = 0 | P0 |
| FB3 | Server-side authorization | authn+authz in every `'use server'` and route handler; no `getSession()` in authz (`getUser()`/`getClaims()`); routes relying only on `middleware.ts`/`proxy.ts` = 0 | P0 |
| FB4 | Known critical vulnerable version | Installed `next` version (Step 0): 2026-10-30 baseline 16.x ≥ 16.4.1 · 15.x ≥ 15.5.28 (2026-10-14 Next.js advisory; covers the CVE-2025-29927 and React2Shell CVE-2025-55182 patches); current advisories: `npm view next version`, GitHub security advisories, `<pm> audit` (npm: `npm audit --omit=dev`); threshold: affected version = 0, critical/high prod dependencies = 0 | P0 |
| FB5 | Security headers | `curl -sI U`: unsafe-inline in CSP script-src = 0, frame-ancestors present; HSTS max-age ≥ 63072000 | P1 |
| FB6 | Protected API | Same as G2 | P0 |
| FB7 | Backup + restore | Automatic backups on (on Free, a scheduled off-site `supabase db dump`) + restore record into an isolated project (date + result), ≥ 1 per year | P1 |
| FB8 | Monitoring | Deliberate test error shows in client + server dashboards (2/2); external uptime probe ≤ 5 min + test alert delivered | P1 |
| FB9 | Legal pages | Privacy notice, cookie and terms pages return `curl -sI` 200 and are linked from the footer (`curl -s U \| grep -ciE 'privacy'` > 0). Baseline per Jurisdiction: global/eu/uk = GDPR-grade (lawful basis, privacy notice, cookie consent where reject is as easy as accept, data subject rights, DPA with processors, breach notice) · us = CCPA/CPRA basics (notice at collection, "Do Not Sell or Share" link where applicable) + CAN-SPAM for e-mail · tr = KVKK aydınlatma metni, çerez politikası, kullanım koşulları; with sales also mesafeli satış sözleşmesi + ön bilgilendirme formu and ETBİS registration; VERBİS registration where required (threshold (not verified)); footer grep pattern becomes `'gizlilik\|privacy'` | P1 |
| FB10 | robots/noindex | `curl -s U/robots.txt` has no `Disallow: /` in prod; no meta robots / X-Robots-Tag noindex on key pages | P1 |
| FB11 | Sitemap | `U/sitemap.xml` 200 + XML; 3 sample URLs from it return 200 | P1 |
| FB12 | Canonical | Indexed pages carry an absolute `rel="canonical"` pointing at the prod host; a page that does not define `alternates` inherits the layout's (shallow merge) → the canonical of 404/utility pages is not the home page | P2 |
| FB13 | 404 status code | `curl -s -o /dev/null -w '%{http_code}' U/nonexistent-page-check` = 404 and on a dynamic route `U/<dynamic-segment>/missing-record` = 404 (soft 404 = 0). Once streaming starts the status is locked at 200: if the segment or a parent has `loading.tsx`, or `notFound()` sits inside a suspended Suspense, you get 200 + noindex → check the record before streaming, use Suspense only around the slow component | P1 |
| FB14 | CWV field | `curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=$U&strategy=mobile" \| jq '.loadingExperience.metrics'` p75: LCP ≤ 2.5 s · INP ≤ 200 ms · CLS ≤ 0.1; no field data → not measured | P1 |
| FB15 | Account deletion | An in-app path deletes the account + personal data (deletion route/action in code) and works end to end with a test account | P1 |

## Step 4 — Deduplicate and build one priority list
- [ ] Same root cause (same file:line, same URL + header, or same rule) → one row; list every source in the "Source audit" column (e.g. SEC-3, BASE-5, G2).
- [ ] Severity = the highest of the merged findings. Order: severity → measured live first → small fix first.
- [ ] Every row carries evidence: the Finding cell has Where (file:line/URL) + Measured + Expected; the Fix cell has the minimal fix + a verification command.
- [ ] Scale (shared across the pack): P0 critical (live data/account/payment/secret risk, now) · P1 high (before delivery) · P2 medium · P3 low.
- [ ] If Gate = periodic, diff against the previous report: new / closed / still open with age (days from the found date to today).

## Step 5 — Gate criteria (FAIL if any one is not met; an unmeasured criterion counts as FAIL)
| Criterion | PASS condition | Evidence |
|---|---|---|
| P0 | P0 count = 0 | merged list |
| P1 | Owner + date for every open P1 | Owner column + date |
| Test + build | `<pm> test` and `<pm> run build` exit 0 (no test script → FAIL + P1 finding); G3 RLS tests green (Backend = shared: G3 lives in the backend repo, not a FAIL) | last lines of command output |
| Smoke | G1 + G2 green after deploy | curl status code list |
| Live = repo | ahead = 0 and SHA 1:1 | Step 1 output |

If Gate = launch, G6 and G7 are also gate criteria.

Local delivery (Gate = delivery, changes not deployed, release decision pending): write `RELEASE PENDING` instead of FAIL on the Smoke and Live = repo rows; run G1/G2 against `<pm> start` after the build (`U=http://localhost:3000`) and label them "local". If all other criteria pass, the verdict is `PASS — release pending`; it becomes final after deploy via the last item of Step 6. No exception for launch or periodic.

## Step 6 — Fix and re-measure
{{#if fix}}{{#if client_project}}- In P0 → P1 order, write a minimal patch/branch proposal (diff + suggested test); do NOT commit or push.{{else}}- In P0 → P1 order, fix with a minimal diff + test; one commit per item, explicit file paths only.{{/if}}{{else}}- Do not fix; the Fix column carries the minimal proposal.{{/if}}
- [ ] Rerun the verification command of every fixed row; status: found → fixed (repo) → verified live.
- [ ] After deploy, re-measure Step 1 and G1/G2 and issue the gate verdict again. A fix not verified live does not pass the gate.

## Output skeleton (in this order)
- `## Verdict` — PASS / FAIL / `PASS — release pending` + one-sentence reason
- `## Gate criteria` — | Criterion | Status | Evidence |
- `## Findings` — merged priority list: | # | Severity | Area | Finding | Source audit | Fix | Owner |
- `## Checked and clean` — passed G/FB and sub-audit items (shows coverage; never skip)
- `## Audit summaries` — two lines each for SEC, BASE, SEO, G (source run: PLE or fallback list)
- `## Not measured` — item · reason · what is needed to measure it
- `## Not applicable` — item · reason
- `## Next 3 steps` — with owner + date
{{#if client_project}}- `## Client summary` — max 6 lines, non-technical language; no file paths, commands or internal tool names. The technical list in the report is for internal use; keep it separate from the client summary.{{/if}}
- `## PLE feedback` — lines collected from the sub-reports and seen in this run

Feedback: If this prompt missed a check, gave a wrong threshold or you hit a PLE problem, send one line: ple feedback --type prompt --prompt "Web full audit and launch gate" "<what is missing → what it should be>" (use --type bug or --type missing for PLE issues; if the command is unavailable, add a "PLE feedback:" line at the end of your report).

Related prompts