[{"data":1,"prerenderedAt":606},["ShallowReactive",2],{"i-base:v-logo":3,"i-lucide:user":8,"i-lucide:folder-kanban":11,"i-lucide:heart":13,"i-lucide:file-text":15,"i-lucide:sun":17,"i-lucide:github":19,"i-lucide:twitter":22,"i-lucide:linkedin":24,"i-simple-icons:reddit":26,"i-lucide:globe":28,"article-cutting-a-supabase-bill-to-zero-with-cloudflare-d1":30,"articles-all":551,"i-lucide:arrow-left":604},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,512,false,"\u003Cg fill=\"currentColor\">\u003Cg transform=\"translate(0,512) scale(0.1,-0.1)\">\n    \u003Cpath d=\"M2866 3193 c-203 -209 -341 -393 -541 -718 -57 -93 -198 -309 -212 -325 -9 -9 -22 26 -28 75 -4 27 -8 61 -10 75 -2 14 -7 70 -10 125 -21 363 -72 541 -187 656 -30 30 -54 64 -56 78 -7 55 -65 113 -130 130 -21 6 -34 1 -62 -23 -64 -57 -86 -84 -97 -119 -12 -41 -9 -201 7 -349 6 -57 12 -204 12 -327 0 -191 2 -232 18 -270 10 -27 20 -89 24 -148 5 -92 21 -182 84 -478 12 -56 30 -105 46 -129 14 -21 27 -48 27 -60 1 -12 3 -39 4 -61 1 -23 8 -41 16 -44 8 -2 39 -31 69 -63 66 -72 98 -90 159 -91 133 -1 245 118 254 271 4 67 7 74 45 114 22 24 50 55 61 70 33 42 81 138 81 162 0 12 21 44 50 74 27 28 63 80 80 114 57 114 131 230 244 378 48 64 213 266 249 305 78 85 167 207 167 230 0 14 11 47 23 73 34 67 29 133 -20 252 l-38 95 -50 6 c-28 3 -67 13 -87 22 -21 10 -47 17 -58 17 -13 0 -62 -43 -134 -117z\"\u002F>\n    \u003Cpath d=\"M3070 1649 c-30 -44 -76 -96 -102 -115 -27 -20 -48 -40 -48 -45 0 -5 8 -21 17 -36 17 -25 17 -27 -5 -41 l-22 -14 30 -35 c17 -18 30 -39 30 -47 0 -36 34 -118 58 -140 102 -92 120 -104 159 -100 108 10 153 24 153 49 0 7 27 28 59 45 62 33 92 74 90 121 -1 13 10 57 25 97 32 87 33 90 17 165 -15 70 -24 75 -177 111 -159 38 -194 48 -200 57 -12 21 -32 4 -84 -72z\"\u002F>\n  \u003C\u002Fg>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":10},24,"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\"\u002F>\u003Ccircle cx=\"12\" cy=\"7\" r=\"4\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":12},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2m4-10v4m4-4v2m4-2v6\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":14},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M2 9.5a5.5 5.5 0 0 1 9.591-3.676a.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":16},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z\"\u002F>\u003Cpath d=\"M14 2v5a1 1 0 0 0 1 1h5M10 9H8m8 4H8m8 4H8\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":18},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"4\"\u002F>\u003Cpath d=\"M12 2v2m0 16v2M4.93 4.93l1.41 1.41m11.32 11.32l1.41 1.41M2 12h2m16 0h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":20,"hidden":21},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5c.08-1.25-.27-2.48-1-3.5c.28-1.15.28-2.35 0-3.5c0 0-1 0-3 1.5c-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.4 5.4 0 0 0 4 9c0 3.5 3 5.5 6 5.5c-.39.49-.68 1.05-.85 1.65S8.93 17.38 9 18v4\"\u002F>\u003Cpath d=\"M9 18c-4.51 2-5-2-7-2\"\u002F>\u003C\u002Fg>",true,{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":23,"hidden":21},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6c2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4c-.9-4.2 4-6.6 7-3.8c1.1 0 3-1.2 3-1.2\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":25,"hidden":21},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Cpath d=\"M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2a2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6M2 9h4v12H2z\"\u002F>\u003Ccircle cx=\"4\" cy=\"4\" r=\"2\"\u002F>\u003C\u002Fg>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":27},"\u003Cpath fill=\"currentColor\" d=\"M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286A.72.72 0 0 0 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0m4.388 3.199a1.999 1.999 0 1 1-1.947 2.46v.002a2.37 2.37 0 0 0-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363a2.802 2.802 0 1 1 2.908 4.753c-.088 3.256-3.637 5.876-7.997 5.876c-4.361 0-7.905-2.617-7.998-5.87a2.8 2.8 0 0 1 1.189-5.34c.645 0 1.239.218 1.712.585c1.275-.79 2.881-1.291 4.64-1.365v-.01a3.23 3.23 0 0 1 2.88-3.207a2 2 0 0 1 1.959-1.595m-8.085 8.376c-.784 0-1.459.78-1.506 1.797s.64 1.429 1.426 1.429s1.371-.369 1.418-1.385s-.553-1.841-1.338-1.841m7.406 0c-.786 0-1.385.824-1.338 1.841s.634 1.385 1.418 1.385c.785 0 1.473-.413 1.426-1.429c-.046-1.017-.721-1.797-1.506-1.797m-3.703 4.013c-.974 0-1.907.048-2.77.135a.222.222 0 0 0-.183.305a3.2 3.2 0 0 0 2.953 1.964a3.2 3.2 0 0 0 2.953-1.964a.222.222 0 0 0-.184-.305a28 28 0 0 0-2.769-.135\"\u002F>",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":29},"\u003Cg fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\u003Ccircle cx=\"12\" cy=\"12\" r=\"10\"\u002F>\u003Cpath d=\"M12 2a14.5 14.5 0 0 0 0 20a14.5 14.5 0 0 0 0-20M2 12h20\"\u002F>\u003C\u002Fg>",{"id":31,"title":32,"body":33,"category":533,"date":534,"description":535,"extension":536,"image":537,"meta":538,"navigation":21,"path":539,"seo":540,"status":541,"stem":542,"tags":543,"updatedAt":537,"__hash__":550},"articles\u002Farticles\u002F7.cutting-a-supabase-bill-to-zero-with-cloudflare-d1.md","How I cut a $35 Supabase bill to $0 with Cloudflare D1",{"type":34,"value":35,"toc":526},"minimark",[36,40,43,48,51,54,57,61,69,91,104,396,407,411,414,462,465,499,503,506,509,512,516,519,522],[37,38,39],"p",{},"Earlier this year I moved a production app off Supabase and onto Cloudflare D1. Multi-tenant SaaS, 18 organizations, about 70 active sessions, auth and billing and usage metering all live. The Supabase Pro bill was $35 a month. The D1 bill is effectively zero.",[37,41,42],{},"Zero downtime, no data loss, and honestly, the schema rewrite everyone worries about was the easy part. What actually cost me time were three things nobody mentions: a date bug that disguises itself as an auth failure, raw SQL that typechecks and then dies at runtime, and a geospatial assumption I got wrong.",[44,45,47],"h2",{"id":46},"first-the-boring-truth-about-the-money","First, the boring truth about the money",[37,49,50],{},"D1 on the Workers Paid plan is $5 a month, which includes 50 million row writes. After that it's a dollar per million. Against a $35 Supabase bill, the break-even lands around 80 million writes a month. If you're above that, this migration saves you nothing.",[37,52,53],{},"I wasn't above it. I wasn't anywhere near it. My busiest writer was a usage-metering table that a Durable Object upserts into, and when I actually read the write volume off the dashboard it came to a few dozen row writes a month. Not million. Dozen.",[37,55,56],{},"That's the real lesson of the cost section: \"D1 is cheaper\" is not a fact, it's a break-even, and you only know which side you're on after you read your own dashboard. I nearly didn't check because the answer felt obvious. It was obvious, but in an embarrassing direction.",[44,58,60],{"id":59},"the-bug-that-looks-like-broken-auth","The bug that looks like broken auth",[37,62,63,64,68],{},"The first thing that broke after the driver swap was signup. Better-Auth started throwing ",[65,66,67],"code",{},"FAILED_TO_CREATE_USER"," on every new account. Nothing in that error says \"date\", so I spent a while staring at the auth config.",[37,70,71,72,75,76,79,80,83,84,86,87,90],{},"The actual problem: ",[65,73,74],{},"drizzle-orm\u002Fd1"," passes whatever you give it straight through to D1's ",[65,77,78],{},".bind()",", and D1 only accepts strings, numbers, null, and ArrayBuffers. JavaScript ",[65,81,82],{},"Date"," objects get rejected. Better-Auth hands the ORM real ",[65,85,82],{},"s for ",[65,88,89],{},"createdAt",", D1 refuses the bind, and the failure surfaces three layers up wearing an auth costume.",[37,92,93,94,96,97,99,100,103],{},"The fix is a Proxy around the D1 driver that intercepts every ",[65,95,78],{}," and coerces on the way in: ",[65,98,82],{}," becomes an ISO string, ",[65,101,102],{},"boolean"," becomes 0 or 1.",[105,106,111],"pre",{"className":107,"code":108,"language":109,"meta":110,"style":110},"language-ts shiki shiki-themes github-dark github-dark github-light","function wrapD1ForDates(db: D1Database): D1Database {\n  return new Proxy(db, {\n    get(target, prop) {\n      if (prop !== 'prepare') return Reflect.get(target, prop);\n      return (query: string) => {\n        const stmt = target.prepare(query);\n        return new Proxy(stmt, {\n          get(s, p) {\n            if (p !== 'bind') return Reflect.get(s, p);\n            return (...values: unknown[]) => s.bind(...values.map(coerceForD1));\n          },\n        });\n      };\n    },\n  });\n}\n","ts","",[65,112,113,150,165,185,216,241,262,275,292,317,360,366,372,378,384,390],{"__ignoreMap":110},[114,115,118,122,126,130,134,137,140,143,145,147],"span",{"class":116,"line":117},"line",1,[114,119,121],{"class":120},"s6ubI","function",[114,123,125],{"class":124},"s5n6i"," wrapD1ForDates",[114,127,129],{"class":128},"sAvwS","(",[114,131,133],{"class":132},"sAzo5","db",[114,135,136],{"class":120},":",[114,138,139],{"class":124}," D1Database",[114,141,142],{"class":128},")",[114,144,136],{"class":120},[114,146,139],{"class":124},[114,148,149],{"class":128}," {\n",[114,151,153,156,159,162],{"class":116,"line":152},2,[114,154,155],{"class":120},"  return",[114,157,158],{"class":120}," new",[114,160,161],{"class":124}," Proxy",[114,163,164],{"class":128},"(db, {\n",[114,166,168,171,173,176,179,182],{"class":116,"line":167},3,[114,169,170],{"class":124},"    get",[114,172,129],{"class":128},[114,174,175],{"class":132},"target",[114,177,178],{"class":128},", ",[114,180,181],{"class":132},"prop",[114,183,184],{"class":128},") {\n",[114,186,188,191,194,197,201,204,207,210,213],{"class":116,"line":187},4,[114,189,190],{"class":120},"      if",[114,192,193],{"class":128}," (prop ",[114,195,196],{"class":120},"!==",[114,198,200],{"class":199},"s4Y1p"," 'prepare'",[114,202,203],{"class":128},") ",[114,205,206],{"class":120},"return",[114,208,209],{"class":128}," Reflect.",[114,211,212],{"class":124},"get",[114,214,215],{"class":128},"(target, prop);\n",[114,217,219,222,225,228,230,234,236,239],{"class":116,"line":218},5,[114,220,221],{"class":120},"      return",[114,223,224],{"class":128}," (",[114,226,227],{"class":132},"query",[114,229,136],{"class":120},[114,231,233],{"class":232},"sXWYR"," string",[114,235,203],{"class":128},[114,237,238],{"class":120},"=>",[114,240,149],{"class":128},[114,242,244,247,250,253,256,259],{"class":116,"line":243},6,[114,245,246],{"class":120},"        const",[114,248,249],{"class":232}," stmt",[114,251,252],{"class":120}," =",[114,254,255],{"class":128}," target.",[114,257,258],{"class":124},"prepare",[114,260,261],{"class":128},"(query);\n",[114,263,265,268,270,272],{"class":116,"line":264},7,[114,266,267],{"class":120},"        return",[114,269,158],{"class":120},[114,271,161],{"class":124},[114,273,274],{"class":128},"(stmt, {\n",[114,276,278,281,283,286,288,290],{"class":116,"line":277},8,[114,279,280],{"class":124},"          get",[114,282,129],{"class":128},[114,284,285],{"class":132},"s",[114,287,178],{"class":128},[114,289,37],{"class":132},[114,291,184],{"class":128},[114,293,295,298,301,303,306,308,310,312,314],{"class":116,"line":294},9,[114,296,297],{"class":120},"            if",[114,299,300],{"class":128}," (p ",[114,302,196],{"class":120},[114,304,305],{"class":199}," 'bind'",[114,307,203],{"class":128},[114,309,206],{"class":120},[114,311,209],{"class":128},[114,313,212],{"class":124},[114,315,316],{"class":128},"(s, p);\n",[114,318,320,323,325,328,331,333,336,339,341,344,347,349,351,354,357],{"class":116,"line":319},10,[114,321,322],{"class":120},"            return",[114,324,224],{"class":128},[114,326,327],{"class":120},"...",[114,329,330],{"class":132},"values",[114,332,136],{"class":120},[114,334,335],{"class":232}," unknown",[114,337,338],{"class":128},"[]) ",[114,340,238],{"class":120},[114,342,343],{"class":128}," s.",[114,345,346],{"class":124},"bind",[114,348,129],{"class":128},[114,350,327],{"class":120},[114,352,353],{"class":128},"values.",[114,355,356],{"class":124},"map",[114,358,359],{"class":128},"(coerceForD1));\n",[114,361,363],{"class":116,"line":362},11,[114,364,365],{"class":128},"          },\n",[114,367,369],{"class":116,"line":368},12,[114,370,371],{"class":128},"        });\n",[114,373,375],{"class":116,"line":374},13,[114,376,377],{"class":128},"      };\n",[114,379,381],{"class":116,"line":380},14,[114,382,383],{"class":128},"    },\n",[114,385,387],{"class":116,"line":386},15,[114,388,389],{"class":128},"  });\n",[114,391,393],{"class":116,"line":392},16,[114,394,395],{"class":128},"}\n",[37,397,398,399,402,403,406],{},"I'd call this optional if signup merely logged a warning. It doesn't. Without the proxy, user creation is just broken, and the error message sends you to the wrong layer. And if you run local dev on ",[65,400,401],{},"better-sqlite3"," against a file database, congratulations, you get to patch the same coercion there too, at the ",[65,404,405],{},"Statement.prototype"," level, because it's the same class of bug in a different driver.",[44,408,410],{"id":409},"the-rewrite-is-total-and-thats-fine","The rewrite is total, and that's fine",[37,412,413],{},"You cannot half-migrate a Drizzle schema. The moment the driver changes, everything that imports the schema breaks, so all ~30 tables moved in one commit. The substitutions are mechanical once you know them:",[415,416,417,427,434,443,455],"ul",{},[418,419,420,423,424],"li",{},[65,421,422],{},"pgTable"," becomes ",[65,425,426],{},"sqliteTable",[418,428,429,430,433],{},"every ",[65,431,432],{},"pgEnum"," becomes a frozen TS tuple with a Zod enum derived from it, because SQLite has no enum type",[418,435,436,439,440],{},[65,437,438],{},"gen_ulid()"," column defaults become app-side ",[65,441,442],{},"$defaultFn(() => generateULID())",[418,444,445,448,449,423,452],{},[65,446,447],{},"text[]"," becomes JSON in a text column, ",[65,450,451],{},"jsonb",[65,453,454],{},"text({ mode: 'json' })",[418,456,457,458,461],{},"booleans become ",[65,459,460],{},"int({ mode: 'bool' })",", timestamps become ISO-8601 strings",[37,463,464],{},"Tedious, yes. Risky, not really. The compiler walks you through it.",[37,466,467,468,471,472,475,476,479,480,483,484,423,487,490,491,494,495,498],{},"The dangerous part is the raw SQL, because it typechecks. ",[65,469,470],{},"vue-tsc"," has no opinion about whether your SQL dialect is right. Every ",[65,473,474],{},"ilike"," needs to become ",[65,477,478],{},"like",". Every ",[65,481,482],{},"::int"," cast has to go. ",[65,485,486],{},"jsonb ->> 'key'",[65,488,489],{},"json_extract(json, '$.key')",", and comparing a JSON boolean means comparing against ",[65,492,493],{},"1",", because SQLite stores JSON ",[65,496,497],{},"true"," as an integer. None of this fails in CI. All of it fails when a real request hits it. Grep for the Postgres-isms explicitly; do not wait for users to find them.",[44,500,502],{"id":501},"the-audit-that-actually-matters","The audit that actually matters",[37,504,505],{},"SQLite has no PostGIS, so before committing I audited what actually depended on it. I \"knew\" the only geospatial code was an admin-gated dataset feature. That was false. A public, CDN-fronted, unauthenticated tile endpoint was running a PostGIS MVT function on every request, on the read path, for everyone.",[37,507,508],{},"That discovery forced a real decision with three options: keep that one table on Postgres and run a hybrid (which quietly un-cancels the bill I was trying to cancel), rewrite the server-side MVT generation as client-side turf.js (a genuine product regression), or delete the feature. I traced its usage, found it wasn't load-bearing, and deleted it.",[37,510,511],{},"The point isn't the deletion. The point is that the migration decision was never really about tables. It was about tracing every consumer of the one capability the new engine cannot provide. My assumption covered the obvious consumer and missed the public one. Audit consumers, not schemas.",[44,513,515],{"id":514},"was-it-worth-it","Was it worth it",[37,517,518],{},"The bill went from $35 to $5, and that $5 was already being paid for Workers. Marginal cost: zero. The app got simpler too. One platform, one deploy, the database bound directly to the Worker with no connection pooling story at all.",[37,520,521],{},"But I'd frame the tradeoff honestly: this worked because the write volume was tiny, the geospatial dependency turned out to be expendable, and the app was already living on Cloudflare. Change any of those and the answer changes. A migration like this is 20% schema rewrite and 80% finding the code paths that quietly depend on the engine you're leaving. The rewrite fits in a weekend. The finding is the job.",[523,524,525],"style",{},"html pre.shiki code .s6ubI, html code.shiki .s6ubI{--shiki-default:#F97583;--shiki-dark:#F97583;--shiki-light:#D73A49}html pre.shiki code .s5n6i, html code.shiki .s5n6i{--shiki-default:#B392F0;--shiki-dark:#B392F0;--shiki-light:#6F42C1}html pre.shiki code .sAvwS, html code.shiki .sAvwS{--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8;--shiki-light:#24292E}html pre.shiki code .sAzo5, html code.shiki .sAzo5{--shiki-default:#FFAB70;--shiki-dark:#FFAB70;--shiki-light:#E36209}html pre.shiki code .s4Y1p, html code.shiki .s4Y1p{--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF;--shiki-light:#032F62}html pre.shiki code .sXWYR, html code.shiki .sXWYR{--shiki-default:#79B8FF;--shiki-dark:#79B8FF;--shiki-light:#005CC5}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);}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);}",{"title":110,"searchDepth":152,"depth":152,"links":527},[528,529,530,531,532],{"id":46,"depth":152,"text":47},{"id":59,"depth":152,"text":60},{"id":409,"depth":152,"text":410},{"id":501,"depth":152,"text":502},{"id":514,"depth":152,"text":515},"cloudflare","2026-07-07T18:30:00.000+05:30","Moving a live multi-tenant SaaS off Supabase Postgres onto Cloudflare D1 with zero downtime. The schema rewrite was the easy part. The date-coercion bug, the PostGIS audit, and the break-even math were not.","md",null,{"position":264},"\u002Farticles\u002Fcutting-a-supabase-bill-to-zero-with-cloudflare-d1",{"title":32,"description":535},"published","articles\u002F7.cutting-a-supabase-bill-to-zero-with-cloudflare-d1",[533,544,545,546,547,548,549],"d1","supabase","sqlite","drizzle","nuxt","migration","34vjmIhzQ3h7oLc1_BA1AhLlRCbtpU2XwHyFYpD3MTk",[552,554,563,572,580,589,595],{"path":539,"title":32,"description":535,"date":534,"updatedAt":537,"category":533,"tags":553,"image":537,"status":541},[533,544,545,546,547,548,549],{"path":555,"title":556,"description":557,"date":558,"updatedAt":537,"category":548,"tags":559,"image":537,"status":541},"\u002Farticles\u002Ftil-shiki-multitheme-code-block-background","TIL: Shiki multi-theme highlighting does not switch the code-block background","Shiki multi-theme mode swaps token colours per colour scheme but leaves the pre background variable empty, so light-theme text can land on a dark block. You have to theme the container yourself.","2026-07-07T02:30:00.000+05:30",[548,560,561,562],"shiki","nuxt-content","dark-mode",{"path":564,"title":565,"description":566,"date":567,"updatedAt":537,"category":533,"tags":568,"image":537,"status":541},"\u002Farticles\u002Fdynamic-og-images-on-cloudflare-workers","Generating dynamic Open Graph images on Cloudflare Workers","Rendering per-page and per-article social cards at the edge with Satori and @cf-wasm\u002Fog: how the route works, what Satori refuses to render, and the cold-versus-warm numbers.","2026-07-06T22:00:00.000+05:30",[533,569,570,571,548],"workers","open-graph","satori",{"path":573,"title":574,"description":575,"date":576,"updatedAt":537,"category":533,"tags":577,"image":537,"status":541},"\u002Farticles\u002Fmigrating-a-nuxt-site-from-cloudflare-pages-to-workers","Migrating a Nuxt site from Cloudflare Pages to Workers","The full path from the cloudflare-pages preset to a cloudflare_module Worker: the config changes, the custom-domain cutover that no deploy step handles for you, and the gotchas that only show up in CI.","2026-07-06T21:00:00.000+05:30",[533,569,548,578,579],"nitro","devops",{"path":581,"title":582,"description":583,"date":584,"updatedAt":537,"category":585,"tags":586,"image":537,"status":541},"\u002Farticles\u002Ftil-tailwind-v4-webkit-mask-prefix","TIL: Tailwind v4 mask utilities need the webkit prefix in plain CSS","Porting a Tailwind mask-composite utility to scoped CSS quietly breaks in Chrome and Safari unless you also write the -webkit-mask-* properties, because Tailwind was emitting them for you.","2026-07-06T20:15:00.000+05:30","css",[587,585,588],"tailwind","webkit",{"path":590,"title":591,"description":592,"date":593,"updatedAt":537,"category":533,"tags":594,"image":537,"status":541},"\u002Farticles\u002Ftil-workers-cache-nitro-passthrough","TIL: Enabling Workers Cache through Nitro without a native option","Nitro forwards arbitrary keys into the generated wrangler config, so you can turn on Cloudflare Workers Cache with a single line even though there is no dedicated Nitro option for it.","2026-07-06T14:30:00.000+05:30",[533,569,578,548],{"path":596,"title":597,"description":598,"date":599,"updatedAt":537,"category":600,"tags":601,"image":537,"status":541},"\u002Farticles\u002Fbuild-and-publish-vue-3-components-using-rollup","Build & Publish Vue 3 Components using Rollup.js","Let's build a reusable Vue 3 component using rollup","2022-07-04T16:20:09.000+05:30","vue",[602,603],"vue 3","rollup",{"left":4,"top":4,"width":9,"height":9,"rotate":4,"vFlip":6,"hFlip":6,"body":605},"\u003Cpath fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m12 19l-7-7l7-7m7 7H5\"\u002F>",1783535833735]