{
 "view": {
  "w": 1250,
  "h": 800
 },
 "stages": [
  {
   "n": 1,
   "title": "Start your database",
   "checks": [
    {
     "id": "MODEL-1",
     "title": "Design your tables",
     "tool": "Neon",
     "level": "v1",
     "done": "Tables, relationships, and constraints match how the business actually works, with one plain sentence per table.",
     "verify": "Codex can explain every table and how they connect without reading the app code."
    },
    {
     "id": "MODEL-2",
     "title": "Make every change a migration",
     "tool": "Drizzle",
     "level": "v1",
     "done": "Every change to the schema is a file in git; nobody edits tables in the dashboard.",
     "verify": "A fresh database built from the migrations matches production exactly."
    },
    {
     "id": "MODEL-3",
     "title": "Lock every table",
     "tool": "server-side checks",
     "level": "v1",
     "done": "Every query runs on the server, scoped to the signed-in person, with each access rule written in plain English; Postgres row-level security adds a second lock where it fits.",
     "verify": "Signed in as one test user, you can't read or change another user's rows."
    },
    {
     "id": "MODEL-6",
     "title": "Seed realistic test data",
     "tool": "preview branches",
     "level": "v1",
     "done": "Every preview gets a database with realistic fake data and no real customers.",
     "verify": "A preview branch shows sample data and none of production's."
    }
   ]
  },
  {
   "n": 2,
   "title": "Store files and passwords",
   "checks": [
    {
     "id": "MODEL-4",
     "title": "Store files outside the database",
     "tool": "Neon Object Storage",
     "level": "v1",
     "done": "Files live in private object storage, served through short-lived links only to their owner; the database keeps only each file's address.",
     "verify": "Uploading as one user and fetching as another fails."
    },
    {
     "id": "SAFE-1",
     "title": "Never store passwords",
     "tool": "Neon Auth",
     "level": "v1",
     "done": "Passwords are handled only by Neon Auth, which stores salted hashes.",
     "verify": "Your tables contain no password column."
    }
   ]
  },
  {
   "n": 3,
   "title": "Bring data in",
   "checks": [
    {
     "id": "DATA-1",
     "title": "List every source of data",
     "tool": "STACK.md data map",
     "level": "v1",
     "done": "Every place data comes from is written down: people, your app, the web, spreadsheets, and every outside tool.",
     "verify": "Ask Codex to compare the list against the code and STACK.md; nothing new turns up."
    },
    {
     "id": "IN-1",
     "title": "Validate everything at the door",
     "tool": "schemas at every entry point",
     "level": "v1",
     "done": "Every write is checked for shape and sense before it reaches the database.",
     "verify": "Sending a malformed request is refused with a clear error, and nothing is saved."
    },
    {
     "id": "IN-3",
     "title": "Import spreadsheets once, cleanly",
     "tool": "import script",
     "level": "v1",
     "done": "Spreadsheet data is imported with duplicates removed into the system that should own it (usually the app; a CRM for sales leads), and that system becomes its owner.",
     "verify": "Running the import twice doesn't create duplicates."
    },
    {
     "id": "IN-2",
     "title": "Receive webhooks safely",
     "tool": "webhooks + events table",
     "level": "v1",
     "done": "Every webhook's signature is checked, each event is stored once, and the reply is fast.",
     "verify": "Sending the same event twice saves it once; a forged event is rejected."
    },
    {
     "id": "IN-5",
     "title": "Run background jobs with retries",
     "tool": "Vercel Workflow",
     "level": "v1",
     "done": "Slow or failure-prone work runs in jobs that retry safely and never do the same thing twice.",
     "verify": "Killing a job mid-run and restarting it gives the same result."
    },
    {
     "id": "IN-4",
     "title": "Scrape responsibly",
     "tool": "Firecrawl",
     "level": "v1",
     "done": "Scraped data records its source and time, respects the site's rules, and refreshes on a schedule.",
     "verify": "Each scraped row shows where and when it came from."
    }
   ]
  },
  {
   "n": 4,
   "title": "Make it fast and live",
   "checks": [
    {
     "id": "FAST-1",
     "title": "Measure before you optimize",
     "tool": "Neon",
     "level": "v1",
     "done": "You have a list of the slowest and most frequent queries.",
     "verify": "You can name the three most expensive queries and how long they take."
    },
    {
     "id": "FAST-3",
     "title": "Fetch only what you need",
     "tool": "query review",
     "level": "v1",
     "done": "Queries select only the columns they use, paginate long lists, and don't run once per row.",
     "verify": "Loading a long list makes a handful of queries, not hundreds."
    },
    {
     "id": "FAST-2",
     "title": "Index the queries you actually run",
     "tool": "Postgres indexes",
     "level": "v1",
     "done": "Each slow query has an index that it really uses.",
     "verify": "The query plan shows the index being used, and the query got faster."
    },
    {
     "id": "FAST-4",
     "title": "Pool your connections",
     "tool": "Supavisor",
     "level": "v1",
     "done": "Serverless functions connect through the pooler, so traffic spikes don't exhaust the database.",
     "verify": "A burst of requests doesn't produce connection errors."
    },
    {
     "id": "FAST-6",
     "title": "Make search good",
     "tool": "Postgres search",
     "level": "v1",
     "done": "Search finds things by words, typos, and meaning, using an index.",
     "verify": "Searching with a typo still finds the right result, quickly."
    },
    {
     "id": "FAST-7",
     "title": "Use live updates only where they matter",
     "tool": "a realtime service",
     "level": "v1",
     "done": "Live updates are on only for screens people share in real time.",
     "verify": "Two browsers on the same screen see each other's changes instantly."
    },
    {
     "id": "FAST-5",
     "title": "Cache only what's proven slow",
     "tool": "Vercel cache",
     "level": "v1",
     "done": "Caches exist only for measured slow reads, and they're cleared when the data changes.",
     "verify": "An update shows up immediately, not after the cache expires."
    }
   ]
  },
  {
   "n": 5,
   "title": "Connect your business tools",
   "checks": [
    {
     "id": "DATA-2",
     "title": "Name the owner of every fact",
     "tool": "STACK.md data map",
     "level": "v1",
     "done": "Each kind of fact (users, payments, leads, files, behavior) has exactly one owner, written down.",
     "verify": "Pick any column in the database and you can say who owns it and where it comes from."
    },
    {
     "id": "SYNC-3",
     "title": "Give every person one ID",
     "tool": "ID map table",
     "level": "v1",
     "done": "One table links each person's IDs across your app, Stripe, HubSpot, and PostHog.",
     "verify": "Given any one ID, you can find the person in every tool."
    },
    {
     "id": "MODEL-5",
     "title": "Keep keys in the vault",
     "tool": "Vercel",
     "level": "v1",
     "done": "Keys the syncs and imports need are sensitive Vercel environment variables, never stored in plain columns.",
     "verify": "Searching the tables for a key's value finds nothing."
    },
    {
     "id": "SYNC-1",
     "title": "Mirror Stripe in your database",
     "tool": "Stripe sync",
     "level": "v1",
     "done": "Payments, customers, and subscriptions are mirrored, and updated by Stripe's events.",
     "verify": "A test payment appears in your mirror within a minute."
    },
    {
     "id": "SYNC-2",
     "title": "Sync your CRM both ways",
     "tool": "HubSpot sync",
     "level": "v1",
     "done": "New users flow to HubSpot as contacts, and the leads the app needs flow back.",
     "verify": "A new sign-up appears in HubSpot, matched to the existing lead."
    },
    {
     "id": "SYNC-4",
     "title": "Let the owner win",
     "tool": "conflict rules",
     "level": "v1",
     "done": "When a mirror and its owner disagree, the owner's value wins, written down per field.",
     "verify": "Change a value in the mirror by hand; the next scheduled reconcile puts the owner's value back."
    },
    {
     "id": "SYNC-5",
     "title": "Know when a sync breaks",
     "tool": "sync health",
     "level": "v1",
     "done": "Each sync records when it last succeeded, and you're alerted when it falls behind.",
     "verify": "Pausing a sync triggers an alert."
    }
   ]
  },
  {
   "n": 6,
   "title": "Understand your numbers",
   "checks": [
    {
     "id": "DATA-5",
     "title": "Write down what your numbers mean",
     "tool": "metric definitions",
     "level": "v1",
     "done": "“Active user,” “customer,” “revenue” each have one written definition.",
     "verify": "Two people asked the same question get the same number."
    },
    {
     "id": "ETL-1",
     "title": "Extract from every owner",
     "tool": "PostHog sources",
     "level": "v1",
     "done": "Copies of your database, Stripe, HubSpot, and product events land in the warehouse on a schedule.",
     "verify": "Every source shows a recent successful sync."
    },
    {
     "id": "ETL-2",
     "title": "Transform into tables you trust",
     "tool": "SQL models",
     "level": "v1",
     "done": "Raw copies are cleaned and joined into a few trusted tables, like one row per customer.",
     "verify": "Totals in the trusted tables match the owners' own totals."
    },
    {
     "id": "ETL-3",
     "title": "Load on a schedule",
     "tool": "PostHog warehouse",
     "level": "v1",
     "done": "The trusted tables refresh automatically, and everyone knows how fresh they are.",
     "verify": "The warehouse shows when each table last refreshed."
    },
    {
     "id": "ETL-4",
     "title": "Test your data",
     "tool": "data checks",
     "level": "v1",
     "done": "Automatic checks catch empty tables, missing values, duplicates, and stale data.",
     "verify": "Breaking a source on purpose makes a check fail."
    },
    {
     "id": "ETL-5",
     "title": "Answer questions with dashboards",
     "tool": "PostHog dashboards",
     "level": "v1",
     "done": "A few dashboards answer the questions you ask every week.",
     "verify": "Each chart says which trusted table it uses."
    },
    {
     "id": "ETL-6",
     "title": "Push segments back out",
     "tool": "reverse ETL",
     "level": "v1",
     "done": "Segments built in the warehouse update email lists and the CRM automatically.",
     "verify": "A person who joins a segment shows up in the right email list."
    }
   ]
  },
  {
   "n": 7,
   "title": "Protect people's data",
   "checks": [
    {
     "id": "DATA-3",
     "title": "List every copy and why it exists",
     "tool": "STACK.md data map",
     "level": "v1",
     "done": "Every copy is listed with its owner, why you keep it, and how it stays fresh.",
     "verify": "For each copy you can answer: if it were deleted, how would we rebuild it?"
    },
    {
     "id": "DATA-4",
     "title": "Find the personal data",
     "tool": "STACK.md data map",
     "level": "v1",
     "done": "Every place that holds names, emails, or other personal data is marked.",
     "verify": "A privacy request could be answered from this list alone."
    },
    {
     "id": "SAFE-2",
     "title": "Hash the tokens you issue",
     "tool": "API key table",
     "level": "v1",
     "done": "API keys you give out are stored only as hashes, shown to the user once.",
     "verify": "Reading the keys table never reveals a usable key."
    },
    {
     "id": "SAFE-3",
     "title": "Encrypt sensitive fields",
     "tool": "pgcrypto",
     "level": "v1",
     "done": "Especially sensitive values are encrypted, and only the server can read them.",
     "verify": "Reading the column directly shows unreadable text."
    },
    {
     "id": "SAFE-4",
     "title": "Keep personal data out of copies",
     "tool": "data minimization",
     "level": "v1",
     "done": "Analytics, logs, and the warehouse use IDs instead of names and emails wherever possible.",
     "verify": "Searching events and logs for a test user's email finds nothing."
    },
    {
     "id": "SAFE-5",
     "title": "Record who changed what",
     "tool": "audit log",
     "level": "v1",
     "done": "Important changes record who made them and when.",
     "verify": "Editing a record as a test user shows up in the audit log."
    },
    {
     "id": "SAFE-6",
     "title": "Handle export and deletion requests",
     "tool": "privacy runbook",
     "level": "v1",
     "done": "You can export one person's data, and delete it from every owner and copy.",
     "verify": "A test user's data is fully exported, then fully deleted everywhere."
    }
   ]
  },
  {
   "n": 8,
   "title": "Keep it running",
   "checks": [
    {
     "id": "OPS-1",
     "title": "Back up and practice restoring",
     "tool": "Neon",
     "level": "v1",
     "done": "Restore points exist, and you've restored one into a test branch.",
     "verify": "Yesterday's data comes back in a test branch."
    },
    {
     "id": "OPS-3",
     "title": "Rehearse every schema change",
     "tool": "preview branches",
     "level": "v1",
     "done": "Every migration runs on a preview branch with test data before production.",
     "verify": "A pull request shows its migration applied on its preview."
    },
    {
     "id": "OPS-2",
     "title": "Check health every week",
     "tool": "Neon",
     "level": "v1",
     "done": "Security and performance checks run weekly, and findings get fixed.",
     "verify": "The weekly check finds no unlocked tables and no missing indexes."
    },
    {
     "id": "OPS-4",
     "title": "Clean up old data on a schedule",
     "tool": "retention jobs",
     "level": "v1",
     "done": "Old logs, events, and stale copies are deleted on a schedule you've chosen.",
     "verify": "Rows older than the limit are gone the morning after."
    },
    {
     "id": "OPS-5",
     "title": "Watch size and growth",
     "tool": "usage reports",
     "level": "v1",
     "done": "You know how fast each table grows and when you'll need a bigger plan.",
     "verify": "You can say roughly when the database will hit its size limit."
    }
   ]
  }
 ],
 "nodes": [
  {
   "id": "people",
   "label": "People",
   "tool": "",
   "logos": [],
   "glyph": "person",
   "x": 95,
   "y": 110,
   "w": 140,
   "h": 58,
   "kind": "source",
   "desc": "Everyone using the app. Almost every fact starts with something a person does.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "DATA-1",
    "SAFE-6"
   ],
   "prompts": [
    "p-DATA-1",
    "p-SAFE-6"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "web",
   "label": "The web",
   "tool": "",
   "logos": [],
   "glyph": "globe",
   "x": 95,
   "y": 210,
   "w": 140,
   "h": 58,
   "kind": "source",
   "desc": "Public pages you pull data from: prices, listings, articles. Collected with a scraper like Firecrawl.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "DATA-1",
    "IN-4"
   ],
   "prompts": [
    "p-DATA-1",
    "p-IN-4"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "sheets",
   "label": "Spreadsheets",
   "tool": "",
   "logos": [
    "googlesheets"
   ],
   "glyph": null,
   "x": 95,
   "y": 305,
   "w": 140,
   "h": 58,
   "kind": "source",
   "desc": "Often a company's real records before the app exists. Import them once into whichever system should own them (your database, or a CRM for leads), then retire the sheet.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "DATA-1",
    "IN-3"
   ],
   "prompts": [
    "p-DATA-1",
    "p-IN-3"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "stripe",
   "label": "Stripe",
   "tool": "",
   "logos": [
    "stripe"
   ],
   "glyph": null,
   "x": 95,
   "y": 430,
   "w": 140,
   "h": 58,
   "kind": "owner",
   "desc": "Stripe owns money: charges, refunds, subscriptions. Your database keeps a synced mirror, never the truth.",
   "views": {
    "owners": "the truth for money",
    "copies": ""
   },
   "checks": [
    "SYNC-1"
   ],
   "prompts": [
    "p-SYNC-1"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "crm",
   "label": "CRM",
   "tool": "HubSpot",
   "logos": [
    "hubspot"
   ],
   "glyph": null,
   "x": 95,
   "y": 530,
   "w": 140,
   "h": 58,
   "kind": "owner",
   "desc": "HubSpot owns leads and deals: people who aren't users yet, and every sales conversation. Your database keeps a synced copy of the parts the app needs.",
   "views": {
    "owners": "the truth for leads",
    "copies": ""
   },
   "checks": [
    "SYNC-2"
   ],
   "prompts": [
    "p-SYNC-2"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "app",
   "label": "Your app",
   "tool": "Next.js · Vercel",
   "logos": [
    "nextdotjs",
    "vercel"
   ],
   "glyph": null,
   "x": 290,
   "y": 110,
   "w": 160,
   "h": 58,
   "kind": "door",
   "desc": "The only code allowed to write app facts. Every change a person makes goes through here.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "IN-1",
    "FAST-3",
    "FAST-4"
   ],
   "prompts": [
    "p-IN-1",
    "p-FAST-3",
    "p-FAST-4"
   ],
   "env": [
    "same",
    ""
   ],
   "link": {
    "href": "../finishing-your-prototypes/index.html",
    "zoom": "out"
   }
  },
  {
   "id": "imports",
   "label": "Imports",
   "tool": "Workflow · Firecrawl",
   "logos": [
    "vercel"
   ],
   "glyph": null,
   "x": 290,
   "y": 257,
   "w": 160,
   "h": 58,
   "kind": "door",
   "desc": "Background jobs that bring outside data in: scraping pages, importing spreadsheets, pulling from APIs.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "IN-1",
    "IN-5"
   ],
   "prompts": [
    "p-IN-1",
    "p-IN-5"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "syncs",
   "label": "Syncs",
   "tool": "Vercel",
   "logos": [
    "vercel"
   ],
   "glyph": null,
   "x": 290,
   "y": 480,
   "w": 160,
   "h": 58,
   "kind": "door",
   "desc": "Keeps your mirrors of outside owners matched: events pushed in by webhooks, scheduled pulls, and changes pushed back out.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "IN-2",
    "IN-5",
    "SYNC-3",
    "SYNC-4",
    "SYNC-5"
   ],
   "prompts": [
    "p-IN-2",
    "p-IN-5",
    "p-SYNC-3",
    "p-SYNC-4",
    "p-SYNC-5"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "storage",
   "label": "File storage",
   "tool": "Neon",
   "logos": [],
   "glyph": "folder",
   "x": 520,
   "y": 110,
   "w": 160,
   "h": 58,
   "kind": "owner",
   "desc": "Uploads and media live here. The database stores only each file's address.",
   "views": {
    "owners": "the truth for files",
    "copies": ""
   },
   "checks": [
    "MODEL-4"
   ],
   "prompts": [
    "p-MODEL-4"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "events",
   "label": "Product events",
   "tool": "PostHog",
   "logos": [
    "posthog"
   ],
   "glyph": null,
   "x": 705,
   "y": 110,
   "w": 160,
   "h": 58,
   "kind": "owner",
   "desc": "What people did: every click, sign-up, and step of the core flow. Behavior has its own owner.",
   "views": {
    "owners": "the truth for behavior",
    "copies": ""
   },
   "checks": [
    "SAFE-4"
   ],
   "prompts": [
    "p-SAFE-4"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "db",
   "label": "Database",
   "tool": "Neon",
   "logos": [
    "postgresql"
   ],
   "glyph": null,
   "x": 610,
   "y": 260,
   "w": 260,
   "h": 120,
   "kind": "owner",
   "desc": "The system of record: the one true copy of every app fact, locked so each person sees only their own rows.",
   "views": {
    "owners": "the truth for app facts",
    "copies": ""
   },
   "checks": [
    "MODEL-1",
    "MODEL-3",
    "SAFE-1",
    "FAST-1",
    "FAST-2",
    "SAFE-2",
    "OPS-4"
   ],
   "prompts": [
    "p-MODEL-1",
    "p-MODEL-3",
    "p-SAFE-1",
    "p-FAST-1",
    "p-FAST-2",
    "p-SAFE-2",
    "p-OPS-4"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "vault",
   "label": "Secret vault",
   "tool": "Vercel",
   "logos": [
    "vercel"
   ],
   "glyph": null,
   "x": 610,
   "y": 385,
   "w": 160,
   "h": 58,
   "kind": "owner",
   "desc": "Keys the syncs and imports need, like a CRM's API key, kept as sensitive environment variables and never in a table.",
   "views": {
    "owners": "the truth for keys",
    "copies": ""
   },
   "checks": [
    "MODEL-5",
    "SAFE-3"
   ],
   "prompts": [
    "p-MODEL-5",
    "p-SAFE-3"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "live",
   "label": "Live updates",
   "tool": "",
   "logos": [],
   "glyph": "refresh",
   "x": 932,
   "y": 110,
   "w": 150,
   "h": 58,
   "kind": "copy",
   "desc": "Pushes changes to open screens, so everyone sees the same thing without refreshing. Neon doesn't do this itself; it takes a realtime service, or polling where a short delay is fine.",
   "views": {
    "owners": "",
    "copies": "kept for speed"
   },
   "checks": [
    "FAST-7"
   ],
   "prompts": [
    "p-FAST-7"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "search",
   "label": "Search",
   "tool": "Neon",
   "logos": [
    "postgresql"
   ],
   "glyph": null,
   "x": 1132,
   "y": 110,
   "w": 150,
   "h": 58,
   "kind": "copy",
   "desc": "An index for finding things by words or meaning (including AI search with embeddings).",
   "views": {
    "owners": "",
    "copies": "kept for finding"
   },
   "checks": [
    "FAST-6"
   ],
   "prompts": [
    "p-FAST-6"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "cache",
   "label": "Cache",
   "tool": "Vercel",
   "logos": [
    "vercel"
   ],
   "glyph": null,
   "x": 932,
   "y": 200,
   "w": 150,
   "h": 58,
   "kind": "copy",
   "desc": "Saved answers to repeated questions, so pages load fast and the database rests.",
   "views": {
    "owners": "",
    "copies": "kept for speed"
   },
   "checks": [
    "FAST-5"
   ],
   "prompts": [
    "p-FAST-5"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "extract",
   "label": "Extract",
   "tool": "PostHog",
   "logos": [
    "posthog"
   ],
   "glyph": null,
   "x": 932,
   "y": 335,
   "w": 150,
   "h": 58,
   "kind": "copy",
   "desc": "Copies data out of every owner on a schedule: your database, Stripe, HubSpot, and PostHog's events.",
   "views": {
    "owners": "",
    "copies": "kept raw, for questions"
   },
   "checks": [
    "ETL-1"
   ],
   "prompts": [
    "p-ETL-1"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "transform",
   "label": "Transform",
   "tool": "PostHog",
   "logos": [
    "posthog"
   ],
   "glyph": null,
   "x": 1132,
   "y": 335,
   "w": 150,
   "h": 58,
   "kind": "copy",
   "desc": "Cleans and joins the raw copies into tables people can trust: one row per customer, one definition of “active.”",
   "views": {
    "owners": "",
    "copies": "kept clean, for questions"
   },
   "checks": [
    "ETL-2",
    "ETL-4"
   ],
   "prompts": [
    "p-ETL-2",
    "p-ETL-4"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "warehouse",
   "label": "Warehouse",
   "tool": "PostHog",
   "logos": [
    "posthog"
   ],
   "glyph": null,
   "x": 1132,
   "y": 440,
   "w": 150,
   "h": 58,
   "kind": "copy",
   "desc": "Where the transformed tables are loaded, ready to answer questions without slowing down the live database.",
   "views": {
    "owners": "",
    "copies": "kept for questions"
   },
   "checks": [
    "ETL-3"
   ],
   "prompts": [
    "p-ETL-3"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "dashboards",
   "label": "Dashboards",
   "tool": "PostHog",
   "logos": [
    "posthog"
   ],
   "glyph": null,
   "x": 932,
   "y": 440,
   "w": 150,
   "h": 58,
   "kind": "copy",
   "desc": "Charts that answer the business questions you ask every week.",
   "views": {
    "owners": "",
    "copies": "kept for answers"
   },
   "checks": [
    "DATA-5",
    "ETL-5"
   ],
   "prompts": [
    "p-DATA-5",
    "p-ETL-5"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "lists",
   "label": "Email lists",
   "tool": "Resend",
   "logos": [
    "resend"
   ],
   "glyph": null,
   "x": 1132,
   "y": 540,
   "w": 150,
   "h": 58,
   "kind": "copy",
   "desc": "Segments built in the warehouse, like “trial users who haven't paid,” pushed out to Resend.",
   "views": {
    "owners": "",
    "copies": "kept for outreach"
   },
   "checks": [
    "ETL-6"
   ],
   "prompts": [
    "p-ETL-6"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "migrations",
   "label": "Migrations",
   "tool": "Drizzle",
   "logos": [
    "drizzle"
   ],
   "glyph": null,
   "x": 115,
   "y": 670,
   "w": 170,
   "h": 58,
   "kind": "ops",
   "desc": "Every change to your tables is a file in git, reviewed and replayable. Never click-to-edit the schema.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "MODEL-2",
    "OPS-3"
   ],
   "prompts": [
    "p-MODEL-2",
    "p-OPS-3"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "testdata",
   "label": "Test data",
   "tool": "Neon",
   "logos": [
    "postgresql"
   ],
   "glyph": null,
   "x": 315,
   "y": 670,
   "w": 170,
   "h": 58,
   "kind": "copy",
   "desc": "A database branch for every preview, with the schema only and fake data. Real customer data never leaves production.",
   "views": {
    "owners": "",
    "copies": "kept for safe testing"
   },
   "checks": [
    "MODEL-6"
   ],
   "prompts": [
    "p-MODEL-6"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "backups",
   "label": "Backups",
   "tool": "Neon",
   "logos": [
    "postgresql"
   ],
   "glyph": null,
   "x": 515,
   "y": 670,
   "w": 170,
   "h": 58,
   "kind": "copy",
   "desc": "A history of the database you can rewind to any moment within it. A backup you've never restored is only a hope.",
   "views": {
    "owners": "",
    "copies": "kept for bad days"
   },
   "checks": [
    "OPS-1"
   ],
   "prompts": [
    "p-OPS-1"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "health",
   "label": "Health checks",
   "tool": "Neon",
   "logos": [
    "postgresql"
   ],
   "glyph": null,
   "x": 715,
   "y": 670,
   "w": 170,
   "h": 58,
   "kind": "ops",
   "desc": "Automatic checks for unlocked tables, missing indexes, and slow queries.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "OPS-2",
    "OPS-5"
   ],
   "prompts": [
    "p-OPS-2",
    "p-OPS-5"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "audit",
   "label": "Audit log",
   "tool": "",
   "logos": [],
   "glyph": "steps",
   "x": 915,
   "y": 670,
   "w": 170,
   "h": 58,
   "kind": "ops",
   "desc": "A permanent record of who changed which row, and when.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "SAFE-5"
   ],
   "prompts": [
    "p-SAFE-5"
   ],
   "env": [
    "same",
    ""
   ]
  },
  {
   "id": "privacy",
   "label": "Privacy requests",
   "tool": "",
   "logos": [],
   "glyph": "shield",
   "x": 1115,
   "y": 670,
   "w": 170,
   "h": 58,
   "kind": "ops",
   "desc": "When someone asks for their data, or asks you to delete it, it has to happen in every owner and every copy.",
   "views": {
    "owners": "",
    "copies": ""
   },
   "checks": [
    "DATA-4",
    "SAFE-6"
   ],
   "prompts": [
    "p-DATA-4",
    "p-SAFE-6"
   ],
   "env": [
    "same",
    ""
   ]
  }
 ],
 "edges": [
  {
   "id": "people-app",
   "from": "people",
   "to": "app",
   "path": "req",
   "pts": [
    [
     165,
     110
    ],
    [
     210,
     110
    ]
   ],
   "label": "uses"
  },
  {
   "id": "app-storage",
   "from": "app",
   "to": "storage",
   "path": "req",
   "pts": [
    [
     370,
     98
    ],
    [
     440,
     98
    ]
   ],
   "label": "uploads"
  },
  {
   "id": "app-events",
   "from": "app",
   "to": "events",
   "path": "req",
   "pts": [
    [
     290,
     81
    ],
    [
     290,
     42
    ],
    [
     705,
     42
    ],
    [
     705,
     81
    ]
   ],
   "label": "events",
   "labelAt": [
    560,
    42
   ]
  },
  {
   "id": "app-db",
   "from": "app",
   "to": "db",
   "path": "req",
   "pts": [
    [
     370,
     122
    ],
    [
     410,
     122
    ],
    [
     410,
     230
    ],
    [
     480,
     230
    ]
   ],
   "label": "writes"
  },
  {
   "id": "web-imports",
   "from": "web",
   "to": "imports",
   "path": "req",
   "pts": [
    [
     165,
     210
    ],
    [
     250,
     210
    ],
    [
     250,
     228
    ]
   ],
   "label": "scraped",
   "labelAt": [
    207,
    210
   ]
  },
  {
   "id": "sheets-imports",
   "from": "sheets",
   "to": "imports",
   "path": "req",
   "pts": [
    [
     165,
     305
    ],
    [
     250,
     305
    ],
    [
     250,
     286
    ]
   ],
   "label": "imported",
   "labelAt": [
    207,
    305
   ]
  },
  {
   "id": "imports-db",
   "from": "imports",
   "to": "db",
   "path": "req",
   "pts": [
    [
     370,
     257
    ],
    [
     480,
     257
    ]
   ],
   "label": "loads"
  },
  {
   "id": "stripe-syncs",
   "from": "stripe",
   "to": "syncs",
   "path": "req",
   "pts": [
    [
     165,
     430
    ],
    [
     188,
     430
    ],
    [
     188,
     470
    ],
    [
     210,
     470
    ]
   ],
   "label": "events",
   "labelAt": [
    176,
    430
   ]
  },
  {
   "id": "crm-syncs",
   "from": "crm",
   "to": "syncs",
   "path": "req",
   "pts": [
    [
     165,
     530
    ],
    [
     188,
     530
    ],
    [
     188,
     490
    ],
    [
     210,
     490
    ]
   ],
   "label": "two-way",
   "labelAt": [
    176,
    530
   ],
   "both": true
  },
  {
   "id": "syncs-db",
   "from": "syncs",
   "to": "db",
   "path": "req",
   "pts": [
    [
     370,
     480
    ],
    [
     410,
     480
    ],
    [
     410,
     295
    ],
    [
     480,
     295
    ]
   ],
   "label": "mirrors",
   "both": true
  },
  {
   "id": "vault-db",
   "from": "vault",
   "to": "db",
   "path": "req",
   "pts": [
    [
     610,
     356
    ],
    [
     610,
     320
    ]
   ],
   "label": "keys",
   "side": "right"
  },
  {
   "id": "db-live",
   "from": "db",
   "to": "live",
   "path": "dep",
   "pts": [
    [
     740,
     230
    ],
    [
     790,
     230
    ],
    [
     790,
     110
    ],
    [
     857,
     110
    ]
   ]
  },
  {
   "id": "db-search",
   "from": "db",
   "to": "search",
   "path": "dep",
   "pts": [
    [
     790,
     230
    ],
    [
     790,
     158
    ],
    [
     1132,
     158
    ],
    [
     1132,
     139
    ]
   ],
   "label": "indexed",
   "labelAt": [
    1042,
    158
   ],
   "below": true
  },
  {
   "id": "db-cache",
   "from": "db",
   "to": "cache",
   "path": "dep",
   "pts": [
    [
     790,
     230
    ],
    [
     790,
     200
    ],
    [
     857,
     200
    ]
   ]
  },
  {
   "id": "db-extract",
   "from": "db",
   "to": "extract",
   "path": "dep",
   "pts": [
    [
     740,
     270
    ],
    [
     815,
     270
    ],
    [
     815,
     335
    ],
    [
     857,
     335
    ]
   ],
   "label": "extract",
   "labelAt": [
    790,
    270
   ]
  },
  {
   "id": "extract-transform",
   "from": "extract",
   "to": "transform",
   "path": "dep",
   "pts": [
    [
     1007,
     335
    ],
    [
     1057,
     335
    ]
   ]
  },
  {
   "id": "transform-warehouse",
   "from": "transform",
   "to": "warehouse",
   "path": "dep",
   "pts": [
    [
     1132,
     364
    ],
    [
     1132,
     411
    ]
   ],
   "label": "load",
   "side": "right",
   "labelAt": [
    1132,
    388
   ]
  },
  {
   "id": "warehouse-dash",
   "from": "warehouse",
   "to": "dashboards",
   "path": "dep",
   "pts": [
    [
     1057,
     440
    ],
    [
     1007,
     440
    ]
   ],
   "label": "answers"
  },
  {
   "id": "warehouse-lists",
   "from": "warehouse",
   "to": "lists",
   "path": "dep",
   "pts": [
    [
     1132,
     469
    ],
    [
     1132,
     511
    ]
   ],
   "label": "reverse ETL",
   "side": "right",
   "labelAt": [
    1132,
    490
   ]
  },
  {
   "id": "migrations-db",
   "from": "migrations",
   "to": "db",
   "path": "fb",
   "pts": [
    [
     115,
     641
    ],
    [
     115,
     600
    ],
    [
     492,
     600
    ],
    [
     492,
     320
    ]
   ],
   "label": "changes",
   "labelAt": [
    300,
    600
   ]
  },
  {
   "id": "migrations-test",
   "from": "migrations",
   "to": "testdata",
   "path": "dep",
   "pts": [
    [
     200,
     670
    ],
    [
     230,
     670
    ]
   ]
  },
  {
   "id": "db-backups",
   "from": "db",
   "to": "backups",
   "path": "dep",
   "pts": [
    [
     515,
     320
    ],
    [
     515,
     641
    ]
   ],
   "label": "nightly",
   "side": "right",
   "labelAt": [
    515,
    560
   ]
  },
  {
   "id": "health-db",
   "from": "health",
   "to": "db",
   "path": "fb",
   "pts": [
    [
     715,
     641
    ],
    [
     715,
     320
    ]
   ],
   "label": "checks",
   "side": "right",
   "labelAt": [
    715,
    560
   ]
  },
  {
   "id": "db-audit",
   "from": "db",
   "to": "audit",
   "path": "fb",
   "pts": [
    [
     740,
     300
    ],
    [
     765,
     300
    ],
    [
     765,
     600
    ],
    [
     915,
     600
    ],
    [
     915,
     641
    ]
   ],
   "label": "records",
   "labelAt": [
    850,
    600
   ]
  }
 ],
 "zones": [
  {
   "id": "sources",
   "label": "sources",
   "x": 18,
   "y": 62,
   "w": 154,
   "h": 284
  },
  {
   "id": "outside",
   "label": "outside owners",
   "x": 18,
   "y": 385,
   "w": 154,
   "h": 190
  },
  {
   "id": "doors",
   "label": "ways in",
   "x": 202,
   "y": 62,
   "w": 176,
   "h": 454
  },
  {
   "id": "owners",
   "label": "your owners",
   "x": 420,
   "y": 62,
   "w": 372,
   "h": 372
  },
  {
   "id": "appcopies",
   "label": "copies for performance",
   "x": 832,
   "y": 62,
   "w": 397,
   "h": 180,
   "labelRight": true
  },
  {
   "id": "etl",
   "label": "etl pipeline",
   "x": 832,
   "y": 278,
   "w": 397,
   "h": 300,
   "labelRight": true
  },
  {
   "id": "care",
   "label": "data care",
   "x": 18,
   "y": 618,
   "w": 1211,
   "h": 96
  }
 ],
 "tours": {
  "req": {
   "label": "a sign-up",
   "steps": [
    {
     "edge": "people-app",
     "text": "Maya signs up."
    },
    {
     "edge": "app-db",
     "text": "Her account is written to the database, the one true copy."
    },
    {
     "edge": "app-storage",
     "text": "Her photo goes to file storage. The database keeps only its address."
    },
    {
     "edge": "app-events",
     "text": "PostHog records that she signed up."
    },
    {
     "edge": "syncs-db",
     "text": "A sync picks up the new user..."
    },
    {
     "edge": "crm-syncs",
     "text": "...and matches her to the lead HubSpot already had. Both sides stay in step."
    }
   ]
  },
  "dep": {
   "label": "a question",
   "steps": [
    {
     "edge": "stripe-syncs",
     "text": "Stripe takes a payment and tells your app."
    },
    {
     "edge": "syncs-db",
     "text": "The sync records a mirror of it in your database. Stripe stays the owner."
    },
    {
     "edge": "db-extract",
     "text": "Overnight, the pipeline extracts copies from every owner: your database, Stripe, HubSpot, and PostHog."
    },
    {
     "edge": "extract-transform",
     "text": "It cleans and joins them: one row per customer, with payments and activity side by side."
    },
    {
     "edge": "transform-warehouse",
     "text": "It loads the result into the warehouse."
    },
    {
     "edge": "warehouse-dash",
     "text": "Your dashboard answers: which features do paying customers use?"
    }
   ]
  },
  "fb": {
   "label": "a bad day",
   "steps": [
    {
     "edge": "migrations-db",
     "text": "A change to the tables deletes the wrong column."
    },
    {
     "edge": "db-audit",
     "text": "The audit log shows exactly what changed, and when."
    },
    {
     "edge": "db-backups",
     "text": "Last night's backup still has it. You restore it into a test project and copy the rows back."
    },
    {
     "edge": "migrations-test",
     "text": "Next time, the change runs on a preview branch with test data first."
    }
   ]
  }
 },
 "prompts": {
  "p-MODEL-1": {
   "id": "p-MODEL-1",
   "title": "Design your tables",
   "stage": 1,
   "checks": [
    "MODEL-1"
   ],
   "primary": true,
   "body": "Make this app's database tables match how the business actually works, with one plain sentence per table on what it holds, so anyone can understand the data without reading the code.\n\nWork out the business from the code and whatever data exists; there may be a database or nothing yet. Neon Postgres, added through the Vercel Marketplace, is a good default if nothing is set up. The classic failures are facts stored twice, catch-all fields hiding data that deserves its own table, missing links between tables, and columns that accept empty or nonsense values. Money and times go wrong quietly: keep money in whole cents, and times with their time zone.\n\nResearch current practice and choose the simplest design that fits, making changes the way the project already does (ideally migration files, not dashboard edits). Before changing anything, show me the tables, how they connect, the rules each enforces, and what you'd do differently. Never lose existing data; move it first.\n\nTry it on a local or preview database, never production, then explain every table from the schema alone. Record them in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-MODEL-2": {
   "id": "p-MODEL-2",
   "title": "Make every change a migration",
   "stage": 1,
   "checks": [
    "MODEL-2"
   ],
   "primary": true,
   "body": "Make every change to the database's structure a migration file in git, so the database can be rebuilt exactly from the repo and nobody edits tables by hand in the dashboard.\n\nThe project may have migrations, partial ones, or none, and production may have drifted from them. A migration tool such as Drizzle is a good default, with migrations run against Neon from the repository. The classic failures are editing tables or policies in the dashboard, and changing a migration that already ran in production instead of writing a new one.\n\nResearch the current recommended workflow and look at what exists. If production and the files disagree, capture the real difference rather than guessing. Before saving anything, show me that difference, how migrations will reach production and who may apply them, and what you'd do differently.\n\nThen build a fresh database from the migrations alone, somewhere safe, and show me it matches production apart from what the platform manages itself. Write the workflow down in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-MODEL-3": {
   "id": "p-MODEL-3",
   "title": "Lock every table",
   "stage": 1,
   "checks": [
    "MODEL-3"
   ],
   "primary": true,
   "body": "Make sure people can read and change only the rows they're allowed to, in every table, with each rule also written in plain English.\n\nWith Neon, the app reaches the database only from server code, so every query must be scoped to the signed-in person, and Postgres row-level security can add a second lock that holds even when a query forgets. Work out who should see what from the code, and ask me where it's unclear. The classic failures are a query that forgets the owner filter, a rule that lets everyone in, and the database connection string in browser code, which skips every rule.\n\nResearch current practice and check what's in place. Before changing anything, show me each table's rules as plain sentences, like \"a user can edit only their own projects,\" plus anything risky and what you'd do differently.\n\nTry it with two test users on a local or preview database. Show me neither can touch the other's rows, and the platform's security checks finding no unlocked tables. Record the rules in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-MODEL-6": {
   "id": "p-MODEL-6",
   "title": "Seed realistic test data",
   "stage": 1,
   "checks": [
    "MODEL-6"
   ],
   "primary": true,
   "body": "Give every preview and local database realistic fake data, so new features get tested properly without ever touching real customers.\n\nNeon gives each Vercel preview its own database branch, which by default copies production data; use schema-only branches loaded from a seed file instead, so real customer data stays in production. A seed file may exist already. A good seed is a small version of the real business: users with different roles who can sign in, and awkward cases like empty accounts and cancelled subscriptions. The classic failures are copying \"just a few\" production rows into the seed, and previews wired to live payment, CRM, or email keys.\n\nLook at the schema, research the current way to seed this setup, and choose the simplest approach. Before writing data, show me what the fake business will look like and what you'd do differently.\n\nTry it on a preview or local database. Show me the sample data loading cleanly and evidence that no real customer data is there. Record the test logins and how seeding works in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-MODEL-4": {
   "id": "p-MODEL-4",
   "title": "Store files outside the database",
   "stage": 2,
   "checks": [
    "MODEL-4"
   ],
   "primary": true,
   "body": "Keep uploaded files in proper file storage instead of the database, with rules so only each file's owner can reach them, while the database keeps only each file's address.\n\nFiles may live in database columns, another service, or nowhere; if the app handles no files, say so and stop. Neon Object Storage is a good default, with private files organized by owner and handed out by the server as short-lived links. The classic failures are permanent public links to private files, and storage or admin keys in browser code, which skip every rule.\n\nResearch current practice and choose the simplest approach. Before building, show me every kind of file, who should see each, how you'll move existing files without losing any, and what you'd do differently.\n\nTry it on a preview with two test users. Show me neither can read, overwrite, or delete the other's files. Record where files live and their rules in our tech stack notes (probably a file like STACK.md or the README) and a data map, if we've written one."
  },
  "p-SAFE-1": {
   "id": "p-SAFE-1",
   "title": "Never store passwords",
   "stage": 2,
   "checks": [
    "SAFE-1"
   ],
   "primary": true,
   "body": "Make sure this app never stores passwords itself, so a leaked database can't leak anyone's password. Only the sign-in service should handle them.\n\nFind how people sign in from the code and our tech stack notes (probably a file like STACK.md or the README). Neon Auth is the usual owner; it keeps salted hashes, which check a password but can't be reversed. Look for anything storing or sending a password or PIN, including custom sign-in code and logs that capture request bodies. The app may already be clean; if so, show me and stop. The classic mistakes are hashing passwords yourself in your own table, and copying existing passwords across instead of asking those users to reset.\n\nResearch current best practice. Before changing anything, show me what you found and your plan, especially for existing users. Use a preview with test accounts.\n\nShow me that no table holds a password, and that a test user's password appears nowhere in logs or analytics. Record in our tech stack notes that the sign-in service owns passwords."
  },
  "p-DATA-1": {
   "id": "p-DATA-1",
   "title": "List every source of data",
   "stage": 3,
   "checks": [
    "DATA-1"
   ],
   "primary": true,
   "body": "Write down every place this app's data comes from, so nothing enters the business without us knowing: people, the app, imports, spreadsheets, websites, and outside tools.\n\nWork it out from the code, the database, and the connected services rather than asking me to describe the app. A list may already exist in our tech stack notes (probably a file like STACK.md or the README) or a data map, if we've written one; don't assume it's complete. The classic misses are data that never passes through the code, like payments in Stripe or leads in a CRM, and sources kept by hand.\n\nFor each source, note what comes in, how, how often, and what handles it. Never invent a source you can't point to; mark it unconfirmed and ask.\n\nBefore saving, show me the list and the questions only I can answer. Then search again from scratch and show me nothing new turns up. Save it with our tech stack notes as the start of a data map."
  },
  "p-IN-1": {
   "id": "p-IN-1",
   "title": "Validate everything at the door",
   "stage": 3,
   "checks": [
    "IN-1"
   ],
   "primary": true,
   "body": "Check every piece of data entering this app for shape and sense before it reaches the database, so bad data is refused with a clear error and nothing is saved.\n\nFind every way data gets in: forms, API routes, server actions, webhooks, and imports. Some may already validate. If the app has a validation library, keep it; otherwise Zod is a good default for TypeScript, with database constraints as a last line of defense. The classic failures are trusting browser checks alone, and saving half a request when the other half fails.\n\nResearch current practice and choose the simplest consistent approach. Before changing anything, show me the entry points, what each accepts, any that look risky, and what you'd do differently.\n\nTry it on a local or preview deployment, not production. Show me malformed requests refused with clear errors, and the database unchanged afterward. Record the entry points and how they're checked in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-IN-3": {
   "id": "p-IN-3",
   "title": "Import spreadsheets once, cleanly",
   "stage": 3,
   "checks": [
    "IN-3"
   ],
   "primary": true,
   "body": "Import the data that lives in spreadsheets into the database once, cleanly, so the system that should own it (usually the app; a CRM for sales leads) becomes its owner and the spreadsheet can be retired.\n\nA list of data sources, if we've written one, may name the spreadsheets; otherwise ask me, and stop if there are none. The hard part is matching: deciding what makes two rows the same person or thing, often a cleaned-up email, so a second run updates rows instead of duplicating them. The other classic failure is a spreadsheet people keep editing, a second owner.\n\nResearch a sensible approach and reuse whatever validation the app has. Before importing, show me how columns map to tables, how you'll handle blanks and bad rows, the matching rule, and what you'd do differently. Wait for my yes.\n\nRun it on a local or preview database first. Show me the counts, rejected rows, and that a second run adds no duplicates. Then run it on production with my OK. Record it, and the retired spreadsheet, in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-IN-2": {
   "id": "p-IN-2",
   "title": "Receive webhooks safely",
   "stage": 3,
   "checks": [
    "IN-2"
   ],
   "primary": true,
   "body": "Make every webhook this app receives safe: check that it really came from the sender, store each event exactly once, and reply fast.\n\nLook for webhook routes in the code and our tech stack notes, probably a file like STACK.md or the README; if there are none, say so and stop. Senders like Stripe repeat events, send them out of order, and retry slow replies. The usual approach is to verify the signature first, record each event under a unique key so repeats are ignored, reply right away, and do the real work afterward. The classic failures are slow work before replying, trusting event order, and parsing the body before checking the signature. Keep stored events away from the browser.\n\nResearch each sender's current guidance. Before building, show me your plan, anything risky, and what you'd do differently.\n\nTry it on a preview with test events. Show me a repeated event stored once and a forged one rejected. Record each webhook in our tech stack notes and a data map, if we've written one."
  },
  "p-IN-5": {
   "id": "p-IN-5",
   "title": "Run background jobs with retries",
   "stage": 3,
   "checks": [
    "IN-5"
   ],
   "primary": true,
   "body": "Move slow or failure-prone work into background jobs that retry safely and never do the same thing twice, even if they crash halfway.\n\nLook for work that's slow, depends on an outside service, or would hurt if repeated, like processing webhooks, imports, emails, and syncs. Vercel Workflow is a good default on Vercel, since it retries each step on its own. The classic failure is a retry that sends the email or charges the card twice. Every step must be safe to repeat, by checking whether the work is done or passing a unique key to services that accept one.\n\nResearch current practice and choose the simplest setup. Before changing code, show me each task, what would break if it ran twice, and what you'd do differently.\n\nTry it on a preview deployment with test accounts. Crash a job partway, restart it, and show me nothing was duplicated. Record each job and what starts it in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-IN-4": {
   "id": "p-IN-4",
   "title": "Scrape responsibly",
   "stage": 3,
   "checks": [
    "IN-4"
   ],
   "primary": true,
   "body": "Bring in data from public websites responsibly: respect each site's rules, record where and when every row came from, and refresh on a schedule.\n\nFirst check whether the app needs scraped data at all; if not, say so and stop. Firecrawl is a good default, with its key kept on the server. Each row should record its source and fetch time. The classic failures are scraping disallowed or logged-in pages, hitting a site too fast, and keeping personal data you don't need.\n\nResearch current practice and read each site's rules and terms. Before scraping, show me what's allowed, the fields you'd take, a refresh schedule as rare as the need allows, and what you'd do differently.\n\nTry it on a local or preview database. Show me sample rows with their source and fetch time, and that a second run updates rows instead of duplicating them. Record each site, its rules, and the schedule in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-FAST-1": {
   "id": "p-FAST-1",
   "title": "Measure before you optimize",
   "stage": 4,
   "checks": [
    "FAST-1"
   ],
   "primary": true,
   "body": "Find out which database queries in this app are actually slow or expensive, before anyone changes anything to make it faster. This step only measures.\n\nNeon's query performance and monitoring views are a good start, along with the database's own statistics on how often each query runs and for how long. Rank by total time as well as average; a fast query run a million times can cost more than one slow report. Match each top query to the feature that sends it, and read the worst few query plans to see why. The classic mistake is guessing from the code.\n\nResearch the best way to measure on our setup, and change nothing. If nothing is meaningfully slow, say so and stop; that's a good result.\n\nShow me the three most expensive queries with average time, calls per day, and total time, plus advisor findings. Save this as a dated baseline linked from our tech stack notes (probably a file like STACK.md or the README), so later fixes can compare."
  },
  "p-FAST-3": {
   "id": "p-FAST-3",
   "title": "Fetch only what you need",
   "stage": 4,
   "checks": [
    "FAST-3"
   ],
   "primary": true,
   "body": "Make this app's pages fetch only what they show: the columns they use, long lists a page at a time, and never one query per row.\n\nMeasure first. A baseline of slow queries may be linked from our tech stack notes (probably a file like STACK.md or the README); if not, load the main list pages with realistic data and count queries and load time. The classic problems are selecting every column, unlimited lists, and a query inside a loop, the N+1 problem. If pages already make a few cheap queries, tell me and stop.\n\nResearch current best practice for our stack, and fix only what measurement shows is costly. Before changing code, show me each problem, its file, its cost, and the fix. Never drop the per-person access checks for speed, and never paginate by loading everything and hiding it in the browser.\n\nTry fixes on a branch or preview. Show me before and after numbers side by side, and record the paging approach in our tech stack notes."
  },
  "p-FAST-2": {
   "id": "p-FAST-2",
   "title": "Index the queries you actually run",
   "stage": 4,
   "checks": [
    "FAST-2"
   ],
   "primary": true,
   "body": "Add database indexes only for the slow queries this app really runs, and prove each one is used and makes its query faster.\n\nA baseline of slow queries may be linked from our tech stack notes (probably a file like STACK.md or the README). If not, measure first; if nothing is slow, tell me and stop. Read each slow query's plan with realistic inputs. Unindexed foreign keys are a common find; they slow joins and deletes. The classic failure is indexing on a hunch: every index slows writes and takes space, and the database may ignore it.\n\nResearch current best practice and propose one index per slow query in plain English, with why it fits. Show me the list before creating anything. Add them the way this project makes schema changes, on a preview first.\n\nShow me each query's plan and time before and after, with the index in use, and drop any index the plan ignores. Record each index and its query in our tech stack notes."
  },
  "p-FAST-4": {
   "id": "p-FAST-4",
   "title": "Pool your connections",
   "stage": 4,
   "checks": [
    "FAST-4"
   ],
   "primary": true,
   "body": "Make sure a burst of traffic can't use up the database's connections, by having the app's server code connect through a connection pooler.\n\nFirst find how the app connects. Neon's serverless driver talks over HTTP and needs no pooling; direct Postgres connections, such as from an ORM, do. If nothing connects directly, tell me and stop. For serverless functions, Neon's pooled connection string is the usual answer, with the direct connection kept for migrations. Pooled, per-transaction mode doesn't support everything a direct connection does, prepared statements especially, so check what our database client needs. The classic failures are raising the connection limit to hide the problem, and a connection string reachable from browser code.\n\nResearch current guidance. Measure first with a burst of simultaneous requests against a preview whose database isn't production, and show me the results and your planned change before editing anything.\n\nThen rerun the burst and show me before and after: connection errors, peak connections, and response times. Record which connection each part of the app uses in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-FAST-6": {
   "id": "p-FAST-6",
   "title": "Make search good",
   "stage": 4,
   "checks": [
    "FAST-6"
   ],
   "primary": true,
   "body": "Make search in this app find the right things by words, by close misspellings, and where worth it, by meaning, quickly and using an index.\n\nIf the app has no search, stop. Measure each search box with realistic data; a plain \"contains this text\" match scans the whole table. Postgres can do all of this: full-text search for words, trigram matching for typos, vector search for meaning, often ranked together. Meaning search costs more and is often unnecessary. The classic failures are slow wildcard matching, search that ignores access rules, and sending private rows to an outside service unasked.\n\nResearch current best practice. Before changing anything, show me what each search needs, your plan, and what you rejected. Make schema changes the way the project already does, on a preview.\n\nShow me a misspelled search returning the right result first, before and after times, and the plan using the index. Record the setup in our tech stack notes (probably a file like STACK.md or the README), noting the index is a copy."
  },
  "p-FAST-7": {
   "id": "p-FAST-7",
   "title": "Use live updates only where they matter",
   "stage": 4,
   "checks": [
    "FAST-7"
   ],
   "primary": true,
   "body": "Use live updates only on screens people really share and change at the same time, like a shared board or chat.\n\nFind every place the app listens for live changes or polls, and measure each one's cost. If nothing is live and no screen is truly shared, tell me and stop. Neon doesn't push changes to screens, so this needs a realtime service (Ably and Pusher are common choices) that sends each person only what they may see, or simple polling where a few seconds' delay is fine. The classic failures are subscribing to a whole table just in case, and live updates on a table without access rules, which leak other people's rows.\n\nResearch current best practice. Before changing anything, show me each screen and what you'd keep, add, or remove. Use a preview with test accounts.\n\nShow me two test users seeing each other's change within a second, a third user without access receiving nothing, and before and after request counts. Record which screens are live in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-FAST-5": {
   "id": "p-FAST-5",
   "title": "Cache only what's proven slow",
   "stage": 4,
   "checks": [
    "FAST-5"
   ],
   "primary": true,
   "body": "Add caching only for reads that measurement shows are still slow, and make each cache clear the moment its data changes, so nobody sees stale data.\n\nA baseline of slow queries may be linked from our tech stack notes (probably a file like STACK.md or the README). Measure what's still slow after indexes and query fixes; if nothing is, tell me and stop. On Vercel, its runtime cache with tags is a good default. A cache is a copy; the database stays the owner. The classic failures are one user's private data served to another, and a timer as the only way to clear stale entries.\n\nResearch current best practice. Before caching anything, show me each read you'd cache, who sees it, how often it changes, and which writes must clear it.\n\nTry it on a preview. Show me before and after response times, and an update made through the app appearing on the very next load. Record each cache and what clears it in our tech stack notes."
  },
  "p-DATA-2": {
   "id": "p-DATA-2",
   "title": "Name the owner of every fact",
   "stage": 5,
   "checks": [
    "DATA-2"
   ],
   "primary": true,
   "body": "Give every kind of fact this business keeps exactly one owner, its system of record, so we always know which version is the truth. Everything else is a copy.\n\nWork it out from the code, the database, and the connected tools, starting from a data map, if we've written one, perhaps in a file like STACK.md or the README. The usual owners: the database for app facts, file storage for files, analytics for behavior, Stripe for money, and the CRM (often HubSpot) for leads and deals. The classic failure is a fact two systems both write, like a subscription status the app edits while Stripe changes it too.\n\nName each fact's owner, mark which columns copy something owned elsewhere, and say where you'd draw the lines differently. Show me every conflict before saving; I decide those.\n\nThen pick a few columns from different tables and show me each one's owner and the code or sync that writes it. Record the owners in the data map, creating it if needed."
  },
  "p-SYNC-3": {
   "id": "p-SYNC-3",
   "title": "Give every person one ID",
   "stage": 5,
   "checks": [
    "SYNC-3"
   ],
   "primary": true,
   "body": "Give every person one ID, linked to their ID in every outside tool, so any one ID finds them everywhere: the app, Stripe, the CRM, analytics, and so on.\n\nCheck which tools the app actually connects to; our tech stack notes (probably a file like STACK.md or the README) or a data map, if we've written one, may list them. The usual approach is a small table keyed by the app's user ID, with one unique column per outside ID, written only by the server whenever an outside record is created. The classic failures are using email as the ID (emails change), creating a second record for one person, and guessing matches for existing users.\n\nLook at how IDs are stored today and research current practice. Show me the plan and what you'd do differently before changing anything.\n\nTry it on a local or preview database. Match existing users carefully, listing any you couldn't. Then show me one test user found from each of their IDs. Record the design in our tech stack notes."
  },
  "p-MODEL-5": {
   "id": "p-MODEL-5",
   "title": "Keep keys in the vault",
   "stage": 5,
   "checks": [
    "MODEL-5"
   ],
   "primary": true,
   "body": "Make sure any secret key the database itself needs, for syncs, wrappers, or jobs calling outside services, is stored encrypted, never in a plain column, function, or migration.\n\nThe database may hold no keys; check, and stop if so. Keys only the app uses belong in the hosting platform's environment variables instead. Keys the database's own jobs need are rare with Neon; if there are any, encrypt them with a key kept in those environment variables. The classic failures are a key committed to git in a migration or seed file, and signed-in users able to read decrypted values. Rotate any key ever stored in plain text or git.\n\nSearch the database and migrations for anything key-like, and research current practice. Before moving anything, show me what you found, values masked, your plan, and what you'd do differently.\n\nTry it on a preview first. Show me that searching the database for each key's value finds nothing, and that a test user can't read the secrets. List each secret's name and purpose, never its value, in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-SYNC-1": {
   "id": "p-SYNC-1",
   "title": "Mirror Stripe in your database",
   "stage": 5,
   "checks": [
    "SYNC-1"
   ],
   "primary": true,
   "body": "Keep a mirror of Stripe's customers, subscriptions, and payments in our database so the app can read them fast. Stripe stays the owner of money; the mirror is a copy its events keep fresh.\n\nCheck whether the app uses Stripe, and stop if not. A partial mirror or webhook may already exist. Verified, deduplicated webhooks that write to the mirror are a good default. Mirror only what the app needs. The classic failures are the app writing to the mirror, charging or refunding based on the mirror instead of asking Stripe, and one user seeing another's payments.\n\nResearch the current options. Before installing anything, show me which data you'd mirror, your plan, and what you'd do differently.\n\nTry it in Stripe test mode on a preview deployment. Show me a test payment reaching the mirror within a minute, and a test user seeing only their own. Record the mirror and how it stays fresh in our tech stack notes (probably a file like STACK.md or the README) and the data map, if we've written one."
  },
  "p-SYNC-2": {
   "id": "p-SYNC-2",
   "title": "Sync your CRM both ways",
   "stage": 5,
   "checks": [
    "SYNC-2"
   ],
   "primary": true,
   "body": "Connect the app and the CRM both ways: new users become contacts, matched to any existing lead, and the few lead fields the app needs flow back. The CRM owns leads and deals; the app owns user accounts.\n\nCheck which CRM the project uses; HubSpot is our usual default. If there's none, say so and stop. Most leads never become users, so bring back only what signed-up users need. The classic failures are duplicate contacts because nobody matched on email, and both sides writing the same field. Keep the CRM's token on the server.\n\nResearch the current best approach. Before building, show me each synced field with its one owner and one direction, plus what you'd do differently.\n\nTry it with a test lead and a test sign-up, never real contacts. Show me one matched contact, with its CRM ID saved wherever the app keeps outside IDs. Record the sync in our tech stack notes (probably a file like STACK.md or the README) and a data map, if we've written one."
  },
  "p-SYNC-4": {
   "id": "p-SYNC-4",
   "title": "Let the owner win",
   "stage": 5,
   "checks": [
    "SYNC-4"
   ],
   "primary": true,
   "body": "Write down, field by field, which system owns each mirrored value, and make every sync put the owner's value back when a copy drifts.\n\nFind every sync in the code, and a data map, if we've written one. The rule is almost always that the owner wins. Webhooks only fix records that change, so a scheduled reconcile that re-reads from the owner catches the rest. The classic failures are trusting whichever timestamp is newest, since clocks and event order lie, and the app editing a mirrored field instead of changing it at the owner.\n\nResearch current practice. Before changing code, show me each mirrored field, its owner and rule, any field two systems write today, and what you'd do differently. Wait for my OK. Overwrites should be logged.\n\nTry it on a preview with test data. Edit a mirrored value by hand, run the reconcile, and show me the owner's value restored and the change logged. Record the rules and schedule in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-SYNC-5": {
   "id": "p-SYNC-5",
   "title": "Know when a sync breaks",
   "stage": 5,
   "checks": [
    "SYNC-5"
   ],
   "primary": true,
   "body": "Make every sync and scheduled import record when it last succeeded, and alert someone when one falls behind, so a broken sync is noticed in hours, not weeks.\n\nFind every sync, webhook, and scheduled job in the code and our tech stack notes, probably a file like STACK.md or the README. The usual approach is a health record per sync (last success, last error, how stale is too stale) and a scheduled check that alerts by email or chat. The classic failure is treating \"no errors\" as healthy, because a sync that silently stops produces no errors. Alert on time since the last success.\n\nResearch current practice and reuse whatever the project has for scheduling and alerts. Show me each sync with a suggested staleness limit, where alerts would go, and what you'd do differently. Wait for my yes.\n\nTry it on a preview or in test mode. Pause one sync, show me the alert, then resume it and show it recover. Record each sync, its limit, and its alert channel in our tech stack notes."
  },
  "p-DATA-5": {
   "id": "p-DATA-5",
   "title": "Write down what your numbers mean",
   "stage": 6,
   "checks": [
    "DATA-5"
   ],
   "primary": true,
   "body": "Give each number this business watches one written definition, starting with \"active user,\" \"customer,\" and \"revenue,\" so two people asking the same question get the same number.\n\nThese may already be counted in dashboards, admin pages, analytics, or queries in the code. Find them and show me where they disagree. A good definition names what counts, the time window and time zone, the owner the data comes from (revenue usually from Stripe), and what's excluded, like test accounts, staff, and refunds. The classic failure is a definition that depends on who runs it, like \"recent\" or \"real users.\"\n\nDraft each definition in plain English with the query behind it, and tell me where you'd define things differently. I choose each one before you save.\n\nThen compute last month's numbers from two different places and show me both. Explain any gap and tighten the definition until they match. Record the definitions in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-ETL-1": {
   "id": "p-ETL-1",
   "title": "Extract from every owner",
   "stage": 6,
   "checks": [
    "ETL-1"
   ],
   "primary": true,
   "body": "Get regular copies of this app's database, payments, CRM, and product events into one warehouse, so business questions never hit production.\n\nFind which systems own our data. A data map, if we've written one, may say, perhaps in our tech stack notes (probably a file like STACK.md or the README); otherwise read the code and ask me. Every fact has one owner; the warehouse holds copies that must be rebuildable from them. PostHog's data warehouse is a good default, since it pulls from Postgres, Stripe, and HubSpot and already holds events. The classic mistakes are a full-access key, and copying everything, including password hashes, tokens, and secrets.\n\nResearch current best practice and check what's already connected. Before connecting anything, show me each source, the tables you'd copy and why, each connection's access, and the schedule. Use read-only, least-privilege access.\n\nThen show me every source with a recent successful sync, and one row count beside the owner's own. Record what you chose, and why, in our tech stack notes."
  },
  "p-ETL-2": {
   "id": "p-ETL-2",
   "title": "Transform into tables you trust",
   "stage": 6,
   "checks": [
    "ETL-2"
   ],
   "primary": true,
   "body": "Turn the raw copies in our warehouse into a few tables we can trust, such as one row per customer, so charts start from agreed numbers.\n\nCheck what's actually in the warehouse; extraction may or may not be done. Look for written metric definitions and an ID map linking one person across tools, if we have them, perhaps in our tech stack notes (probably a file like STACK.md or the README). Raw data hides duplicates, test accounts, refunds, and mismatched IDs. The classic failure is a trusted table that quietly disagrees with its owner or our definitions. Keep each table's logic in the repo.\n\nResearch current best practice and propose three to five trusted tables, each with a sentence on what a row means and which owners feed it. Before writing anything, show me the design, what you rejected, and any definition the data forces us to change.\n\nThen show me each table's totals beside the owner's own, explaining any gap. Record what you built, and why, in our tech stack notes."
  },
  "p-ETL-3": {
   "id": "p-ETL-3",
   "title": "Load on a schedule",
   "stage": 6,
   "checks": [
    "ETL-3"
   ],
   "primary": true,
   "body": "Make our trusted warehouse tables refresh on their own, and make it obvious how fresh each one is, so nobody decides on old numbers without knowing.\n\nCheck which trusted tables exist, what feeds them, and how often each source syncs; our tech stack notes (probably a file like STACK.md or the README) may say. A table can't be fresher than its slowest source, so refreshes belong after sources land. If the tables live in PostHog's warehouse, its scheduled refresh is a good default, and daily is usually plenty. The classic failure is a refresh that breaks silently while dashboards keep showing old numbers as current.\n\nResearch the current options and pick the simplest schedule that matches how often people look. Before changing anything, show me the plan, including what a person sees when a refresh fails.\n\nThen show me each trusted table's last refresh beside its sources' last syncs, and one refresh actually happening. Record the schedules, and where to check freshness, in our tech stack notes."
  },
  "p-ETL-4": {
   "id": "p-ETL-4",
   "title": "Test your data",
   "stage": 6,
   "checks": [
    "ETL-4"
   ],
   "primary": true,
   "body": "Add automatic checks on our warehouse data, so empty tables, missing values, duplicates, or stale numbers get caught before anyone trusts a wrong chart.\n\nLook at what the warehouse holds and which tables people rely on; there may be trusted tables, or only raw copies. If the project already sends alerts somewhere, such as for failed syncs, use that. A good check returns the wrong rows, so nothing returned means it passed. The classic failures are a check that errors and counts as a pass, and one loosened just to go quiet.\n\nResearch how teams our size test warehouse data and choose the simplest approach. Before setting anything up, show me the checks, thresholds, where alerts go, and what you rejected.\n\nThen safely break a source on purpose, such as pausing one sync, and show me a check failing and the alert arriving, then passing once restored. Record the checks and where alerts go in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-ETL-5": {
   "id": "p-ETL-5",
   "title": "Answer questions with dashboards",
   "stage": 6,
   "checks": [
    "ETL-5"
   ],
   "primary": true,
   "body": "Build a few dashboards that answer the questions this business asks every week, so nobody rebuilds the same numbers by hand.\n\nFind what we have: trusted warehouse tables, written metric definitions, and existing dashboards. Our tech stack notes (probably a file like STACK.md or the README) may point to them, or they may not exist yet. PostHog dashboards are a good default if our data lives there. The classic failures are charts on raw copies, a chart inventing its own meaning for \"active user\" or \"revenue\", and too many dashboards.\n\nResearch current best practice, then propose five to eight weekly questions and let me cut or add before you build. Each chart should name the trusted table and definition it uses. If a question needs a new definition, propose one and ask me. Prefer counts and IDs to names and emails.\n\nThen show me the dashboards, and check two charts against their tables directly. Record each dashboard, its link, and its questions in our tech stack notes."
  },
  "p-ETL-6": {
   "id": "p-ETL-6",
   "title": "Push segments back out",
   "stage": 6,
   "checks": [
    "ETL-6"
   ],
   "primary": true,
   "body": "Send groups of people defined in the warehouse, such as trial users quiet for a week, to our email lists and CRM automatically. This is often called reverse ETL.\n\nCheck which email tool and CRM we use, whether an ID map links each person across tools, and which segments matter; our tech stack notes (probably a file like STACK.md or the README) may help. If there's nothing to push to, tell me and stop. The usual shape is a scheduled job that adds new members and removes people who left. The classic failures are resubscribing someone who unsubscribed, since the email tool owns that choice, sending whole profiles when a flag would do, and retries that act twice.\n\nResearch current best practice, including whether our tools do this natively. Before building, show me the segments, destinations, plan, and risks.\n\nUse test contacts, never real customers. Show me a test person joining a segment and reaching the right list, then leaving and being removed. Record each segment and schedule in our tech stack notes."
  },
  "p-DATA-3": {
   "id": "p-DATA-3",
   "title": "List every copy and why it exists",
   "stage": 7,
   "checks": [
    "DATA-3"
   ],
   "primary": true,
   "body": "List every copy of our data, why we keep it, and how we'd rebuild it from its owner, so losing any copy is an annoyance, never a disaster.\n\nEvery fact has one owner; everything else is a copy, kept for a reason. Start from a data map, if we've written one, then search the code, database, and connected tools for anything holding data owned elsewhere, from mirrors and caches to backups and test data. The classic failure is a forgotten copy quietly treated as the truth.\n\nFor each copy, note its owner, reason, how it stays fresh, and how to rebuild it. Before saving, show me any copy with no rebuild path, no reason to exist, or kept fresh only by hand, and recommend what to do.\n\nThen point me to the code, command, or setting behind each rebuild step for a few copies, including a backup if one exists. Record the list in the data map, or our tech stack notes (probably a file like STACK.md or the README)."
  },
  "p-DATA-4": {
   "id": "p-DATA-4",
   "title": "Find the personal data",
   "stage": 7,
   "checks": [
    "DATA-4"
   ],
   "primary": true,
   "body": "Mark every place that holds personal data, like names, emails, and anything else that identifies a person, so a privacy request could be answered from one list.\n\nCheck the database, file storage, logs, and every outside tool that receives information about people. Include every copy, not just owners: mirrors, the warehouse, email lists, exports, and backups, since a deletion that misses a copy isn't a deletion. A data map, if we've written one, is a starting point. The classic misses are emails hiding in free-text fields, personal details sent to analytics, and logs that print emails.\n\nFor each place, note what it holds, how to find one person there, and how to export and delete their data. Flag anywhere that doesn't need it at all. Never paste real personal data into the notes as an example.\n\nShow me the list before saving. Then walk through a pretend export-and-delete request for one test user using only the list, and tell me anything it missed. Record it in the data map, or our tech stack notes (probably a file like STACK.md or the README)."
  },
  "p-SAFE-2": {
   "id": "p-SAFE-2",
   "title": "Hash the tokens you issue",
   "stage": 7,
   "checks": [
    "SAFE-2"
   ],
   "primary": true,
   "body": "If this app gives out API keys or similar tokens, store them only as hashes and show each to its owner once, so a leaked database never leaks a usable key.\n\nFirst check whether the app issues keys at all, and stop if it doesn't. The usual pattern: generate a long random key on the server, show it once, keep only its hash plus a short visible prefix so people can tell keys apart, and check incoming keys by hashing them. Long random keys make a fast hash safe; slow password-style hashing just slows every request. The classic failures are plain-text keys, keys in logs, and no way to revoke one.\n\nResearch current best practice. Before changing anything, show me the design and how existing keys would migrate. Make schema changes the way the project already does, on a preview.\n\nShow me a key working, its stored row holding nothing usable, and a revoked key refused. Record the design in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-SAFE-3": {
   "id": "p-SAFE-3",
   "title": "Encrypt sensitive fields",
   "stage": 7,
   "checks": [
    "SAFE-3"
   ],
   "primary": true,
   "body": "Encrypt this app's most sensitive values, like outside service tokens or ID numbers, so reading the table directly shows unreadable text and only the server can decrypt them.\n\nWork out which values are sensitive from the database, the code, and a list of personal data, if one exists. There may be none; if so, tell me and stop. Encrypt what we must read back; hash what only needs checking. Encrypting in the app with a key kept in Vercel's sensitive environment variables is a good default; Postgres's pgcrypto is another option. The classic failures are a decryption path signed-in users can reach, decrypted values in logs, and losing the key a restored database needs.\n\nResearch the current recommended approach for Neon. Before changing anything, show me the candidate fields, who needs each, your plan, and what you rejected. Try it on a preview with test data.\n\nShow me the column as unreadable text, the server reading it, and a signed-in test user refused. Record each encrypted field, and where the key lives, in our tech stack notes (probably a file like STACK.md or the README)."
  },
  "p-SAFE-4": {
   "id": "p-SAFE-4",
   "title": "Keep personal data out of copies",
   "stage": 7,
   "checks": [
    "SAFE-4"
   ],
   "primary": true,
   "body": "Keep names, emails, and other personal data out of our copies, like analytics, logs, error reports, and the warehouse, so they identify people only by an ID.\n\nStart from a list of where personal data lives, if we've written one, perhaps a data map in our tech stack notes (probably a file like STACK.md or the README). Then read the code for everything that sends data to a copy, including session replay and warehouse syncs. The classic failures are using an email as the analytics ID, logging whole request bodies, and free-text fields carrying personal data into events. Copies that truly need an email, like a mailing list, should be few and written down.\n\nResearch current best practice. Before changing anything, show me each risky field and what you'd do.\n\nOn a preview, use the main features as a test user with a unique email, then search analytics, logs, and the warehouse for it and show me nothing turns up. Record which copies still hold personal data, and why, in our tech stack notes."
  },
  "p-SAFE-5": {
   "id": "p-SAFE-5",
   "title": "Record who changed what",
   "stage": 7,
   "checks": [
    "SAFE-5"
   ],
   "primary": true,
   "body": "Record who changed important data and when, so we can always answer \"who did this?\" about money, permissions, and settings.\n\nWork out which tables matter most from the database, the code, and our tech stack notes (probably a file like STACK.md or the README), and check whether an audit trail already exists; if so, build on it. The usual approach is a record the database writes automatically on every change, so the app can't forget or fake it, holding who acted, when, and what changed. The classic failures are an audit log the app writes directly, so a bug or attacker can forge entries, one users can edit or delete, and one that copies secrets.\n\nResearch current best practice. Before changing anything, show me which tables you'd audit, why, and your design. Use a preview with test accounts.\n\nShow me a test user's edit recorded with their ID, the time, and before and after values, and that they can't delete that entry. Record the audited tables in our tech stack notes."
  },
  "p-SAFE-6": {
   "id": "p-SAFE-6",
   "title": "Handle export and deletion requests",
   "stage": 7,
   "checks": [
    "SAFE-6"
   ],
   "primary": true,
   "body": "Make it possible to export everything we hold about one person, then delete it from every owner and every copy. Privacy laws give people both rights.\n\nStart from a list of where personal data and its copies live, if we've written one, perhaps in a file like STACK.md or the README; otherwise build one. A deletion that misses a copy isn't a deletion, and the next sync brings it back. Run deletion as steps that can safely retry. The classic failures are deleting the account before its files, rows that don't delete with the user, and deleting what the law says we must keep, like invoices.\n\nResearch current requirements. Before building, show me the full list and your plan.\n\nBuild it on a preview, with outside tools in test mode where possible, and try it only on a test user with data everywhere. Ask me before it runs for a real person. Show me the export, then the test user gone everywhere, warehouse included. Write a short runbook in the project's docs and link it from our tech stack notes."
  },
  "p-OPS-1": {
   "id": "p-OPS-1",
   "title": "Back up and practice restoring",
   "stage": 8,
   "checks": [
    "OPS-1"
   ],
   "primary": true,
   "body": "Make sure this app's data could survive a bad day: daily backups exist, and we've actually restored one somewhere safe.\n\nFind our plan and backup settings, and what our tech stack notes (probably a file like STACK.md or the README) say, if anything. Neon can restore the database to any moment within a history window that depends on the plan; check the current details. Check separately whether uploaded files are covered, and encrypted secrets may need a separately kept key. The classic failures are assuming backups work, practicing on production, and backup copies kept less privately than the database.\n\nResearch current best practice, and decide with me whether we need our own copy outside Neon. Before changing anything, show me what's covered, what isn't, and your plan.\n\nRestore into a separate test project, never production. Show me yesterday's data there, row counts matching production, and a file that opens. Write the restore steps in the project's docs, and record the drill date in our tech stack notes."
  },
  "p-OPS-3": {
   "id": "p-OPS-3",
   "title": "Rehearse every schema change",
   "stage": 8,
   "checks": [
    "OPS-3"
   ],
   "primary": true,
   "body": "Make every database change run on a preview with test data before it reaches production, so a bad migration breaks a throwaway copy instead of the real app.\n\nCheck what's in place: whether schema changes live as migration files in git, whether Neon branching gives each preview its own database (and whether those branches copy production data), whether app previews use it, and whether there's realistic fake seed data. Keep whatever already works. The classic failures are editing production tables in a dashboard, editing a migration that already ran, and a preview quietly pointing at production data.\n\nResearch the current setup for our tools. Before changing anything, show me what exists, what's missing, and your plan. Stop and ask me before any change that destroys data.\n\nOpen a test pull request with a small migration, and show me it applied on its preview, with seed data and no real customers. Write the rule down for contributors, and record the flow in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-OPS-2": {
   "id": "p-OPS-2",
   "title": "Check health every week",
   "stage": 8,
   "checks": [
    "OPS-2"
   ],
   "primary": true,
   "body": "Run security and performance checks on the database every week, and fix what they find, so problems get caught while they're small.\n\nNeon's monitoring and query performance views are a good default, with a short checklist for what they don't cover: tables without access rules, unindexed foreign keys, and storage growth. Check whether the project already has a scheduled check or a health checklist, perhaps in the docs or our tech stack notes (probably a file like STACK.md or the README), and build on it. The classic failures are silencing a warning instead of fixing it, like loosening an access rule, and a weekly report nobody reads.\n\nRun them now and, before changing anything, show me each finding in plain words, how serious it is, and your proposed fix. If one should be left alone, tell me why. Fix things the way the project makes schema changes, on a preview first.\n\nShow me the advisors reporting no unlocked tables and no missing indexes, and the weekly check reaching someone who reads it. Record the check and its schedule in our tech stack notes."
  },
  "p-OPS-4": {
   "id": "p-OPS-4",
   "title": "Clean up old data on a schedule",
   "stage": 8,
   "checks": [
    "OPS-4"
   ],
   "primary": true,
   "body": "Delete old logs, events, and stale copies on a schedule I choose, so the database stays small and personal data isn't kept longer than it should be.\n\nFind the tables that grow forever, such as logs, received webhook events, import staging, and cached copies, using the database and a data map, if we've written one. There may be nothing worth cleaning yet; if so, tell me and stop. A Vercel Cron Job that calls a cleanup route is a good default. The classic failures are putting data the app owns, like users or orders, on a timer, deletes with no date limit, and removing audit records the law requires.\n\nResearch current best practice and propose a limit for each table with the reason; I choose. Show me the plan before deleting anything.\n\nOn a preview, add test rows just past the limit, and show me they're gone while newer rows remain, plus the job's run history. Record each limit in our tech stack notes, probably a file like STACK.md or the README."
  },
  "p-OPS-5": {
   "id": "p-OPS-5",
   "title": "Watch size and growth",
   "stage": 8,
   "checks": [
    "OPS-5"
   ],
   "primary": true,
   "body": "Find out how fast each table grows, and roughly when we'll hit our plan's database and storage limits, so nothing fills up by surprise.\n\nCheck our plan's limits in the provider's current docs; our tech stack notes (probably a file like STACK.md or the README) may say which plan we're on. Measure the database, each table with its indexes, and file storage. Estimate past growth from creation dates; a small daily record of sizes sharpens it over time. The classic surprises are a log table bigger than the core data, unused indexes, and files stored inside the database. Don't upgrade until the numbers say so.\n\nResearch current best practice. Measuring is read-only. Before adding a scheduled job or changing anything, show me the largest tables and your plan, and ask before fixing what you find.\n\nShow me a short report: current sizes, monthly growth, the date we'd hit each limit, and the biggest tables. Record it in our tech stack notes with a reminder to check monthly."
  }
 },
 "legend": {
  "req": "writes",
  "dep": "copies",
  "fb": "care"
 },
 "boxKey": true,
 "modes": {
  "label": "Show",
  "options": [
   {
    "id": "all",
    "label": "All"
   },
   {
    "id": "owners",
    "label": "Owners",
    "icon": "own"
   },
   {
    "id": "copies",
    "label": "Copies",
    "icon": "cpy",
    "tone": "copy"
   }
  ]
 }
}