[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-site-config":3,"$fIUEMBvNA0ug-3bmCLD98zOVfJd1jKQSS-NUY0NOP1xw":6,"mdc-96sqyh-key":54},{"gaMeasurementId":4,"bookingUrl":5},"G-7BYTDCVDDR","https:\u002F\u002Ftidycal.com\u002Fcmdcntr\u002F30-minute-meeting",{"post":7,"related":53},{"id":8,"slug":9,"title":10,"description":11,"content":12,"coverImageUrl":13,"coverImageAlt":13,"tags":14,"targetKeywords":20,"demo":21,"featured":25,"tool":13,"status":26,"reviewNote":13,"metaTitle":27,"metaDescription":28,"canonicalUrl":29,"sourceTicketId":13,"agentGenerated":25,"publishedAt":30,"publishedBy":31,"createdAt":30,"updatedAt":32,"authorId":33,"clusterId":34,"ctaId":35,"author":36,"cluster":43,"cta":47},"b846e15a-d4bc-43c4-93d2-1bb9cecc2cf6","vibe-code-to-production-migration-playbook","From Vibe Code to Production: A Migration Playbook for Apps That Outgrew Their Prompt","Your vibe-coded app works in the demo and falls over with real users. Here is the exact playbook we run to take an AI-built prototype to a codebase you can ship and maintain.","Vibe coding gets you to a working demo faster than anything I have used in fifteen years of building software. The trouble starts the week after the demo. The app that wowed three people in a screen share starts dropping data once twenty real users touch it, the login works for you and nobody else, and a feature change that should take an hour takes two days because no one, including the AI, can explain how the pieces fit together. That gap between \"works in the demo\" and \"survives production\" is where most vibe-coded projects stall. This playbook is the exact sequence we run when a founder hands us an AI-built app and asks us to make it real. It is not a rewrite-from-scratch argument. Most of these apps are worth saving. They just need to be migrated from a prompt artifact into a codebase.\n\n## Step 1: Triage before you touch anything\n\nThe first mistake teams make is opening the editor and fixing whatever annoys them first. Resist that. Spend a day reading instead.\n\nWhen we audit a vibe-coded app, we are answering three questions before we change a line:\n\n1. **Where does the data actually live, and is it ever validated?** Vibe-coded apps almost always trust the client. The database accepts whatever the frontend sends.\n2. **What is the real auth boundary?** Not the login screen, the boundary. Which endpoints would return your data to an unauthenticated request right now?\n3. **What breaks silently?** Look for the transformations and writes that fail without throwing. Those are the ones eating your data.\n\nWrite the answers down. This becomes your migration order, because you fix the things that lose data or leak data before you fix anything cosmetic.\n\n## Step 2: Pin the data model\n\nThe single biggest source of production failures in vibe-coded apps is a data model that was never designed, only accreted. The AI added a column here, a loosely typed JSON blob there, and a relationship that exists in the UI but not in the schema.\n\nReconstruct the model explicitly. Draw the tables, the foreign keys, and the constraints that should exist. Then compare that to what is actually in the database. The difference is your bug list.\n\nA pattern we see constantly: a field the app depends on is nullable in the schema, so half the rows have it and half do not, and the code that reads it works until it hits a null.\n\n```sql\n-- What the AI generated: everything optional, nothing enforced\nCREATE TABLE orders (\n  id text PRIMARY KEY,\n  user_id text,           -- no foreign key, no NOT NULL\n  total numeric,          -- accepts negative numbers, accepts null\n  status text             -- any string is \"valid\"\n);\n\n-- What production needs: the invariants written down and enforced\nCREATE TABLE orders (\n  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n  user_id uuid NOT NULL REFERENCES users(id),\n  total numeric NOT NULL CHECK (total >= 0),\n  status text NOT NULL CHECK (status IN ('pending','paid','shipped','cancelled'))\n);\n```\n\nAdding constraints will surface the bad data that is already in your tables. That is the point. You want those failures on your screen during migration, not in a customer's account next month.\n\n## Step 3: Rebuild the auth boundary on the server\n\nVibe-coded authentication is the topic I get the most frantic messages about, and it deserves its own treatment. The short version: the AI builds a login screen and conflates it with security. The screen hides the UI, but the API underneath still answers anyone who asks.\n\nThe fix is to stop trusting the client entirely and enforce ownership on every endpoint that returns or mutates user data. We go deep on the specific failure patterns and fixes in [Why AI-Generated Authentication Always Breaks](\u002Fblog\u002Ffix-ai-code\u002Fwhy-ai-generated-authentication-breaks), and that piece is worth reading before you ship anything with a login.\n\n## Step 4: Add tests where the money is\n\nYou are not going to retrofit full coverage onto a vibe-coded app, and you should not try. Put tests on the paths where a bug costs you a customer or a dollar: payments, auth, and any write that touches the data model you just pinned.\n\nThe goal is a safety net that lets you refactor the rest with confidence. Once payment and auth flows are locked behind tests, you can clean up the messy interior of the app without holding your breath every deploy.\n\n## Step 5: Decide what to keep, refactor, or replace\n\nNow you make the call, module by module, with evidence instead of vibes:\n\n- **Keep** the parts that are simple, correct, and isolated. A lot of CRUD and UI falls here. Do not rewrite working code to satisfy your sense of tidiness.\n- **Refactor** the parts that work but are tangled. Extract the logic, add types, give it a test, move on.\n- **Replace** the parts that are both critical and untrustworthy. Auth, payments, and anything the audit flagged as a silent data risk usually land here.\n\nThe mistake on both ends is treating the whole app as one decision. It is not all garbage and it is not all fine. Migrate it in pieces, in the order your triage gave you.\n\n## Key takeaways\n\n- Read before you fix. A one-day audit of data, auth, and silent failures gives you the migration order.\n- The data model is the root cause of most production failures. Make the invariants explicit and enforce them in the database.\n- Authentication has to be enforced on the server, per endpoint. A login screen is not a security boundary.\n- Test the paths where bugs cost money, then refactor everything else behind that net.\n- Migrate module by module. Keep what works, refactor what is tangled, replace only what is both critical and untrustworthy.\n\nVibe coding is a genuinely good way to find out what to build. It is a poor way to find out how to run it at scale. The migration is the work, and it is very doable when you sequence it right. If your app is past the demo and starting to wobble under real users, that is exactly the moment this playbook is for.",null,[15,16,17,18,19],"vibe coding","production","technical debt","migration","architecture",[],{"dir":22,"repoUrl":23,"bugBranch":24},"demos\u002Fvibe-to-production","https:\u002F\u002Fgithub.com\u002Fcommandcenterio\u002Fcmdcntr-demos","bug\u002Fvibe-to-production",false,"published","Vibe Code to Production: The Migration Playbook (2026)","A step-by-step playbook for taking a vibe-coded app to production: triage, data model, auth, tests, and the rewrite decisions that actually matter.","https:\u002F\u002Fcmdcntr.io\u002Fblog\u002Fvibe-coding\u002Fvibe-code-to-production-migration-playbook","2026-07-04T15:08:41.522Z","7452d3cc-b0aa-46e4-96e6-da9c0225c471","2026-07-04T16:23:59.734Z","ed80da88-f3d1-4aa9-9373-18c39fecb740","a36027e0-91d5-45a8-9098-481ef34f3534","c0fcf6a1-58d2-4262-8996-3ad29d58eea5",{"slug":37,"name":38,"avatarUrl":39,"role":40,"bio":41,"isPublic":42},"michael-graham","Michael Graham","\u002Fapi\u002Fpublic\u002Favatar\u002F7452d3cc-b0aa-46e4-96e6-da9c0225c471","Founder & Software Engineer","Obsessed with building top-tier web software and crafting unique, polished user experiences.",true,{"slug":44,"name":45,"color":46},"vibe-coding","Vibe Coding","#8B5CF6",{"id":35,"name":48,"ctaType":49,"heading":50,"description":51,"buttonText":52,"url":5,"leadMagnetUrl":13},"Book a Call (blog)","book_call","Shipping AI-generated code that keeps breaking?","Book a free 30-minute call with a senior engineer. We diagnose what is going wrong and give you a concrete fix plan, no obligation.","Book a free call",[],{"data":55,"body":56},{},{"type":57,"children":58},"root",[59,67,74,79,84,120,125,131,136,141,146,293,298,304,309,323,329,334,339,345,350,384,389,395,423,428],{"type":60,"tag":61,"props":62,"children":63},"element","p",{},[64],{"type":65,"value":66},"text","Vibe coding gets you to a working demo faster than anything I have used in fifteen years of building software. The trouble starts the week after the demo. The app that wowed three people in a screen share starts dropping data once twenty real users touch it, the login works for you and nobody else, and a feature change that should take an hour takes two days because no one, including the AI, can explain how the pieces fit together. That gap between \"works in the demo\" and \"survives production\" is where most vibe-coded projects stall. This playbook is the exact sequence we run when a founder hands us an AI-built app and asks us to make it real. It is not a rewrite-from-scratch argument. Most of these apps are worth saving. They just need to be migrated from a prompt artifact into a codebase.",{"type":60,"tag":68,"props":69,"children":71},"h2",{"id":70},"step-1-triage-before-you-touch-anything",[72],{"type":65,"value":73},"Step 1: Triage before you touch anything",{"type":60,"tag":61,"props":75,"children":76},{},[77],{"type":65,"value":78},"The first mistake teams make is opening the editor and fixing whatever annoys them first. Resist that. Spend a day reading instead.",{"type":60,"tag":61,"props":80,"children":81},{},[82],{"type":65,"value":83},"When we audit a vibe-coded app, we are answering three questions before we change a line:",{"type":60,"tag":85,"props":86,"children":87},"ol",{},[88,100,110],{"type":60,"tag":89,"props":90,"children":91},"li",{},[92,98],{"type":60,"tag":93,"props":94,"children":95},"strong",{},[96],{"type":65,"value":97},"Where does the data actually live, and is it ever validated?",{"type":65,"value":99}," Vibe-coded apps almost always trust the client. The database accepts whatever the frontend sends.",{"type":60,"tag":89,"props":101,"children":102},{},[103,108],{"type":60,"tag":93,"props":104,"children":105},{},[106],{"type":65,"value":107},"What is the real auth boundary?",{"type":65,"value":109}," Not the login screen, the boundary. Which endpoints would return your data to an unauthenticated request right now?",{"type":60,"tag":89,"props":111,"children":112},{},[113,118],{"type":60,"tag":93,"props":114,"children":115},{},[116],{"type":65,"value":117},"What breaks silently?",{"type":65,"value":119}," Look for the transformations and writes that fail without throwing. Those are the ones eating your data.",{"type":60,"tag":61,"props":121,"children":122},{},[123],{"type":65,"value":124},"Write the answers down. This becomes your migration order, because you fix the things that lose data or leak data before you fix anything cosmetic.",{"type":60,"tag":68,"props":126,"children":128},{"id":127},"step-2-pin-the-data-model",[129],{"type":65,"value":130},"Step 2: Pin the data model",{"type":60,"tag":61,"props":132,"children":133},{},[134],{"type":65,"value":135},"The single biggest source of production failures in vibe-coded apps is a data model that was never designed, only accreted. The AI added a column here, a loosely typed JSON blob there, and a relationship that exists in the UI but not in the schema.",{"type":60,"tag":61,"props":137,"children":138},{},[139],{"type":65,"value":140},"Reconstruct the model explicitly. Draw the tables, the foreign keys, and the constraints that should exist. Then compare that to what is actually in the database. The difference is your bug list.",{"type":60,"tag":61,"props":142,"children":143},{},[144],{"type":65,"value":145},"A pattern we see constantly: a field the app depends on is nullable in the schema, so half the rows have it and half do not, and the code that reads it works until it hits a null.",{"type":60,"tag":147,"props":148,"children":153},"pre",{"className":149,"code":150,"language":151,"meta":152,"style":152},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","-- What the AI generated: everything optional, nothing enforced\nCREATE TABLE orders (\n  id text PRIMARY KEY,\n  user_id text,           -- no foreign key, no NOT NULL\n  total numeric,          -- accepts negative numbers, accepts null\n  status text             -- any string is \"valid\"\n);\n\n-- What production needs: the invariants written down and enforced\nCREATE TABLE orders (\n  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n  user_id uuid NOT NULL REFERENCES users(id),\n  total numeric NOT NULL CHECK (total >= 0),\n  status text NOT NULL CHECK (status IN ('pending','paid','shipped','cancelled'))\n);\n","sql","",[154],{"type":60,"tag":155,"props":156,"children":157},"code",{"__ignoreMap":152},[158,169,178,187,196,205,214,223,232,241,249,258,267,276,285],{"type":60,"tag":159,"props":160,"children":163},"span",{"class":161,"line":162},"line",1,[164],{"type":60,"tag":159,"props":165,"children":166},{},[167],{"type":65,"value":168},"-- What the AI generated: everything optional, nothing enforced\n",{"type":60,"tag":159,"props":170,"children":172},{"class":161,"line":171},2,[173],{"type":60,"tag":159,"props":174,"children":175},{},[176],{"type":65,"value":177},"CREATE TABLE orders (\n",{"type":60,"tag":159,"props":179,"children":181},{"class":161,"line":180},3,[182],{"type":60,"tag":159,"props":183,"children":184},{},[185],{"type":65,"value":186},"  id text PRIMARY KEY,\n",{"type":60,"tag":159,"props":188,"children":190},{"class":161,"line":189},4,[191],{"type":60,"tag":159,"props":192,"children":193},{},[194],{"type":65,"value":195},"  user_id text,           -- no foreign key, no NOT NULL\n",{"type":60,"tag":159,"props":197,"children":199},{"class":161,"line":198},5,[200],{"type":60,"tag":159,"props":201,"children":202},{},[203],{"type":65,"value":204},"  total numeric,          -- accepts negative numbers, accepts null\n",{"type":60,"tag":159,"props":206,"children":208},{"class":161,"line":207},6,[209],{"type":60,"tag":159,"props":210,"children":211},{},[212],{"type":65,"value":213},"  status text             -- any string is \"valid\"\n",{"type":60,"tag":159,"props":215,"children":217},{"class":161,"line":216},7,[218],{"type":60,"tag":159,"props":219,"children":220},{},[221],{"type":65,"value":222},");\n",{"type":60,"tag":159,"props":224,"children":226},{"class":161,"line":225},8,[227],{"type":60,"tag":159,"props":228,"children":229},{"emptyLinePlaceholder":42},[230],{"type":65,"value":231},"\n",{"type":60,"tag":159,"props":233,"children":235},{"class":161,"line":234},9,[236],{"type":60,"tag":159,"props":237,"children":238},{},[239],{"type":65,"value":240},"-- What production needs: the invariants written down and enforced\n",{"type":60,"tag":159,"props":242,"children":244},{"class":161,"line":243},10,[245],{"type":60,"tag":159,"props":246,"children":247},{},[248],{"type":65,"value":177},{"type":60,"tag":159,"props":250,"children":252},{"class":161,"line":251},11,[253],{"type":60,"tag":159,"props":254,"children":255},{},[256],{"type":65,"value":257},"  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n",{"type":60,"tag":159,"props":259,"children":261},{"class":161,"line":260},12,[262],{"type":60,"tag":159,"props":263,"children":264},{},[265],{"type":65,"value":266},"  user_id uuid NOT NULL REFERENCES users(id),\n",{"type":60,"tag":159,"props":268,"children":270},{"class":161,"line":269},13,[271],{"type":60,"tag":159,"props":272,"children":273},{},[274],{"type":65,"value":275},"  total numeric NOT NULL CHECK (total >= 0),\n",{"type":60,"tag":159,"props":277,"children":279},{"class":161,"line":278},14,[280],{"type":60,"tag":159,"props":281,"children":282},{},[283],{"type":65,"value":284},"  status text NOT NULL CHECK (status IN ('pending','paid','shipped','cancelled'))\n",{"type":60,"tag":159,"props":286,"children":288},{"class":161,"line":287},15,[289],{"type":60,"tag":159,"props":290,"children":291},{},[292],{"type":65,"value":222},{"type":60,"tag":61,"props":294,"children":295},{},[296],{"type":65,"value":297},"Adding constraints will surface the bad data that is already in your tables. That is the point. You want those failures on your screen during migration, not in a customer's account next month.",{"type":60,"tag":68,"props":299,"children":301},{"id":300},"step-3-rebuild-the-auth-boundary-on-the-server",[302],{"type":65,"value":303},"Step 3: Rebuild the auth boundary on the server",{"type":60,"tag":61,"props":305,"children":306},{},[307],{"type":65,"value":308},"Vibe-coded authentication is the topic I get the most frantic messages about, and it deserves its own treatment. The short version: the AI builds a login screen and conflates it with security. The screen hides the UI, but the API underneath still answers anyone who asks.",{"type":60,"tag":61,"props":310,"children":311},{},[312,314,321],{"type":65,"value":313},"The fix is to stop trusting the client entirely and enforce ownership on every endpoint that returns or mutates user data. We go deep on the specific failure patterns and fixes in ",{"type":60,"tag":315,"props":316,"children":318},"a",{"href":317},"\u002Fblog\u002Ffix-ai-code\u002Fwhy-ai-generated-authentication-breaks",[319],{"type":65,"value":320},"Why AI-Generated Authentication Always Breaks",{"type":65,"value":322},", and that piece is worth reading before you ship anything with a login.",{"type":60,"tag":68,"props":324,"children":326},{"id":325},"step-4-add-tests-where-the-money-is",[327],{"type":65,"value":328},"Step 4: Add tests where the money is",{"type":60,"tag":61,"props":330,"children":331},{},[332],{"type":65,"value":333},"You are not going to retrofit full coverage onto a vibe-coded app, and you should not try. Put tests on the paths where a bug costs you a customer or a dollar: payments, auth, and any write that touches the data model you just pinned.",{"type":60,"tag":61,"props":335,"children":336},{},[337],{"type":65,"value":338},"The goal is a safety net that lets you refactor the rest with confidence. Once payment and auth flows are locked behind tests, you can clean up the messy interior of the app without holding your breath every deploy.",{"type":60,"tag":68,"props":340,"children":342},{"id":341},"step-5-decide-what-to-keep-refactor-or-replace",[343],{"type":65,"value":344},"Step 5: Decide what to keep, refactor, or replace",{"type":60,"tag":61,"props":346,"children":347},{},[348],{"type":65,"value":349},"Now you make the call, module by module, with evidence instead of vibes:",{"type":60,"tag":351,"props":352,"children":353},"ul",{},[354,364,374],{"type":60,"tag":89,"props":355,"children":356},{},[357,362],{"type":60,"tag":93,"props":358,"children":359},{},[360],{"type":65,"value":361},"Keep",{"type":65,"value":363}," the parts that are simple, correct, and isolated. A lot of CRUD and UI falls here. Do not rewrite working code to satisfy your sense of tidiness.",{"type":60,"tag":89,"props":365,"children":366},{},[367,372],{"type":60,"tag":93,"props":368,"children":369},{},[370],{"type":65,"value":371},"Refactor",{"type":65,"value":373}," the parts that work but are tangled. Extract the logic, add types, give it a test, move on.",{"type":60,"tag":89,"props":375,"children":376},{},[377,382],{"type":60,"tag":93,"props":378,"children":379},{},[380],{"type":65,"value":381},"Replace",{"type":65,"value":383}," the parts that are both critical and untrustworthy. Auth, payments, and anything the audit flagged as a silent data risk usually land here.",{"type":60,"tag":61,"props":385,"children":386},{},[387],{"type":65,"value":388},"The mistake on both ends is treating the whole app as one decision. It is not all garbage and it is not all fine. Migrate it in pieces, in the order your triage gave you.",{"type":60,"tag":68,"props":390,"children":392},{"id":391},"key-takeaways",[393],{"type":65,"value":394},"Key takeaways",{"type":60,"tag":351,"props":396,"children":397},{},[398,403,408,413,418],{"type":60,"tag":89,"props":399,"children":400},{},[401],{"type":65,"value":402},"Read before you fix. A one-day audit of data, auth, and silent failures gives you the migration order.",{"type":60,"tag":89,"props":404,"children":405},{},[406],{"type":65,"value":407},"The data model is the root cause of most production failures. Make the invariants explicit and enforce them in the database.",{"type":60,"tag":89,"props":409,"children":410},{},[411],{"type":65,"value":412},"Authentication has to be enforced on the server, per endpoint. A login screen is not a security boundary.",{"type":60,"tag":89,"props":414,"children":415},{},[416],{"type":65,"value":417},"Test the paths where bugs cost money, then refactor everything else behind that net.",{"type":60,"tag":89,"props":419,"children":420},{},[421],{"type":65,"value":422},"Migrate module by module. Keep what works, refactor what is tangled, replace only what is both critical and untrustworthy.",{"type":60,"tag":61,"props":424,"children":425},{},[426],{"type":65,"value":427},"Vibe coding is a genuinely good way to find out what to build. It is a poor way to find out how to run it at scale. The migration is the work, and it is very doable when you sequence it right. If your app is past the demo and starting to wobble under real users, that is exactly the moment this playbook is for.",{"type":60,"tag":429,"props":430,"children":431},"style",{},[432],{"type":65,"value":433},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}"]