# AI app setup review kit: accounts, guardrails and recovery October 30, 2026. For a small business owner whose CRM or store app was built with an AI coding agent. Companion article: [AI API or subscription: guardrails, LiteLLM and backups](https://jwatte.com/blog/ai-api-vs-subscription-guardrails-litellm-backups/). Use this file with your own repository and an AI coding agent such as Claude Code or Codex. GitHub, Supabase and Shopify form the worked example; mark a platform check not applicable when you use something else, then record the equivalent control for your platform. The owner handles account settings and approves each change. Commands are templates that need your paths, domain, approved model IDs and secret references before use. ## How to use this file 1. Copy this file into the root of your CRM repository. If you'd rather not commit it, add `ai-app-setup-review-kit.md` and `SETUP_REVIEW_REPORT*.md` to `.git/info/exclude`. 2. Open Claude Code in that folder and paste: ```text Read ai-app-setup-review-kit.md and follow its Agent rules. Do Phase 0 and Phase 1 only, read only. Write the report to SETUP_REVIEW_REPORT.md and stop. ``` 3. Get a second opinion from a different model. With Codex installed, from the same folder: ```text codex exec --sandbox workspace-write "Read ai-app-setup-review-kit.md and follow its Agent rules. Do Phase 0 and Phase 1 only, read only. Write the report to SETUP_REVIEW_REPORT_codex.md and stop." ``` Compare the two reports. Where they disagree is where to look first. That is model diversity doing real work. 4. Each report ends with a list of **owner checks**: dashboard settings only you can see (GitHub, Supabase, Shopify, AI billing). Do them and paste the results back to the agent when it asks. Never paste a key, token or password into any chat. 5. Read the report and pick the fixes you want. Then approve them one at a time: ```text Do Phase 3 for fix F3 from SETUP_REVIEW_REPORT.md on a new branch. Run the tests, open a pull request and stop. ``` 6. Before merging, have the other model review that pull request (`/ponytail-review` in Claude Code, or Codex). 7. If you have no backup outside Supabase today, do section K first: set up the T7 backup script, then run one T8 restore into a scratch project before changing anything else. Every other fix is safer once you can roll back. 8. Rerun Phase 1 once a quarter and after any big change. ## Agent rules (read before doing anything) 1. Phases 0, 1 and 2 inspect the application without changing it. Writing the requested review report is allowed. Do not edit application files, commit, push, deploy, run migrations, install tools or change settings. Phase 3 starts only when the owner approves a specific fix by its ID. 2. Never print secrets or include them in a review report: API keys, tokens, the Supabase service role or secret key, Shopify client secrets or access tokens, `.env` contents, database URLs with passwords. Report the file and variable name only. Owner-approved provisioning may write credentials to a protected secret store or owner-only temporary file for immediate import, as in T9. Do not copy existing secrets into an agent transcript. 3. Do not send this repository's code, data or logs to any outside service. The routine network calls allowed are package registry and vulnerability lookups (`npm audit`, OSV.dev queries with package names and versions); section I requires the owner's separate authorization. Report-only review does not authorize installing the optional plugin in section C. If your sandbox has no network, write the command into the report as an owner check instead. 4. No attack tools, load tests or active scans against anything. The only live check is the passive ZAP baseline in section I, against the owner's own domain, after the owner confirms the domain and authorizes that check. 5. Every finding needs evidence: a file and line, a command with its output, or a value the owner supplied. No evidence, no finding. Anything you could not check is marked "not checked" with the reason. 6. Prefer the smallest fix that solves the problem. Don't propose new frameworks or services when the existing stack already covers it. ## Phase 0: Inventory Write a short inventory at the top of the report: 1. **Stack:** framework, language, Node version, package manager, whether a lockfile is committed, where the app is hosted and how it deploys. 2. **Supabase:** client libraries, where the project URL and keys are read (variable names only), which code runs with the service role or secret key, Edge Functions (and their own dependency files), the migrations folder. 3. **Shopify:** app type and auth method, the Admin API version string in code, every webhook topic and its handler file, the access scopes requested. 4. **AI in the product:** every place in code or CI that calls a model: provider, model ID, key variable name, whether it runs automatically (cron, webhook, CI) or only when a person runs it, and whether it goes through a gateway such as LiteLLM or straight to the provider. 5. **AI in development:** which AI tools write code here, and whether `CLAUDE.md` and `AGENTS.md` exist and are current. 6. **CI:** each workflow in `.github/workflows`: triggers, `permissions:`, third party actions and how they're pinned, secrets referenced (names only), whether `.github/dependabot.yml` exists. ## Phase 1: Evaluate Give every check one status: **pass**, **warn**, **fail**, **not checked** or **owner check**. ### A. AI accounts and continuity * [ ] **A1. Software uses approved credentials.** The CRM, scheduled jobs and CI use API credentials from a server environment or secret store, or an integration explicitly supported by the provider. Do not extract a personal subscription session to power an application. Anthropic's consumer terms restrict automated access except through an API key or where Anthropic explicitly permits it [3]. Record any supported exception by name. Some subscriptions include API credits after linking an eligible API organization; this changes the bill without replacing API credentials [52][53]. Those grants exclude Claude Code and Codex API usage respectively. Read the current eligibility and expiry rules before budgeting them. * [ ] **A2. Spending caps.** Owner check: provider spending controls and billing alerts are configured, and each caller has its own gateway budget [4]. Record whether each control actually blocks requests or merely alerts. A budget label alone is not proof of a hard cap. * [ ] **A3. Separate admin identity.** Owner check: the admin and billing login for each AI provider uses an email that is not one of the daily user logins, to keep billing and appeals available if a daily login fails. This does not prevent an organization-wide suspension [4]. * [ ] **A4. Second provider ready.** A second AI provider is set up, has a key, and has done real work here in the last 30 days (code review counts). * [ ] **A5. Lockout runbook.** A written step list exists for "our main AI account was suspended this morning": who to contact, how to appeal, what switches to the second provider [4]. * [ ] **A6. Agent instructions.** `AGENTS.md` and `CLAUDE.md` exist and state how to install, test, build and deploy, plus the data rules below. Claude Code reads `CLAUDE.md`; Codex and most other agents read `AGENTS.md` [26]. One line `@AGENTS.md` in `CLAUDE.md` keeps them in sync. ### B. Model diversity guardrails * [ ] **B1. Data classes.** List what the code and prompts can contain: customer names, emails, addresses, orders, payment details, secrets. * [ ] **B2. No sensitive data to Chinese hosted APIs.** Search code and config for first party Chinese model endpoints (for example `moonshot`, `dashscope`, `aliyuncs`, `deepseek`). Pass when none exist, or when they only ever receive data with none of the classes in B1. This is our recommended data policy. The cited discussion of China's National Intelligence Law explains the concern and the disputed scope of Article 7; it is not evidence that any particular request was accessed by a government [7][8][9]. Verify the operator, processing location, subprocessors and contract for each endpoint. With a gateway (section L), enforce this per key: keys that can see customer data can't reach those models at all (L3). * [ ] **B3. Open weights hosted on purpose.** If Kimi, Qwen or DeepSeek models are used, they run locally or on a US host, with network egress and telemetry checked so prompts do not reach an unapproved endpoint, and they were tested on this project's own tasks first, since model behavior travels with the weights [7]. * [ ] **B4. Review gate.** Every AI written change reaches `main` only through a pull request with passing checks, review by a second model, and a person who approves the change (owner check on branch rules, see J2). * [ ] **B5. Your own test set.** There are 10 to 20 real tasks with known good results to rerun before switching or upgrading a model. Warn if missing. * [ ] **B6. Untrusted customer text.** Treat email, product reviews, uploaded documents and support messages as data. Test prompts that ask the model to ignore its rules, reveal another customer's records or call an unauthorized tool [13]. Keep authorization in application code. A model should draft a reply for a person to send; customer text cannot grant permission to send messages or change records. * [ ] **B7. Data controls follow the fallback.** Test PII masking or DLP with synthetic examples and verify the destination of every fallback [49][50]. A private local task must not silently fall back to a hosted API. Record the permitted providers for each data class, the log retention period, and who can read the logs. ### C. Ponytail audit Ponytail is an MIT licensed set of rules and read only review and audit commands for AI coding agents [10][11]. 1. Optional owner-approved installation in Claude Code, using two separate prompts before the report-only review: ```text /plugin marketplace add DietrichGebert/ponytail ``` ```text /plugin install ponytail@ponytail ``` Or skip the install and have the agent read and follow the audit method directly: https://github.com/DietrichGebert/ponytail/blob/main/skills/ponytail-audit/SKILL.md 2. Run: ```text /ponytail-audit the Shopify webhook handlers, the Supabase sync code and anything that runs on a schedule. Report only. For each finding give the starting state, the failed step and the wrong result. ``` 3. Cases to make sure it covers: 1. The same webhook delivered twice (is the handler idempotent?). 2. The Shopify call succeeds and the Supabase write fails, or the reverse. 3. A sync batch that stops halfway, then reruns. 4. A token or session that expires in the middle of a job. 5. Retries with no backoff or no limit. 6. Dates and times stored without a time zone. 4. Copy its findings into the report under C with their evidence. Do not apply Ponytail fixes in Phase 1. ### D. OWASP Top 10 (2025) for this stack Map each category to this codebase and record pass, warn or fail with evidence [12]: | Code | Category | What to check here | |---|---|---| | A01 | Broken Access Control | RLS on every table (section E); server routes check the signed in user owns the record; no record IDs in URLs without an ownership check | | A02 | Security Misconfiguration | Security headers, CORS rules, debug output off in production, Supabase Auth settings | | A03 | Software Supply Chain Failures | Lockfile, audit, IOC pre check, Dependabot, actions pinned (section H) | | A04 | Cryptographic Failures | HTTPS everywhere, no secrets in the client bundle, tokens stored server side | | A05 | Injection | SQL built from strings in functions or RPCs; customer fields from Shopify rendered as HTML without escaping; prompt injection if AI reads customer text | | A06 | Insecure Design | Rate limits on public endpoints, idempotent webhook handling, safe defaults | | A07 | Authentication Failures | MFA for admin users, session lifetime, redirect URL allowlist, password rules | | A08 | Software or Data Integrity Failures | Webhook HMAC verification (F1), CI workflow permissions, nothing updated without a check | | A09 | Security Logging and Alerting Failures | Logs for sign ins, admin actions and webhook failures; someone gets alerted | | A10 | Mishandling of Exceptional Conditions | Errors fail closed, no stack traces shown to users, partial failures handled (ties to section C) | If the CRM sends customer text to a model, also check the OWASP Top 10 for LLM applications, starting with prompt injection and sensitive information disclosure [13]. ### E. Supabase * [ ] **E1. RLS on every exposed table.** A table in an exposed schema without RLS is readable and writable by any role with a grant on it [14]. Owner check: run the first query in T4 in the Supabase SQL editor and paste the result. Every row returned is a fail. * [ ] **E2. No open policies.** Run the second query in T4. Each `true` policy, or policy for `anon` or `public` on a table with customer data, needs a written reason or a fix. * [ ] **E3. Views respect RLS.** Run the third query in T4. A view without `security_invoker` runs with its owner's rights and can expose rows RLS would hide. * [ ] **E4. Service role key stays on the server.** The service role or secret key bypasses RLS and must never reach the browser [14]. Search for it in client code and in public env names (`NEXT_PUBLIC_`, `VITE_`, `PUBLIC_`, `EXPO_PUBLIC_`). Search built output folders too if they're committed. * [ ] **E5. Advisors clean.** Owner check: Supabase dashboard, Advisors, Security Advisor. Paste any errors and warnings. * [ ] **E6. Backups you've tested.** Owner check: plan level. Supabase backs up Pro, Team and Enterprise projects daily (Pro keeps 7 days); point in time recovery is an add on; Free projects should export their own data regularly with the CLI [23]. Pass when an off Supabase copy exists (for example a scheduled `supabase db dump`) and a restore has been tested at least once. * [ ] **E7. Pausing risk.** Free projects with insufficient user database activity over the past week can be paused; paid projects are not subject to inactivity pausing [24]. Record the recovery procedure and warn if production depends on a project that can be paused. * [ ] **E8. Auth settings.** Owner check: MFA available and used for admin accounts, email confirmation on, redirect URLs limited to your own domains. ### F. Shopify * [ ] **F1. HMAC on every webhook.** Each webhook route verifies `X-Shopify-Hmac-SHA256` against the raw request body before reading the payload [16]. Pattern in T5. Fail if any handler parses JSON first or skips the check. * [ ] **F2. Idempotent handlers.** Handlers record the webhook's event or delivery ID and ignore repeats, because Shopify can deliver the same event more than once. * [ ] **F3. API version current.** Find the Admin API version string. Shopify releases a version every quarter and supports each for at least 12 months [25]. Warn when the version in use has less than 3 months of support left; record its end date for the calendar. * [ ] **F4. Least privilege scopes.** Compare requested scopes with what the code actually calls. Every unused scope is a warn. * [ ] **F5. Tokens server side.** Shopify access tokens and client secrets live only in server environment variables or a secret store. Fail if one appears in the repo or its history (see G). ### G. Secrets * [ ] **G1. History scan.** Run `gitleaks git --redact -v` over the full history [15]. Report file, commit and rule, never the secret. * [ ] **G2. Optional verified scan.** Owner check: TruffleHog's verified Git scan checks found credentials against their providers to see if they still work, so the owner runs it separately [15]. * [ ] **G3. Ignore rules.** `.env*` files are in `.gitignore` and none are tracked (`git ls-files | grep -i env`). * [ ] **G4. Rotation.** Anything ever committed gets rotated. Deleting the commit does not make it safe [15]. * [ ] **G5. Stop the next leak.** GitHub push protection is on (on private repos it needs GitHub Secret Protection), or a gitleaks pre commit hook is installed [15][22]. ### H. Dependencies, updates and the IOC pre check * [ ] **H1. Reproducible installs.** A lockfile is committed and CI installs with `npm ci`. * [ ] **H2. Known vulnerabilities.** `npm audit --audit-level=high` and, if available, `osv-scanner scan source -r .` (OSV Scanner v2 syntax). List every finding with its fixed version or "no fix" [21]. * [ ] **H3. Frameworks behind.** `npm outdated`. Flag anything a major version behind (framework, `@supabase/supabase-js`, Shopify libraries) and anything end of life. * [ ] **H4. Dependabot.** `.github/dependabot.yml` exists with weekly updates and a cooldown for both `npm` and `github-actions` (template T1) [19]. Owner check: Dependabot alerts and security updates are on in the repo settings. * [ ] **H5. IOC pre check.** A check runs before dependency changes merge that blocks known malicious releases and holds versions younger than 24 hours (templates T2 and T3). The LiteLLM incident illustrates why a hold matters [20], but T2 covers npm only. Check Python packages and container images through the separate upgrade process in L5. * [ ] **H6. Workflow safety.** Every workflow sets the smallest `permissions:` it needs, third party actions are pinned, and no `pull_request_target` workflow checks out pull request code. T3 preserves release tags from the starting template. For stronger immutability, have your maintainer replace them with reviewed full commit SHAs and keep Dependabot watching them [54]. * [ ] **H7. Edge Functions.** Supabase Edge Functions have their own dependencies (Deno imports or `deno.json`). List them and their versions, since Dependabot may not cover them. ### I. Live site * [ ] **I1. ZAP baseline.** After the owner confirms the domain he owns, run the passive baseline [17]: ```text docker run --rm -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py -t https://YOUR-DOMAIN ``` It spiders briefly and reports passive findings; it doesn't attack. A logged in ZAP session is a deeper pass the owner can run later on his own site [18]. * [ ] **I2. Headers.** `curl -sI https://YOUR-DOMAIN` shows HSTS, a Content Security Policy, `X-Content-Type-Options: nosniff`, a Referrer Policy and frame protection. * [ ] **I3. Alerts.** Owner check: uptime monitoring and error alerts exist and reach a person. ### J. GitHub account controls (owner checks) * [ ] **J1.** Two factor on every account with access to the repo, Supabase, Shopify and each AI provider. * [ ] **J2.** A branch rule or ruleset on `main`: pull request required, the H5 check and tests required, no force pushes. * [ ] **J3.** Dependabot alerts on; secret scanning and push protection status recorded; code scanning (CodeQL default setup) on if your plan includes it. * [ ] **J4.** Collaborators, deploy keys, personal access tokens and installed GitHub Apps reviewed; anything unused removed. ### K. Backups, recovery and rollback The backup pattern uses restic for encrypted snapshots, a second drive for quick local restores, and the cloud storage you already pay for to hold a separate repository [28][29]. Google Drive through rclone is the worked option below. Confirm capacity, upload limits and recovery access before choosing your backend. Restic encrypts the backup data and deduplicates stored content; a snapshot can restore the selected state while depending on shared repository objects [28][30]. **The plan to check against:** | Layer | Short term (days) | Long term (months and years) | How you roll back | |---|---|---|---| | Code (GitHub) | Every commit; a tag on every production deploy | Nightly `git clone --mirror` inside the restic snapshot | Redeploy the last good tag, or `git revert` the bad commit | | Database (Supabase) | Nightly CLI dump into the local repository: 7 daily, 4 weekly. Supabase's own daily backups and point in time recovery on paid plans [23] | Copied to cloud storage: 7 daily, 5 weekly, 12 monthly, 3 yearly | Today: point in time recovery or Supabase's backup. Older: restore the dump into a scratch project and copy back only the rows you need | | Files in Supabase Storage | Weekly copy into the staging folder (the database dump does not carry the files; Supabase's guide copies them separately [31]) | Same cloud retention | Restore the files and upload them again | | Shopify | Weekly CSV exports (products, customers, orders, gift card and discount codes, financial data) and the theme in GitHub [32][39] | Monthly exports kept 12 months | Products come back through CSV import and the theme through GitHub. Orders can't be imported through the admin, so order exports are records, not a restore path [32] | | Secrets and recovery codes | Password manager | One offline copy of the restic password and account recovery codes | Rotate first, then restore | * [ ] **K1. Inventory.** List everything that would hurt to lose: database, Storage buckets, Shopify data and theme, the repository, environment variable names, DNS and domain settings, and the AI and payment accounts' recovery codes. * [ ] **K2. A database dump off Supabase.** A scheduled dump with Supabase's documented CLI method (roles, schema and data as separate files) lands outside Supabase at least nightly. The job fails if an expected schema or data export is empty [31]; inspect the roles export separately because a project may have no custom roles. Do not rely on a managed daily backup on the Free plan; keep your own export [23]. * [ ] **K3. Two restic repositories, joined by `restic copy`.** A local repository on a second drive (short term) and a separate cloud repository (long term). The example uses Google Drive through rclone. Each new snapshot is copied with `restic copy`, which only adds. Fail if the off site copy is a mirror made with `rclone sync`, which copies local pruning to Drive and erases the history you meant to keep [28][29][30]. * [ ] **K4. Retention runs weekly.** `restic forget` with the policy in the table, `--prune`, and `restic check` afterwards. `forget` alone frees no space, and pruning locks the repository, so schedule it away from the nightly run [33]. Local pruning happens only after the Drive copy has been checked [29]. * [ ] **K5. Drive set up the safe way.** Owner check for this worked option: create your own OAuth client in Google Cloud and choose the narrow `drive.file` scope [34]. Rclone's current guide says the shared client is being retired during 2026 and warns that grants for an app left in Testing expire after a week. Follow its publishing instructions for your own client. Check the upload quota for your account before the first full backup, and include trash usage in your capacity check [28][35]. * [ ] **K6. The password survives the machine.** Owner check: the restic password lives in a password manager plus one offline copy, never only on the machine being backed up. Without it the Drive copy can't be opened [28]. * [ ] **K7. Restores are tested.** Restore into a scratch folder and a scratch database every month, and record the date, elapsed time and result. Rebuild on a clean machine every quarter. A successful backup job alone does not demonstrate recovery [28][29][30]. * [ ] **K8. Silence raises an alarm.** A failed run alerts a person, and so does a run that never starts (a heartbeat or dead man's switch that fires when no success has been seen for 26 hours) [29]. * [ ] **K9. GitHub used for what it's good at.** Production deploys are tagged; `main` blocks force pushes (J2) so history can't be rewritten; migrations, configuration and the Shopify theme live in git [39]. Customer data, secrets and large files never go in git: history is permanent and GitHub blocks files over 100 MiB [15][40]. Eligible deleted repositories can be restored within 90 days, subject to GitHub's fork-network restrictions [38]. The nightly mirror covers a locked or lost GitHub account. * [ ] **K10. Optional third copy under separate control.** A Google Cloud Storage bucket as a second restic target, with a separate service account and recovery administration [36]. Do not give the daily backup credential permission to change the retention policy or delete the recovery account. Soft delete is on by default for 7 days and can be raised to 90, so deleted backup files can be recovered inside that window [37]. Separately controlled storage limits the damage from stolen workstation credentials [41]. **Where the nightly job runs:** on a computer you control with an external or second drive (the T7 script, scheduled with cron, launchd or Task Scheduler), or as a scheduled GitHub Action that dumps the database and backs up straight to the Drive or Cloud Storage repository with secrets in GitHub Actions secrets. The computer gives a fast local short term copy; the Action needs no machine left on. Many setups use both. ### L. AI gateway (LiteLLM or Cloudflare AI Gateway) There are two good options; pick one. * **Cloudflare AI Gateway (managed, nothing to run).** Core analytics, caching and rate limiting are free on all plans; model inference still has its own bill [47]. Route approved calls through the gateway and choose what to log. Dynamic routing can select a fallback and branch on metadata [48]. Automatically created gateways start with caching and rate limiting off. Run tokens apply across the account, so check isolation when callers have different permissions [55]. Accounts creating their first gateway on or after September 24, 2026 use Workers Logs pricing and retention. The 100,000 free log limit belongs to Legacy Logs on earlier accounts [47]. Unified Billing credit purchases carry a 5% fee; using your own provider keys is a different billing path. Cloudflare becomes another processor in the request path. * **LiteLLM (self hosted, most control).** Per caller keys with model lists and budgets, on a gateway server you control. Requests still go to the configured model provider unless the model itself runs locally. You run, patch and back it up yourself. Choose Cloudflare to get visibility and limits this week without a server; choose LiteLLM when you need hard per app model restrictions or want traffic on your own machine. For a single caller, direct API access may be simpler. Add a gateway when central policy, accounting or tested fallback is worth the extra component. The checks below apply to both unless marked. LiteLLM is an open source proxy that puts one endpoint in front of every model provider. Callers ask for a named alias (for example `crm_main`) instead of a vendor model, and the gateway handles routing, retries, ordered fallbacks, per key budgets and a spend log [42][43][44]. The separate server guide shows where this layer fits [29]. Why it fits this setup: 1. **Continuity:** when the primary provider errors, rate limits or suspends the account, eligible failed requests can fall back to the next model group in the order you set [43]. Test authentication failures, timeouts and rate limits separately; not every error takes the same path. That turns the lockout in A5 into a configuration that has already been tested. 2. **Enforced guardrails:** each app or job gets its own virtual key limited to a model list, with a budget and its own spend record [44]. The CRM's key can't reach anything not approved for customer data. 3. **One place for secrets and changes:** provider keys stay on the gateway server, callers hold revocable gateway keys, and switching or adding a model is a config change [45]. * [ ] **L1. One door.** Every automated model call (the CRM, jobs, CI) goes through the gateway with an alias and a gateway key. Provider keys don't appear in application code or app hosting environments. Record any approved direct callers as explicit exceptions with their own budgets and recovery plan. * [ ] **L2. Fallbacks set and tested.** At least the main alias has an ordered fallback to a second provider, and the cutoff drill (revoke the primary key in staging, watch the backup answer) has been run [27][43]. * [ ] **L3. One key per caller, limited.** Each app and job has its own key with `models`, `max_budget` and `budget_duration` set [44]. The key used by anything that touches customer data can't call Chinese hosted models or anything else not approved in B2. LiteLLM's current fallback checks also enforce the target model allowlist and applicable budgets, so include approved fallback aliases on the caller's key and keep those checks enabled [43]. * [ ] **L4. Metered keys only.** Only API keys sit in the gateway, never subscription logins [29]. If Claude Code runs through it, that traffic is billed per token, and Anthropic supports only Claude models through a gateway, so the second model for coding stays a separate tool such as Codex [45]. * [ ] **L5. The gateway itself is supply chain.** LiteLLM had poisoned releases published through its own pipeline in March (1.82.7, 1.82.8) and October (1.107.0) 2026 [20]. Pass when the image is pinned by sha256 digest (never `latest` or a moving tag), every upgrade passes the IOC pre check and 24 hour hold first, and the previous known-good digest is kept for rollback. T2 checks npm packages only; the gateway image and its Python dependencies require their own advisory and provenance review. The hold reduces exposure to newly discovered malware; it does not certify a release safe. * [ ] **L6. Not open to the internet.** The gateway listens on a private address and requires authentication; the admin UI isn't public; the master key (it must start with `sk-`) lives in a secret store [44]. * [ ] **L7. Its own database, backed up.** Virtual keys and spend need a Postgres database [44]. It is separate from the CRM's database and covered by section K. * [ ] **L8. Logs and personal data.** Decide what the gateway stores about prompts and responses, and keep customer data out of gateway logs unless there's a reason. If customer text goes to models, consider LiteLLM's PII masking guardrail [49] or Cloudflare's DLP feature [50]. * [ ] **L9. Kept current with its callers.** Owner check: someone owns gateway upgrades, because Claude Code adds features that a stale gateway won't forward [45]. Upgrades still go through L5. * [ ] **L10. Cloudflare AI Gateway settings (if chosen).** Owner check: authentication on, rate limits on, logs reviewed for customer data, and account-scoped run tokens kept server side [55]. Caching is disabled by default. If enabled, default cache keys use exact request matching; a custom key changes that behavior, so never share one across customers whose responses differ. The documented TTL range is 60 seconds to one month [51]. Skip caching personal content unless isolation and retention have been deliberately designed. On `gateway.ai.cloudflare.com`, put the provider key in its usual provider header and the Cloudflare token in `cf-aig-authorization` [46]. ## Phase 2: Report Write `SETUP_REVIEW_REPORT.md` with: 1. **Summary:** five lines at most, with the top three risks first. 2. **Inventory** from Phase 0. 3. **Scorecard:** a table with ID, check, status, evidence, proposed fix ID and effort (small, medium, large). 4. **Owner checks:** each one with the exact clicks or command, and where to paste the result. 5. **Proposed fixes F1 to Fn,** ordered by risk. Each one says what changes, which files, how to test it and how to undo it. No secret values anywhere in the report. Keep reports containing repository evidence private. If you share a report outside the business, remove account identifiers, private paths, customer data and raw logs first. ## Phase 3: Apply (only after the owner approves a fix ID) 1. One fix per branch and pull request. 2. Run the tests and the build. Database changes go in a migration, tried on a Supabase branch or a copy of the project before production. 3. After any RLS change, prove it: signed out, and signed in as an ordinary user, you can't read another user's rows, and the app still works. 4. Have the other model review the pull request, then get a person's approval before merging. 5. After merging, rerun that fix's Phase 1 check and update the report. ## Templates ### T1. `.github/dependabot.yml` Weekly version updates with a cooldown, so brand new releases wait before they're proposed. Cooldown applies to version updates; security updates still arrive right away, which is why T3 checks every pull request too [19]. ```yaml version: 2 updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" day: "monday" cooldown: default-days: 3 semver-major-days: 14 open-pull-requests-limit: 10 - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" cooldown: default-days: 3 ``` ### T2. `scripts/ioc-precheck.mjs` Blocks any installed version with an OSV malicious package record (`MAL-` IDs) or listed in an optional `ioc/known-bad.json`, and holds any version that is new compared with the base branch and less than 24 hours old. It fails closed when OSV or the npm registry can't be reached. Use a currently supported Node release. The script has no dependencies and runs before `npm ci`. It reads `package-lock.json`; it does not inspect a container, a Python package, Deno imports or GitHub Action contents. If the base lockfile is unavailable, the original template explicitly skips its age hold. Treat that message as an unresolved check and establish a valid base before approving an update. Keep the check and its workflow protected by review, since a pull request could otherwise edit the checker itself. Two additional input limits need a maintainer's check: a legacy lockfile without a `packages` map produces zero package checks, and a malformed nonempty publish timestamp produces an invalid date that this template does not reject. Do not approve an update on either condition. The template is retained here for review; its network failure handling is not complete input validation. ```js // IOC pre check for package-lock.json: block known malicious releases, hold brand new ones. import { readFileSync } from 'node:fs'; import { execFileSync } from 'node:child_process'; const HOLD_HOURS = Number(process.env.IOC_HOLD_HOURS || 24); const BASE = process.env.IOC_BASE_REF || 'origin/main'; function packages(lock) { const out = new Map(); for (const [path, meta] of Object.entries(lock.packages || {})) { if (!path || meta.link || !meta.version) continue; const name = meta.name || path.slice(path.lastIndexOf('node_modules/') + 'node_modules/'.length); out.set(`${name}@${meta.version}`, { name, version: meta.version }); } return out; } const current = packages(JSON.parse(readFileSync('package-lock.json', 'utf8'))); let base = new Map(); try { const text = execFileSync('git', ['show', `${BASE}:package-lock.json`], { encoding: 'utf8', maxBuffer: 1 << 28 }); base = packages(JSON.parse(text)); } catch { console.log(`No lockfile at ${BASE}; skipping the age hold and checking malware records only.`); } const all = [...current.values()]; const added = base.size ? all.filter((p) => !base.has(`${p.name}@${p.version}`)) : []; const problems = []; // Optional local list: { "npm": { "package-name": ["1.2.3"] } } let knownBad = {}; try { knownBad = JSON.parse(readFileSync('ioc/known-bad.json', 'utf8')).npm || {}; } catch (err) { if (err.code !== 'ENOENT') throw err; } for (const p of all) { if ((knownBad[p.name] || []).includes(p.version)) problems.push(`BLOCK ${p.name}@${p.version}: listed in ioc/known-bad.json`); } // OSV malicious package records for every installed version. for (let i = 0; i < all.length; i += 1000) { const chunk = all.slice(i, i + 1000); const res = await fetch('https://api.osv.dev/v1/querybatch', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ queries: chunk.map((p) => ({ package: { name: p.name, ecosystem: 'npm' }, version: p.version })) }), }); if (!res.ok) throw new Error(`OSV unavailable (HTTP ${res.status}); failing closed`); const { results } = await res.json(); if (!Array.isArray(results) || results.length !== chunk.length) throw new Error('OSV returned an incomplete batch; failing closed'); results.forEach((r, j) => { const mal = (r.vulns || []).map((v) => v.id).filter((id) => id.startsWith('MAL-')); if (mal.length) problems.push(`BLOCK ${chunk[j].name}@${chunk[j].version}: malicious package record ${mal.join(', ')}`); }); } // Age hold for versions that are new in this change. for (const p of added) { const res = await fetch('https://registry.npmjs.org/' + p.name.replace('/', '%2F')); if (!res.ok) throw new Error(`npm registry unavailable for ${p.name} (HTTP ${res.status}); failing closed`); const published = (await res.json()).time?.[p.version]; if (!published) { problems.push(`HOLD ${p.name}@${p.version}: no publish time in the registry`); continue; } const hours = (Date.now() - Date.parse(published)) / 36e5; if (hours < HOLD_HOURS) problems.push(`HOLD ${p.name}@${p.version}: published ${hours.toFixed(1)} hours ago, under the ${HOLD_HOURS} hour hold`); } console.log(`Checked ${all.length} package versions; ${added.length} new compared with ${BASE}.`); if (problems.length) { console.log(problems.join('\n')); process.exit(1); } console.log('IOC pre check passed.'); ``` ### T3. `.github/workflows/weekly-deps.yml` Runs every Monday, on every pull request that changes dependencies, and on demand. The IOC pre check runs before anything is installed. A failed weekly run opens an issue, and the outdated list lands in the run summary so you see which libraries and frameworks need attention. Before making this job a required check on `main` (J2), account for its path filter: GitHub can leave a required check pending on a pull request that does not match those paths. Your maintainer should use an always-running required wrapper or adjust the trigger for the repository. Also require review when either the workflow or checker changes. ```yaml name: Weekly dependency and IOC check on: schedule: - cron: "0 14 * * 1" pull_request: paths: ["package.json", "package-lock.json"] workflow_dispatch: permissions: contents: read issues: write jobs: check: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 22 - name: IOC pre check (before anything is installed) env: IOC_BASE_REF: origin/${{ github.base_ref || github.event.repository.default_branch }} run: node scripts/ioc-precheck.mjs - name: Install without lifecycle scripts run: npm ci --ignore-scripts - name: Known vulnerabilities run: npm audit --audit-level=high - name: Outdated libraries and frameworks if: always() run: | { echo "### Outdated packages"; echo "~~~"; npm outdated || true; echo "~~~"; } >> "$GITHUB_STEP_SUMMARY" - name: Open an issue when the weekly run fails if: failure() && github.event_name == 'schedule' env: GH_TOKEN: ${{ github.token }} run: | gh issue create --title "Weekly dependency check failed $(date -u +%F)" \ --body "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" ``` If your app needs install scripts to build, keep `--ignore-scripts` here anyway: this job only checks, it doesn't build. Dependabot's `github-actions` entry in T1 keeps the action versions current. ### T4. Supabase SQL (run in the SQL editor, read only) ```sql -- 1. Tables in the public schema with Row Level Security off (each row is a finding) select n.nspname as schema, c.relname as table_name from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.relkind in ('r', 'p') and n.nspname = 'public' and not c.relrowsecurity order by 1, 2; -- 2. Policies that let everyone through (each needs a reason or a fix) select schemaname, tablename, policyname, roles, cmd, qual, with_check from pg_policies where schemaname = 'public' and (qual = 'true' or with_check = 'true' or 'anon' = any(roles) or 'public' = any(roles)) order by tablename, policyname; -- 3. Views that run with the owner's rights instead of the caller's (can bypass RLS) select n.nspname as schema, c.relname as view_name from pg_class c join pg_namespace n on n.oid = c.relnamespace where c.relkind = 'v' and n.nspname = 'public' and coalesce(array_to_string(c.reloptions, ','), '') !~* 'security_invoker=(true|on|1)' order by 1, 2; ``` If you expose schemas other than `public` through the API, run the same queries for each of them. ### T5. Shopify webhook HMAC check (Node) Verify against the raw body, before parsing it [16]. In Express use `express.raw({ type: 'application/json' })` on webhook routes; in a Next.js route handler read `await request.text()` first. ```js import crypto from 'node:crypto'; export function verifyShopifyWebhook(rawBody, hmacHeader, clientSecret) { const digest = crypto.createHmac('sha256', clientSecret).update(rawBody, 'utf8').digest('base64'); const expected = Buffer.from(digest); const received = Buffer.from(hmacHeader || ''); return expected.length === received.length && crypto.timingSafeEqual(expected, received); } // In the handler: if (!verifyShopifyWebhook(body, req.headers['x-shopify-hmac-sha256'], process.env.SHOPIFY_API_SECRET)) return 401. ``` ### T6. `AGENTS.md` starting point ```markdown # AGENTS.md ## What this is One paragraph: what the CRM does and who uses it. ## Commands Install: npm ci Test: npm test Build: npm run build Deploy: how it deploys, and that agents never deploy on their own. ## Rules Never commit or print secrets; they live in environment variables only. Database changes go through migrations in supabase/migrations. Every change goes through a pull request with passing checks. ## Data Customer and order data lives in Supabase tables: list them. Never send customer rows, orders or keys to any AI provider. Chinese hosted model APIs get no customer data at all. ``` Then make `CLAUDE.md` a single line, `@AGENTS.md`, so both tools read the same rules. ### T7. `crm_backup.sh` (nightly backup, macOS or Linux; on Windows run it in WSL) One time setup: install `restic`, `rclone`, `git`, the Supabase CLI and its documented prerequisites; run `rclone config` to create a remote named `gdrive` with your own OAuth client and the `drive.file` scope [34]; then create both repositories so their chunking matches and copies deduplicate [30]: ```bash restic -r "$LOCAL_REPO" init restic -r rclone:gdrive:crm-restic init --from-repo "$LOCAL_REPO" --from-password-file "$RESTIC_PASSWORD_FILE" --copy-chunker-params ``` Before running this script, set `STAGE` to a disposable staging folder that contains nothing else. The script recursively deletes that folder at the start and end. Do not point it at your home folder, repository, mounted backup drive or any folder with originals. Set an owner-only process umask, schedule a single instance at a time and keep credentials out of shell history. Initialize both repositories with the intended password configuration before the first run. The script stages its covered exports in one fixed folder (a fixed path keeps restic's retention grouping working), backs it up locally, copies the new snapshot to Drive, verifies both, and applies retention on Sundays after the Drive copy is checked. ```bash #!/usr/bin/env bash # Nightly CRM backup: Supabase dump, repo mirror and Shopify exports into a local restic # repository (short term), copied to Google Drive (long term). Any failure exits non zero. set -euo pipefail : "${SUPABASE_DB_URL:?set SUPABASE_DB_URL from your password manager, never from git}" : "${RESTIC_PASSWORD_FILE:?set RESTIC_PASSWORD_FILE to an owner only file}" : "${REPO_URL:?set REPO_URL to the GitHub clone URL}" : "${LOCAL_REPO:?set LOCAL_REPO to a folder on a second drive}" DRIVE_REPO="${DRIVE_REPO:-rclone:gdrive:crm-restic}" STAGE="${STAGE:-$HOME/.crm_backup-stage}" SHOPIFY_EXPORTS="${SHOPIFY_EXPORTS:-}" # optional folder holding your Shopify CSV exports export RESTIC_PASSWORD_FILE rm -rf "$STAGE" && mkdir -p "$STAGE" && chmod 700 "$STAGE" # 1. Database: Supabase's documented dump, roles, schema and data as separate files. supabase db dump --db-url "$SUPABASE_DB_URL" -f "$STAGE/roles.sql" --role-only supabase db dump --db-url "$SUPABASE_DB_URL" -f "$STAGE/schema.sql" supabase db dump --db-url "$SUPABASE_DB_URL" -f "$STAGE/data.sql" --use-copy --data-only \ -x "storage.buckets_vectors" -x "storage.vector_indexes" for f in schema data; do [ -s "$STAGE/$f.sql" ] || { echo "$f.sql is empty; not backing up a broken dump" >&2; exit 1; } done # 2. Code: a full mirror, so a locked GitHub account doesn't take the history with it. git clone --quiet --mirror "$REPO_URL" "$STAGE/repo.git" # 3. Shopify exports, when you keep them in a folder. if [ -n "$SHOPIFY_EXPORTS" ]; then cp -R "$SHOPIFY_EXPORTS" "$STAGE/shopify"; fi # 4. Short term locally, then copy the new snapshot to Drive. restic copy only adds; never rclone sync. restic -r "$LOCAL_REPO" backup --tag nightly "$STAGE" restic -r "$LOCAL_REPO" check restic -r "$DRIVE_REPO" copy --from-repo "$LOCAL_REPO" --from-password-file "$RESTIC_PASSWORD_FILE" restic -r "$DRIVE_REPO" check --read-data-subset=5% # 5. Sundays: retention after the Drive copy is verified, then full checks. if [ "$(date +%u)" = 7 ]; then restic -r "$LOCAL_REPO" forget --keep-daily 7 --keep-weekly 4 --prune restic -r "$DRIVE_REPO" forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --keep-yearly 3 --prune restic -r "$LOCAL_REPO" check restic -r "$DRIVE_REPO" check fi rm -rf "$STAGE" echo "backup ok $(date -u +%Y-%m-%dT%H:%M:%SZ)" ``` **Coverage to add before calling this a complete backup:** T7 exports database schema and rows, mirrors code, and copies a folder of Shopify CSV exports. It does not export Supabase Storage file contents, refresh the Shopify CSVs, or dump the gateway's separate Postgres database. Add those documented staging jobs and check their freshness before the restic step; test each in T8. A successful shell exit cannot prove those omitted layers are recoverable. The data and schema files are checked for size; the roles file can legitimately be empty. Schedule it nightly, for example with cron: `0 2 * * * /path/crm_backup.sh >> "$BACKUP_LOG" 2>&1`. Set `BACKUP_LOG` and the other variables in protected scheduler configuration, and have a heartbeat service alert you when the "backup ok" line stops appearing. For a Google Cloud Storage copy (K10), add a third repository `gs:YOUR-BUCKET:/crm` with `GOOGLE_PROJECT_ID` and `GOOGLE_APPLICATION_CREDENTIALS` set, and a second `restic copy` line [36]. ### T8. Restore runbook Choose a fresh scratch directory and confirm `SCRATCH_DB_URL` identifies a disposable database before running these commands. Select the intended snapshot by ID when more than one host or backup job uses the repository; `latest` is only a starting example. Inspect the restored directory before importing, and follow the linked Supabase guide for any project-specific role, extension, authentication or Storage steps [31]. The script restores database exports and code; test the separately staged files and gateway database too. **Vault and encrypted columns need an additional recovery secret.** Supabase's manual logical restore instructions require the old project's encryption root key for Vault or column encryption. Secure it while the source project is active; the SQL backup does not include it. Replacing a destination project's root key can make data encrypted with its existing key unreadable, so follow the documented procedure only on the intended recovery project [31]. The sample below is the documented path without Vault or column encryption. Migration history, managed-schema customizations, Realtime publications, Edge Function dependencies and stored files need their separate steps from that guide. ```bash # See what you have (use -r rclone:gdrive:crm-restic if the local drive is gone) restic -r "$LOCAL_REPO" snapshots # Restore into a scratch location, never over live data restic -r "$LOCAL_REPO" restore latest --target /tmp/crm-restore cd "$(dirname "$(find /tmp/crm-restore -name schema.sql | head -n 1)")" # Database into a scratch Supabase project (or `supabase start` locally), with Supabase's documented restore psql --single-transaction --variable ON_ERROR_STOP=1 \ --file roles.sql --file schema.sql \ --command 'SET session_replication_role = replica' \ --file data.sql --dbname "$SCRATCH_DB_URL" # Code from the mirror git clone repo.git /tmp/crm-code ``` Rollback ladder, fastest first: 1. **Bad deploy:** redeploy the last good tag. Minutes. 2. **Bad data change today:** Supabase point in time recovery if you have it, otherwise last night's dump restored into a scratch project, then copy back only the affected rows. 3. **Problem found weeks later:** pick the weekly or monthly snapshot from before it started (`restic snapshots`, then `restore `), restore into scratch, compare, copy back. 4. **Lost account, ransomware or a dead laptop:** use a clean machine to recover accounts and revoke or rotate affected credentials before reconnecting production. Install trusted tools, restore a known clean snapshot from the cloud copy, rebuild the database, validate the files and code, then redeploy with replacement credentials. Preserve incident evidence and reconcile newer legitimate transactions before reopening writes. ### T9. LiteLLM gateway: `config.yaml`, run command and keys `config.yaml` (aliases, ordered fallback; model IDs are examples, use each provider's current ones): ```yaml model_list: - model_name: crm_main # what the CRM asks for litellm_params: model: anthropic/claude-sonnet-5-5 api_key: os.environ/ANTHROPIC_API_KEY - model_name: crm_backup # second provider, US hosted litellm_params: model: openai/YOUR-CURRENT-GPT-MODEL api_key: os.environ/OPENAI_API_KEY - model_name: cheap_experiments # open weights on a US host (B3); never on a customer data key litellm_params: model: together_ai/YOUR-OPEN-WEIGHT-MODEL api_key: os.environ/TOGETHERAI_API_KEY litellm_settings: num_retries: 2 request_timeout: 120 fallbacks: [{"crm_main": ["crm_backup"]}] general_settings: master_key: os.environ/LITELLM_MASTER_KEY # starts with sk- ``` Run it pinned by digest, on a private address, with secrets in an env file only the server account can read. `DATABASE_URL` in that file points at the gateway's own Postgres database [44]: ```bash docker run -d --name litellm --restart unless-stopped \ -p "${LITELLM_BIND_ADDRESS:?set a private bind address}:4000:4000" \ -v "$PWD/config.yaml:/app/config.yaml:ro" \ --env-file litellm.env \ docker.litellm.ai/berriai/litellm@sha256:DIGEST-THAT-PASSED-YOUR-IOC-CHECK \ --config /app/config.yaml ``` One key per caller, limited to approved aliases with a monthly budget [44]. This example budget is a planning placeholder, not a product price. Set `LITELLM_BIND_ADDRESS` to the private interface chosen by your administrator and `GATEWAY_BASE_URL` to its protected URL. Do not expose the master key to application callers. The owner runs key provisioning in a secure terminal. Set `KEY_RESPONSE_FILE` to a new file in an owner-only temporary directory, import its response into a secret manager without printing it, then delete the file. Do not paste the returned key into a review report or chat: ```bash umask 077 curl --fail-with-body -s --output "${KEY_RESPONSE_FILE:?set a new owner-only output file}" \ "${GATEWAY_BASE_URL:?set the protected gateway URL}/key/generate" \ -H "Authorization: Bearer $LITELLM_MASTER_KEY" -H "Content-Type: application/json" \ -d '{"key_alias": "crm_prod", "models": ["crm_main", "crm_backup"], "max_budget": 100, "budget_duration": "30d"}' ``` Then point the CRM's OpenAI compatible client at the gateway URL with that key and ask for `crm_main`. Check: `curl -s "${GATEWAY_BASE_URL:?set the protected gateway URL}/v1/models" -H "Authorization: Bearer $LITELLM_MASTER_KEY"` lists the three aliases, and the same call with no key returns 401. Run both checks yourself after selecting current supported model IDs and an approved image digest. An alias listing alone does not prove model calls or fallbacks work. Send a harmless task through each alias, then run the staged cutoff drill. ### T10. Cloudflare AI Gateway: the one line change Create a gateway in the Cloudflare dashboard (AI, AI Gateway), turn on authentication and rate limiting, decide whether the data permits caching, then send provider calls through it with your own keys [46]. This provider-native Anthropic example uses an example model ID. Confirm it is supported by your account before running it: ```bash curl https://gateway.ai.cloudflare.com/v1/$CF_ACCOUNT_ID/$GATEWAY_ID/anthropic/v1/messages \ --header "x-api-key: $ANTHROPIC_API_KEY" \ --header "anthropic-version: 2023-06-01" \ --header "cf-aig-authorization: Bearer $CF_AIG_TOKEN" \ --header "content-type: application/json" \ --data '{"model": "claude-sonnet-5-5", "max_tokens": 256, "messages": [{"role": "user", "content": "ping"}]}' ``` In code, that's the SDK's base URL set to `https://gateway.ai.cloudflare.com/v1///anthropic` (or `/openai` for OpenAI) plus the `cf-aig-authorization` header. Fallbacks and budget limits are set up as dynamic routes in the dashboard [48]. ## Sources 1. Why use the separate Anthropic API: https://jwatte.com/blog/why-use-the-anthropic-api/ 2. Anthropic, subscription and API are billed separately: https://support.claude.com/en/articles/9876003-i-have-a-paid-claude-subscription-pro-max-team-or-enterprise-plans-why-do-i-have-to-pay-separately-to-use-the-claude-api-and-console 3. Anthropic Consumer Terms, section 3: https://www.anthropic.com/legal/consumer-terms 4. AI vendor lockout continuity plan (Belo suspension, separate admin email, spending caps): https://jwatte.com/blog/ai-vendor-lockout-continuity-plan/ 5. Tom's Hardware on the Belo suspension: https://www.tomshardware.com/tech-industry/artificial-intelligence/anthropic-nuked-a-companys-access-to-claude-stopping-60-employees-dead-in-their-tracks-support-via-google-form-is-the-only-recourse-for-vague-usage-policy-violation 6. Anthropic Commercial Terms, section I.3.b: https://www.anthropic.com/legal/commercial-terms 7. Where your code goes with Kimi, Qwen and DeepSeek: https://jwatte.com/blog/cli-vs-browser-gui-ai-coding/ 8. China's National Intelligence Law: https://en.wikipedia.org/wiki/National_Intelligence_Law_of_the_People%27s_Republic_of_China 9. The debate over Article 7: https://www.chinalawtranslate.com/en/what-the-national-intelligence-law-says-and-why-it-doesnt-matter/ 10. Ponytail: https://github.com/DietrichGebert/ponytail 11. Ponytail on a billing portal, commands and limits: https://jwatte.com/blog/ponytail-lazy-senior-dev-ai-coding-agents/ 12. OWASP Top 10 2025: https://top10.owasp.org/2025 13. OWASP Top 10 for LLM applications: https://genai.owasp.org/llm-top-10/ 14. Supabase Row Level Security: https://supabase.com/docs/guides/database/postgres/row-level-security 15. Leaked API keys, gitleaks and trufflehog, the Moltbook Supabase leak: https://jwatte.com/blog/leaked-api-keys-found-in-minutes-gitignore-rotation-plan/ 16. Shopify HTTPS webhooks and HMAC verification: https://shopify.dev/docs/apps/build/webhooks/subscribe/https 17. ZAP baseline scan: https://www.zaproxy.org/docs/docker/baseline-scan/ 18. Audit your own web app's security: https://jwatte.com/blog/audit-your-web-app-security-yourself/ 19. Dependabot options, including cooldown: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference 20. The LiteLLM compromise and the 24 hour hold: https://jwatte.com/blog/litellm-1-107-0-compromise-ioc-checks/ 21. Supply chain malware and AI dependencies: https://jwatte.com/blog/blog-supply-chain-malware-ai-dependencies/ 22. GitHub push protection: https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection 23. Supabase backups: https://supabase.com/docs/guides/platform/backups 24. Supabase project pausing: https://supabase.com/docs/guides/platform/project-pausing 25. Shopify API versioning: https://shopify.dev/docs/api/usage/versioning 26. AGENTS.md: https://agents.md/ 27. Diversify your AI spend and run the cutoff drill: https://jwatte.com/blog/diversify-ai-api-spend-across-providers/ 28. Backed up to the Google Drive you already pay for (restic, rclone, drive.file, own OAuth client, Drive limits): https://jwatte.com/blog/sovereign-windows-desktop-drive-backups/ 29. Self hosted AI server: the backup and restore flow and retention we run: https://jwatte.com/blog/self-hosted-ai-server-from-scratch/ 30. restic, working with repositories (copy, chunker parameters): https://restic.readthedocs.io/en/stable/045_working_with_repos.html 31. Supabase backup and restore using the CLI: https://supabase.com/docs/guides/platform/migrating-within-supabase/backup-restore 32. Shopify backups and duplication: https://help.shopify.com/en/manual/shopify-admin/duplicate-store 33. restic, removing snapshots according to a policy: https://restic.readthedocs.io/en/stable/060_forget.html 34. rclone Google Drive backend: https://rclone.org/drive/ 35. Google Drive API usage limits: https://developers.google.com/drive/api/guides/limits 36. restic repository backends (Google Cloud Storage, rclone): https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html 37. Google Cloud Storage soft delete: https://docs.cloud.google.com/storage/docs/soft-delete 38. Restoring a deleted GitHub repository: https://docs.github.com/en/repositories/creating-and-managing-repositories/restoring-a-deleted-repository 39. Shopify GitHub integration for themes: https://shopify.dev/docs/storefronts/themes/tools/github 40. GitHub large file limits: https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-large-files-on-github 41. The work around the model: backups, restore tests and separately controlled storage: https://jwatte.com/blog/sovereign-ai-workstation-operating-system/ 42. LiteLLM proxy: https://docs.litellm.ai/docs/simple_proxy 43. LiteLLM fallbacks, retries and timeouts: https://docs.litellm.ai/docs/proxy/reliability 44. LiteLLM virtual keys, budgets, master key and database: https://docs.litellm.ai/docs/proxy/virtual_keys 45. Claude Code with an LLM gateway (what a gateway provides, billing, Claude models only): https://code.claude.com/docs/en/llm-gateway 46. One account, twenty four AI providers: how a small business uses Cloudflare AI Gateway: https://jwatte.com/blog/cloudflare-ai-gateway-small-business/ 47. Cloudflare AI Gateway pricing and log limits: https://developers.cloudflare.com/ai-gateway/reference/pricing/ 48. Cloudflare AI Gateway dynamic routing: https://developers.cloudflare.com/ai-gateway/features/dynamic-routing/ 49. LiteLLM PII masking guardrail: https://docs.litellm.ai/docs/proxy/guardrails/pii_masking_v2 50. Cloudflare AI Gateway DLP: https://developers.cloudflare.com/ai-gateway/features/dlp/ 51. Cloudflare AI Gateway caching: https://developers.cloudflare.com/ai-gateway/features/caching/ 52. Anthropic API credits for subscribers: https://platform.claude.com/docs/en/about-claude/api-credits-for-subscribers 53. OpenAI ChatGPT API credits: https://developers.openai.com/api/docs/guides/chatgpt-api-credits 54. GitHub Actions secure use and commit SHA pinning: https://docs.github.com/en/actions/reference/security/secure-use 55. Cloudflare authenticated gateway and account-scoped tokens: https://developers.cloudflare.com/ai-gateway/configuration/authentication/ Source collection dated October 29, 2026. Official documentation for credits, gateway routing and keys, Cloudflare pricing and caching, Supabase backup and pausing, Shopify webhooks, restic, rclone and Dependabot was checked again on October 29, 2026 UTC. The other links preserve background reading for the checklist. Verify current documentation and account settings before applying a template; product terms, versions and feature limits can change.