# Hey, I'm Yusuf 👋🏼 My fundamental drive in life is to *create things*. I aim to build products that feel intuitive, thoughtful, and genuinely useful. My background is a bit unusual. I used to be an attorney specializing in corporate and technology law. But I kept coming back to what I'd loved since the beginning: computers and coding. So, I took the leap and now build things for the web. I am a big fan of :icon{name="i-simple-icons-vuedotjs"} Vue | :icon{name="i-simple-icons-nuxtdotjs"} Nuxt | :icon{name="i-simple-icons-tailwindcss"} TailwindCSS | :icon{name="i-simple-icons-typescript"} TypeScript | :icon{name="i-simple-icons-cloudflare"} Cloudflare Workers. And I still secretly love :icon{name="i-simple-icons-csharp"} C# (thanks to :icon{name="i-simple-icons-unity"} Unity). I believe in *thoughtful design*, *clear communication*, and *bridging concepts*. To me, great software isn't just about code. It's more about careful research, pedantic distillation, countless refinements, and a relentless pursuit of clarity until everything feels just right. I value *honesty*, *precision*, and *attention to detail* in everything I do. Find me on [::icon{name="i-simple-icons-linkedin"} ::](https://www.linkedin.com/in/ymansurozer/){rel="nofollow"} | [::icon{name="i-simple-icons-github"} ::](https://github.com/ymansurozer){rel="nofollow"} | [::icon{name="i-simple-icons-bluesky"} ::](https://bsky.app/profile/ymansurozer.bsky.social){rel="nofollow"} | [::icon{name="i-simple-icons-x"} ::](https://x.com/ymansurozer){rel="nofollow"} or email me at . ::recent-blog-posts :: # Principles I believe having a set of guiding principles and mental models is crucial. Map is not the territory but having a compass helps. ## On Principles ::callout{color="primary" icon="i-lucide-refresh-cw"} Principles are not static; they evolve and adapt. :: ## On Life ::callout{color="neutral" icon="i-lucide-trending-up"} Play the long game. Compounding is real. :: ::callout{color="neutral" icon="i-lucide-focus"} What's here now when there's no problem to solve? :: ## On Craft ::callout{color="neutral" icon="i-lucide-book-open"} Learn from first principles. :: ::callout{color="neutral" icon="i-lucide-search"} Research before execution. Being methodical beats being clever. :: ::callout{color="neutral" icon="i-lucide-git-branch"} Great work is built on countless small refinements. :: ::callout{color="neutral" icon="i-lucide-hammer"} Ideas are cheap, execution is key. And LLMs still haven't changed that. :: ::callout{color="neutral" icon="i-lucide-palette"} Taste matters more than talent. :: ::callout{color="neutral" icon="i-lucide-paintbrush"} Polish is part of the MVP. :: ::callout{color="neutral" icon="i-lucide-target"} Results matter, hours don't. :: ## On Conduct ::callout{color="neutral" icon="i-lucide-message-square"} Precise and concise communication is a mark of respect. :: ::callout{color="neutral" icon="i-lucide-award"} Giving credit is not optional. :: ::callout{color="neutral" icon="i-lucide-clock"} Busyness is not a virtue. It's often a sign of lazyness or incompetence. :: ::callout{color="neutral" icon="i-lucide-help-circle"} There are no dumb questions. :: ## On Coding ::callout{color="neutral" icon="i-lucide-code"} Code to create, not to code. :: ::callout{color="neutral" icon="i-lucide-check-circle"} Strong types, clean code, clear docs, proper formatting. :: ::callout{color="neutral" icon="i-lucide-package"} Only novel problems deserve novel code. Everything else has a package. :: # Projects ::callout{color="neutral" to="https://musicsmith.ai"} **MusicSmith (2025—)**:br AI music generation platform that creates music based on prompts. :: ::callout{color="neutral" to="https://www.notionfinancetracker.com"} **Notion Finance Tracker (2024—)**:br API-enhanced financial dashboard blending real-time asset tracking, multi-currency budgeting, and interactive charts within Notion's familiar interface. :: ::callout{color="neutral" to="https://www.docuchat.io"} **DocuChat (2022—2025, sold)**:br AI chatbot platform that helps businesses reduce support costs while maintaining answer quality. Built with careful attention to data privacy and EU compliance. :: ::card{color="neutral"} **Career Transition (2022)**:br Former attorney, now software developer. :: ::callout{color="neutral" to="https://www.kisiselverilerinkorunmasi.org"} **Data Protection Initiative (2016—2020)**:br An open knowledge platform that made data protection law accessible to everyone in Turkey, featuring curated legislation, case law, and monthly insights in a methodically organized format. :: ::callout --- color: neutral to: https://web.archive.org/web/20170505213412/http://www.dijitalhaklar.org --- **Digital Rights (2015—2017)**:br An initiative that analyzed and summarized terms of service and privacy policies from major internet platforms, helping users understand their digital rights in plain language. :: # Uses ## Software Stack - **Framework**: [Vue](https://vuejs.org/){rel="nofollow"} and [Nuxt](https://nuxt.com/){rel="nofollow"} - **Server Engine**: [Nitro](https://nitro.build/){rel="nofollow"} or [Hono](https://hono.dev/){rel="nofollow"} - **Hosting**: [Cloudflare Workers](https://workers.cloudflare.com){rel="nofollow"} - **Authentication**: [Better Auth](https://www.better-auth.com/){rel="nofollow"} - **Database**: [Cloudflare D1](https://www.cloudflare.com/developer-platform/products/d1/){rel="nofollow"} or [Prisma Postgres](https://www.prisma.io/postgres){rel="nofollow"} with [Prisma Accelerate](https://www.prisma.io/accelerate){rel="nofollow"} - **File Storage**: [Cloudflare R2](https://www.cloudflare.com/developer-platform/products/r2/){rel="nofollow"} - **Key-Value Store**: [Cloudflare Workers KV](https://www.cloudflare.com/developer-platform/products/workers-kv/){rel="nofollow"} - **Payments**: [Polar](https://polar.sh/){rel="nofollow"} - **Email**: [Cloudflare Email](https://developers.cloudflare.com/email-service/){rel="nofollow"} - **Analytics**: [PostHog](https://posthog.com){rel="nofollow"} - **Error Tracking**: [PostHog](https://posthog.com){rel="nofollow"} or [Sentry](https://sentry.io){rel="nofollow"} - **UI Library**: [Nuxt UI](https://ui.nuxt.com){rel="nofollow"} - **Format & Lint**: [Oxc](https://oxc.rs/){rel="nofollow"} ## Development Environment - **Editor**: [VS Code](https://code.visualstudio.com){rel="nofollow"} - **Font**: [JetBrains Mono](https://www.jetbrains.com/lp/mono/){rel="nofollow"} - **Theme**: [One Hunter Flexoki Dark](https://open-vsx.org/extension/RaillyHugo/one-hunter){rel="nofollow"} - **Icons**: [Carbon Product Icons](https://open-vsx.org/extension/antfu/icons-carbon){rel="nofollow"} and [Flow Icons](https://open-vsx.org/extension/thang-nm/flow-icons){rel="nofollow"} ## Terminal - **Terminal**: [iTerm2](https://iterm2.com){rel="nofollow"} with Quake-style dropdown, running [Zsh](https://zsh.sourceforge.io){rel="nofollow"} with [Starship](https://starship.rs){rel="nofollow"} prompt - **Font**: [MesloLGS Nerd Font](https://www.nerdfonts.com){rel="nofollow"} ## Desktop Applications - **Browser**: [Dia Browser](https://www.diabrowser.com/){rel="nofollow"} - **Note-taking**: [Obsidian](https://obsidian.md){rel="nofollow"} - **Window Management**: [Rectangle](https://rectangleapp.com){rel="nofollow"} - **Launcher & Hotkeys**: [Raycast](https://www.raycast.com){rel="nofollow"} ## Hardware - **Computer**: MacBook Pro 14" M4 Pro 24GB - **Display**: Dell U3223QE 31.5" - **Keyboard**: Currently Logitech MX Keys Mini, but I will forever love my Nuphy Air75 v2 with brown switches - **Mouse**: Logitech MX Master 3S - **Headphones**: Soundcore Life Q35 # Nuxt on the Edge with Cloudflare Workers ::callout{color="primary" icon="i-lucide-info"} I've updated this article with the recent announcements from Cloudflare Developer Week 2025. Cloudflare is truly on a roll and I'm excited to see what they'll come up with next. :: I've been comfortably settled in Vercel's ecosystem, enjoying their excellent DX and integrations. But lately, Cloudflare's expanding ecosystem caught my attention. The breadth and diversity of their products was impressive. Edge computing, databases, queues, AI models, vector stores, and more—*all under one roof*. Curiosity got the better of me and I decided to migrate one of my side projects, [Notion Finance Tracker](https://www.notionfinancetracker.com){rel="nofollow"}, from Vercel to Cloudflare. What followed was an **unexpected journey** through edge computing constraints, database decisions, and valuable lessons in modern web architecture. This blog post documents this journey, sharing the solutions and lessons learned along the way. ::callout{color="neutral" icon="i-simple-icons-nuxt"} Want the easy path? Use [NuxtHub](https://hub.nuxt.com/){rel="nofollow"}. I went DIY to learn and keep control, borrowing a few ideas from NuxtHub. :: ## Cloudflare Architecture ### Workers vs. Pages When deploying a Nuxt application to Cloudflare, you face an immediate choice: **Cloudflare Pages or Cloudflare Workers?** While both run on Cloudflare's global edge network, their capabilities are different. Here's a quick comparison: | Capability | Workers | Pages | | ------------------------------------------------------------------------------------------------------- | -------- | ----- | | Logs | ✅ | ❌ | | Cron Triggers | ✅ | ❌ | | Source Maps | ✅ | ❌ | | Email Workers | ✅ | ❌ | | Queue Consumers | ✅ | ❌ | | [Static Assets](https://developers.cloudflare.com/workers/static-assets/){rel="nofollow"} | ✅ | ✅ | | [Git Integration](https://developers.cloudflare.com/workers/ci-cd/builds){rel="nofollow"} | ✅ (Beta) | ✅ | | [Preview Deployments](https://developers.cloudflare.com/workers/configuration/previews){rel="nofollow"} | ✅ (Beta) | ✅ | Cloudflare's message during their recent Developer Week 2025 was loud and clear: **Workers is the future.** As they put it in the [blog post](https://blog.cloudflare.com/full-stack-development-on-cloudflare-workers/){rel="nofollow"}: *"Now that Workers supports both serving static assets and server-side rendering, you should start with Workers. Cloudflare Pages will continue to be supported, but, going forward, all of our investment, optimizations, and feature work will be dedicated to improving Workers."* For Nuxt applications specifically, despite what the official [Nuxt](https://nuxt.com/deploy/cloudflare){rel="nofollow"} and [Nitro's](https://nitro.build/deploy/providers/cloudflare){rel="nofollow"} docs currently recommend, *Workers is now clearly the better choice for Nuxt projects*. ### Edge Computing Edge computing is one of those terms that sounds more complicated than it is. The idea is that instead of your code running in some distant data center, it runs... well, everywhere. Your application gets distributed across hundreds of locations worldwide, sitting as close as possible to your users. Think of it as the difference between ordering from a central warehouse versus picking up from your local store. Cloudflare takes this concept to the extreme with their famous *"deploy to region: earth"* approach. Their edge network spans over 300 data centers globally, and when you deploy a Worker, your code instantly becomes available on all of them. However, these **edge nodes aren't running your typical Node.js runtime**. They're optimized for running lightweight JavaScript/TypeScript code with [specific constraints](https://workers-nodejs-compat-matrix.pages.dev){rel="nofollow"}. This is where I hit my first roadblock - npm packages that worked perfectly fine in Node.js started throwing errors in the edge runtime. ::callout{color="warning" icon="i-lucide-triangle-alert"} Deploying on the edge means you always need to verify npm package compatibility with edge runtimes before adding them to your project. :: ### Wrangler & Bindings To deploy your Worker to Cloudflare or run it locally, you'll need the [Wrangler CLI](https://developers.cloudflare.com/workers/cli-wrangler){rel="nofollow"}. It is your command-line Swiss Army Knife for managing everything Cloudflare related. At the core of Wrangler is the [`wrangler.toml` or recently introduced `wrangler.json` file](https://developers.cloudflare.com/workers/wrangler/configuration){rel="nofollow"}, which is the blueprint for linking your application to Cloudflare's infrastructure. A key concept in the Cloudflare Workers ecosystem is [Bindings](https://developers.cloudflare.com/workers/runtime-apis/bindings){rel="nofollow"}. Bindings securely connect your Worker to services like databases and KV stores without hardcoding credentials. Unlike traditional environment variables, which merely store string values, bindings go further by establishing authenticated connections to Cloudflare services. **Think of Bindings as pre-authenticated clients available in your Worker code**, not just static configuration values. Bindings are defined in your Wrangler configuration file and are available in your Worker code inside the `env` object: ```json [wrangler.json] { "kv_namespaces": [ { "binding": "MY_KV", "id": "" } ] } ``` ```ts [server/api/save-to-kv.ts] export default defineEventHandler(async (event) => { const kv = event.context.cloudflare.env.MY_KV; await kv.put("my-key", "my-value"); return "Put successful"; }); ``` Here, `MY_KV` is a binding that will be injected into your Worker when a request is made, and it will be available in the `env` object inside your request handler. ## Configuration ### Wrangler To configure Wrangler and thus your Worker for Nuxt, you need to create a `wrangler.json` file in the root of your project. This is my starter template for a Nuxt 3 application: ```json [wrangler.json] { "$schema": "node_modules/wrangler/config-schema.json", "name": "my-project", "main": "./.output/server/index.mjs", "compatibility_date": "2025-02-16", // or later "compatibility_flags": ["nodejs_compat"], "placement": { "mode": "smart" }, "upload_source_maps": true, "observability": { "enabled": true, "head_sampling_rate": 1, "logs": { "enabled": true, "head_sampling_rate": 1 } }, "assets": { "directory": "./.output/public/", "binding": "ASSETS" } } ``` Here's a breakdown of key Wrangler configuration options: - The [`nodejs_compat` compatibility flag](https://developers.cloudflare.com/workers/runtime-apis/nodejs){rel="nofollow"} enables support for many Node.js built-in modules and APIs within Cloudflare Workers. This is particularly useful when migrating existing Node.js applications or using npm packages that weren't originally designed for Cloudflare's edge runtime. Keep in mind, though, that not all Node.js APIs are fully supported. Always check the [compatibility matrix](https://workers-nodejs-compat-matrix.pages.dev){rel="nofollow"} to verify specific APIs. - The [`assets` configuration](https://developers.cloudflare.com/workers/static-assets/binding){rel="nofollow"} allows your Nuxt app to serve static files (such as images, CSS, JavaScript bundles, and other assets generated during the Nuxt build process) directly from Cloudflare's global edge network. - The [`observability` configuration](https://developers.cloudflare.com/workers/observability/logs/workers-logs){rel="nofollow"} enables logging and monitoring for your Nuxt server running on Cloudflare Workers. - The [`upload_source_maps` configuration](https://developers.cloudflare.com/workers/observability/source-maps){rel="nofollow"} allows you to upload source maps directly to Cloudflare. Source maps translate minified or transpiled code back into its original, readable form, making your logs and errors more readable. - The [`placement` configuration](https://developers.cloudflare.com/workers/configuration/smart-placement){rel="nofollow"} controls where your app runs within Cloudflare's global network. With Smart Placement enabled, Cloudflare intelligently positions your Nuxt Worker closer to your backend infrastructure or databases, optimizing latency and performance. ### TypeScript To get proper type support for Cloudflare Workers (especially for your bindings), you need to install the `@cloudflare/workers-types` package and configure your Typescript compiler to use it. ::code-group ```bash [npm] npm install -D @cloudflare/workers-types ``` ```bash [pnpm] pnpm add -D @cloudflare/workers-types ``` ```bash [yarn] yarn add -D @cloudflare/workers-types ``` ```bash [bun] bun add -D @cloudflare/workers-types ``` :: ```ts [tsconfig.json] export default defineNuxtConfig({ typescript: { tsConfig: { compilerOptions: { types: ["@cloudflare/workers-types/2023-07-01"], }, }, }, }); ``` The appended date (`2023-07-01`) corresponds to [the specific compatibility date](https://github.com/cloudflare/workerd/tree/main/npm/workers-types){rel="nofollow"} for the Workers runtime. You can also use `@cloudflare/workers-types/experimental` to get the types for the latest compatibility date. Once done, you can generate types for your Cloudflare Worker bindings using the `wrangler types` command: `wrangler types ./server/types/worker.d.ts --include-runtime false`. Finally, you should augment your `H3EventContext` with Cloudflare-specific properties `cf` and `cloudflare` properties, which will be available in server routes under `event.context`. Nitro Cloudflare Dev module will normally automatically do this, but I've found that Typescript sometimes does not pick up the types correctly, so I recommend adding this explicitly. ```ts [server/types/env.d.ts] import type { CfProperties, ExecutionContext, Request } from "@cloudflare/workers-types"; declare module "h3" { interface H3EventContext { cf: CfProperties; cloudflare: { request: Request; env: Env; context: ExecutionContext; }; } } ``` ::callout{color="neutral" icon="i-lucide-info"} A simpler and more recent way to get types is to use the [`wrangler types` command](https://developers.cloudflare.com/workers/wrangler/commands/#types){rel="nofollow"} without the `--include-runtime false` flag. This generates types based on your project's compatibility date, removing the need to install the `@cloudflare/workers-types` package. You'll just need to point your `tsconfig.json` to the generated type file instead. :br:br However, I've had issues with my types not being picked up correctly this way. So I'm still sticking with the `@cloudflare/workers-types` package for now. :: ### Variables & Secrets In a typical Nuxt project, you define environment variables in a `.env` file at the root of your project. Then you *load* them into your Nuxt runtime configuration via `nuxt.config.ts`. This configuration system integrates well with Cloudflare's environment variables and secrets, allowing you to maintain a familiar workflow. ```ts [.env] MY_ENV_VAR = "my-value"; ``` ```ts [nuxt.config.ts] export default defineNuxtConfig({ runtimeConfig: { myEnvVar: process.env.MY_ENV_VAR, }, }); ``` ::callout{color="neutral" icon="i-lucide-info"} You can also define your environment variables with the `NUXT_` prefix in your `.env` file, which will be available both in build and runtime. This is normally the recommended way to define environment variables for Nuxt applications. But I like being a bit more explicit and defining them in the `runtimeConfig`. :: You can now access these environment variables in your Nuxt application via `useRuntimeConfig()`: ```ts [server/api/hello.ts] export default defineEventHandler((event) => { return `Hello ${useRuntimeConfig().myEnvVar}`; }); ``` Cloudflare's own [documentation on environment variables](https://developers.cloudflare.com/workers/configuration/environment-variables){rel="nofollow"} was a bit confusing for me at first because it talks about a usual Workers project and not a Nuxt project. Unless you're heavily relying on Wrangler CLI to manage your deployments, you don't need to worry about this. Just follow the Nuxt way of doing things and you'll be fine. ::callout{color="warning" icon="i-lucide-triangle-alert"} If you're deploying via the Cloudflare dashboard with a Git repository, you need to **set up Build variables** in the Cloudflare dashboard. The UI is confusing here because it shows a *Variables and Secrets* section twice: one for Runtime variables at the top and one for Build variables at the bottom. Be sure to scroll down to the Build section to set them up there. :br:br Thanks to Nuxt's runtime configuration, these build variables will also be available at runtime via `useRuntimeConfig()`. So you don't need to add them to the runtime variables section. :: You also have the option to use the Cloudflare way and keep your environment variables in your `wrangler.json` file and secrets in `.dev.vars` file for local development. [Nitro Cloudflare Dev module](https://ymo.dev/#local-development) will automatically pick them up. ### Local Development Developing locally with Cloudflare Workers introduces a unique challenge: how will you access your bindings and environment variables defined in your Wrangler configuration inside your Nuxt application? Cloudflare injects bindings into your Worker at runtime, so you normally don't have them available locally. The answer is simple: use the `nitro-cloudflare-dev` module: ::code-group ```bash [npm] npm i -D nitro-cloudflare-dev ``` ```bash [pnpm] pnpm i -D nitro-cloudflare-dev ``` ```bash [yarn] yarn add -D nitro-cloudflare-dev ``` ```bash [bun] bun i -D nitro-cloudflare-dev ``` :: ```ts [nuxt.config.ts] export default defineNuxtConfig({ modules: ["nitro-cloudflare-dev"], }); ``` Nitro Cloudflare Dev module will automatically pick up your bindings from your `wrangler.json` file and environment variables from your `.dev.vars` file for local development. ### Nitro Preset We want to deploy our app as a Worker so we'll need to set the Nitro preset to `cloudflare-module`: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { preset: "cloudflare-module", }, }); ``` We also need to set the correct compatibility date in Nuxt configuration. This ensures our application aligns with Cloudflare's latest runtime features, including beta support for static assets. ```ts [nuxt.config.ts] export default defineNuxtConfig({ compatibilityDate: "2024-09-19", // or later }); ``` ::callout{color="neutral" icon="i-lucide-info"} If you're using Cloudflare's Git integration to deploy your app, you don't need to set the Nitro preset or the compatibility date. Nitro will handle these itself and will even add the Node compatibility flag for you, without needing to handle these in the `wrangler.json` file. :: ### Deployment Cloudflare [recently](https://developers.cloudflare.com/changelog/2025-02-07-new-ways-to-get-started-on-workers/){rel="nofollow"} introduced direct Git integration for Workers. You can now connect your GitHub or GitLab repository directly from the Cloudflare dashboard. Once connected, every push to your selected branch automatically triggers a build and deployment. If you would like to enable preview deployments for pull requests, navigate to `Settings > Build > Branch control` and enable `Builds for non-production branches`. ## Database and ORM I have always been a PostgreSQL and Prisma fan. They're familiar, intuitive, and have served me very well. Naturally, I hoped to carry this comfort over into the Cloudflare ecosystem. But things didn't go as planned. I went down a rabbit hole of trying to find the "perfect" fit for my use case. I tried many combinations of D1, PostgreSQL, Prisma, Drizzle, and Hyperdrive. What began as a simple database decision evolved into a weeks-long exploration of trade-offs between familiarity, performance, and edge compatibility. Each combination offered unique advantages while introducing its own set of challenges: | | **Prisma** | **Drizzle** | | --------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **D1 (SQLite)** | • Limited support:br• Cumbersome migrations:br• No Prisma Studio support | • First-class support:br• Streamlined migrations:br• Drizzle Studio works with D1 | | **PostgreSQL** | • Requires Hyperdrive for edge performance:br• WASM compatibility issues with Workers:br• Needs extra configuration in Nuxt/Nitro | • Requires Hyperdrive for edge performance:br• Better Cloudflare edge compatibility:br• Simpler configuration:br• SQL-like syntax (pro or con depending on preference) | Here's how I worked with the complexities and arrived at a [solution that worked for me](https://ymo.dev/#my-solution). ### Cloudflare D1 Cloudflare offers its own database solution called **D1, an SQLite-compatible database designed specifically for edge computing**. D1 is fast, affordable, scalable, and integrates perfectly with Cloudflare Workers. But there were some issues for me: - **SQLite vs. PostgreSQL**: I'm very used to PostgreSQL, and migrating to D1 would mean adopting SQLite. - **Prisma Migrations**: Prisma migrations with D1 are just [cumbersome](https://www.prisma.io/docs/orm/overview/databases/cloudflare-d1?utm_source=twitter#migration-workflows){rel="nofollow"}. You can't directly apply migrations using Prisma CLI. Instead, you need to generate SQL migration files with Prisma CLI and then manually apply them using Wrangler CLI. However, Prisma has been working on a solution for this and [Prisma-based remote migrations](https://www.prisma.io/blog/prisma-orm-6-6-0-esm-support-d1-migrations-and-prisma-mcp-server#cloudflare-d1--tursolibsql-migrations-early-access){rel="nofollow"} are now in beta. - **Prisma Studio**: Prisma Studio doesn't support D1 databases, neither locally nor remotely. There are community-driven projects like [D1 Manager](https://github.com/JacobLinCool/d1-manager){rel="nofollow"} that provide a somewhat viable alternative, but it's not the same like Prisma Studio. ::callout{color="neutral" icon="i-lucide-info"} Cloudflare recently introduced [read replicas for D1](https://blog.cloudflare.com/d1-read-replication-beta/){rel="nofollow"}. This is a great feature that allows you to have a read-only copy of your database available in all Cloudflare data centers. However, I am not sure how it would work with Prisma (yet). :: ### PostgreSQL & Hyperdrive You can see why sticking with my tried-and-true PostgreSQL + Prisma stack was the clear choice at first. Adding Cloudflare's [Hyperdrive](https://developers.cloudflare.com/hyperdrive){rel="nofollow"} offers global query acceleration, everything seemed perfect. But, of course, there were some new issues now: - Compatibility with Cloudflare Workers' edge runtime in Nitro [isn't whole yet](https://github.com/unjs/db0/issues/50){rel="nofollow"}. It requires enabling [experimental WebAssembly support](https://nitro.build/config#wasm){rel="nofollow"} and tweaking Rollup configurations—steps that quickly became frustrating. - Persistent errors like mysterious WASM file issues ([ENOENT errors with Prisma's query engine](https://github.com/unjs/unwasm/issues/21){rel="nofollow"}) added to the frustration. ### Enter Drizzle Given these persistent issues, I explored Drizzle ORM as an alternative. Drizzle seemed to offer several advantages over Prisma in the Cloudflare Workers ecosystem: - **Better D1 Support**: Drizzle provides robust, out-of-the-box support for Cloudflare D1, including Drizzle Studio that works locally and remotely via the D1 HTTP API. - **Simplified Migrations**: Drizzle's migration workflow is straightforward as [it can use](https://orm.drizzle.team/docs/guides/d1-http-with-drizzle-kit){rel="nofollow"} the D1 HTTP API. - **Edge Compatibility**: Drizzle is designed with edge runtimes in mind, ensuring smooth operation within Cloudflare Workers and Nuxt without additional configuration or experimental flags. At this point, I was very happy to have found the solution to all my database problems. However, after a few hours of using it, I realized that its SQL-first API felt less intuitive compared to Prisma's expressive query builder. I truly missed Prisma's query syntax, which brought me back to the drawing board. ### My Solution After days of ~~over~~thinking, I settled on an unconventional yet practical solution: - **Database**: I chose **Cloudflare D1**. Its tight integration, simplicity, and edge-native performance outweighed PostgreSQL's additional complexity. - **ORM**: I decided to use **both Prisma and Drizzle**, but for different purposes. Prisma's intuitive query syntax and developer experience remain unmatched for me. Meanwhile, Drizzle's studio provide the missing piece for data inspection and management, both locally and remotely. ::div{className="flex,flex-col,items-center,justify-center,gap-4"} ![Prisma and Drizzle](https://ymo.dev/images/prisma_drizzle.png){className="w-full,max-w-md"} :::div{className="text-center,text-sm,text-(--ui-text-muted)"} Prisma ❤️ Drizzle ::: :: Adopting two ORMs is a hack, I know. But it provides exactly what I needed: A simple and intuitive query builder with a beautiful studio for data management. ## Authentication ### Auth Landscape The Nuxt ecosystem offers several authentication solutions: 1. [**Sidebase Auth**](https://auth.sidebase.io){rel="nofollow"}: An Auth.js/NextAuth.js wrapper I'm familiar with from past projects. Unfortunately, it [isn't fully edge-compatible yet](https://github.com/sidebase/nuxt-auth/issues/117#issuecomment-1775434602){rel="nofollow"}, though this may change as they complete their migration from NextAuth to Auth.js. 2. [**Nuxt Auth Utils**](https://github.com/atinux/nuxt-auth-utils){rel="nofollow"}: A simple, lightweight, edge-compatible authentication solution from Atinux himself. **For most applications, Nuxt Auth Utils provides everything you need without unnecessary complexity.** 3. [**Better Auth**](https://www.better-auth.com){rel="nofollow"}: An all-in-one authentication solution with a comprehensive feature set including multi-factor authentication, fine-grained permissions, and an intuitive API. Its plugin ecosystem makes it highly extensible while maintaining developer experience. ### Attempting Better Auth Initially, I tried using Better Auth because of ~~the shiny object syndrome~~ its advanced features and comprehensive capabilities, thinking they might come in handy later, if not now. Of course, I quickly ran into several issues, though it was possible to work around them. - **PostgreSQL/Prisma Compatibility**: Better Auth works great with both PostgreSQL and Prisma. But as soon as you deploy it to Cloudflare Workers (or just run `wrangler dev`), you get a cryptic `The script will never generate a response` error. After digging through [GitHub issues](https://github.com/better-auth/better-auth/issues/969){rel="nofollow"}, I found out that this was due to Cloudflare's CPU time constraints [as explained by Better Auth's creator Bereket Engida](https://github.com/better-auth/better-auth/issues/969#issuecomment-2558043662){rel="nofollow"}. Surprisingly, even custom hashing functions and alternative login methods failed to resolve this for me. - **Globally Scoped Client Instances**: Better Auth expects globally scoped clients on both client and server sides. This proved particularly challenging on the server side, where you don't have access to the Cloudflare context (hence D1 or Hyperdrive) in global scope. Inspired by [Atinux's NuxtHub implementation](https://github.com/atinux/nuxthub-better-auth){rel="nofollow"}, I created a client-side composable that uses a globally scoped client instance and implemented a singleton pattern on the server side to maintain a consistent client across all requests. ```ts [app/composables/auth.ts] import { createAuthClient } from "better-auth/vue"; const client = createAuthClient(); export function useAuth() { async function getSession() { const { data, error } = await client.useSession(useFetch); if (error.value) { throw createError({ statusCode: error.value.status, statusMessage: error.value.statusText, message: error.value.message, fatal: true, }); } return data; } async function signUp() { } async function signIn() { } async function signOut() { } return { getSession, signUp, signIn, signOut }; } ``` ```ts [server/utils/auth.ts] import type { H3Event } from "h3"; import { betterAuth } from "better-auth"; import { prismaAdapter } from "better-auth/adapters/prisma"; let _auth: ReturnType; export function useServerAuth(event: H3Event) { if (!_auth) { const { authSecret, public: { app: { url } } } = useRuntimeConfig(event); const prisma = useDatabase(event); const auth = betterAuth({ secret: authSecret, baseURL: url, database: prismaAdapter(prisma, { provider: "postgresql", // or "sqlite" for D1 }), emailAndPassword: { enabled: true, }, }); _auth = auth; } return _auth; } ``` - **WebAssembly Dependencies**: Adding Better Auth to my project unexpectedly required enabling the [experimental WebAssembly flag](https://nitro.build/config#wasm){rel="nofollow"} in my Nitro configuration. Interestingly, this wasn't necessary when using the lighter-weight Nuxt Auth Utils. ### Embracing Nuxt Auth Utils After some point, I finally realized that I don't need all the features of Better Auth. I just needed a simple, lightweight, and edge-compatible authentication solution. And that's exactly what Nuxt Auth Utils provides. It is called "utils" for a reason. It is not a full-fledged authentication solution like Better Auth. Instead, it is a set of utilities to help you implement authentication in your Nuxt application. This reduces externalities and makes it easier to reason about the codebase. For anyone in a similar situation, *I recommend Nuxt Auth Utils as the default choice for most projects* and only reach for Better Auth if you *really* need its advanced features. ## Pitfalls & Learnings ### Nuxt Image I could not get [Nuxt Image](https://image.nuxt.com){rel="nofollow"} to work with Cloudflare Workers at first. After hours of troubleshooting, I finally discovered the missing piece of the puzzle. For Cloudflare to transform images, you must enable **Image Transformations** for your domain in the Cloudflare dashboard under `Images > Transformations`. If you plan to use images from external domains (like your CDN or third-party services), you also need to enable `Resize Image from Any Origin`. Transformations are available on Cloudflare's free plan, but it's not enabled by default. Once activated, configuring Nuxt Image becomes straightforward: ```ts [nuxt.config.ts] export default defineNuxtConfig({ image: { format: ["webp"], provider: "cloudflare", cloudflare: { baseURL: "https://your-domain.com", // Your deployment's domain }, }, }); ``` ::callout{color="neutral" icon="i-lucide-info"} Don't worry about what Images documentation says about `base url (zone)`. It is simply the domain (base url) of your deployment. :: ### Sentry Sentry has always been my go-to error tracking solution. And naturally, I wanted to use it with Cloudflare Workers. Client-side error tracking with Sentry works seamlessly (either via [@sentry/nuxt module](https://github.com/getsentry/sentry-javascript/tree/HEAD/packages/nuxt){rel="nofollow"} or manual integration via [@sentry/vue](https://github.com/getsentry/sentry-javascript/tree/a4f8a2167f8e59f2a0a181e0c49ad6ac425f0b99/packages/vue){rel="nofollow"}). However, the server-side is a different story. The standard [@sentry/node](https://github.com/getsentry/sentry-javascript/tree/a4f8a2167f8e59f2a0a181e0c49ad6ac425f0b99/packages/node){rel="nofollow"} doesn't work with Cloudflare Workers due to edge runtime constraints. But after some investigation, I found two solutions: 1. [Toucan.js](https://github.com/robertcepa/toucan-js){rel="nofollow"}: A lightweight Sentry client designed specifically for edge environments. This became my preferred option. 2. [Cloudflare Tail Workers](https://developers.cloudflare.com/workers/observability/logs/tail-workers/){rel="nofollow"}: Cloudflare offers automatic error forwarding to Sentry through their Tail Workers integration. While promising in theory, it was impossible for me to set up due to consistent UI bugs in the process. In the end, the combination of the Sentry Nuxt module for client-side and Toucan.js for server-side provided reliable error tracking across my entire application. ### Prisma Client Prisma on the edge was a pain. The issue was not that it was incompatible with the edge runtime, but that there were too many elements in the stack I had to configure to get it to work. Here are my learnings: - You might think that you need to use the `@prisma/client/edge` package. But you don't because it apparently is reserved only if you're using Prisma Accelerate (more on that below). You need to use the `@prisma/client` package but with adapters as defined in the [Prisma docs](https://www.prisma.io/docs/orm/prisma-client/deployment/edge/deploy-to-cloudflare){rel="nofollow"}. - If you want a globally distributed database, you either use **Cloudflare Hyperdrive or Prisma Accelerate**. Using Accelerate is more straightforward but if you want the full Cloudflare experience, you need to use Hyperdrive with the relevant Prisma adapter. - For PostgreSQL adapters, take a look at the more specific adapters before trying out the `PostgreSQL (traditional)` adapter. For instance, if you're migrating from Vercel, you'll need the Neon adapter. - In most cases, you will need to enable the [experimental WebAssembly flag](https://nitro.build/config#wasm){rel="nofollow"} in your Nitro configuration and might need to mark some modules as external in the Rollup configuration (but again, this might have been resolved in the [latest Nitro release](https://github.com/nitrojs/nitro/pull/2976){rel="nofollow"}). ### Cron Jobs Cloudflare's [`cron triggers`](https://developers.cloudflare.com/workers/configuration/cron-triggers){rel="nofollow"} are great for running scheduled tasks. And you can use them in a Nuxt project by using Nitro's experimental tasks feature. Here are the steps to set it up: 1. Define a `task` in the `server/tasks` directory: ```ts [server/tasks/myTask.ts] export default defineTask({ meta: { name: "my-task", description: "My task description", }, run({ payload, context }) { // do something return { result: "Success" }; }, }); ``` 2. Enable tasks in your Nitro configuration and specify scheduled tasks: ```ts [nuxt.config.ts] export default defineNuxtConfig({ nitro: { experimental: { tasks: true }, scheduledTasks: { "* * * * *": ["my-task"], // Make sure to specify the task name as defined in the task file }, }, }); ``` 3. Define a cron trigger in your Wrangler configuration using exactly same pattern you defined in `scheduledTasks`: ```json [wrangler.json] { "triggers": { "crons": ["* * * * *"] } } ``` ::callout{color="neutral" icon="i-lucide-info"} See Atinux's example [here](https://github.com/atinux/nitro-cloudflare-crons){rel="nofollow"} for a more complete example. :: ## Was It Worth It? Absolutely—just *maybe* not for the reasons you'd expect. More than performance or cost, **what truly attracted me to Cloudflare was the unified experience** its platform provides. It offers everything I need, all seamlessly integrated within a single, cohesive ecosystem. This cohesion means fewer moving parts, less time spent hopping between disparate platforms, and more energy spent on building and shipping. Sure, navigating the Cloudflare ecosystem initially felt like too much to handle, but each bump clarified my understanding. Rather than fighting the complexity of scattered external services, I had a single, integrated ecosystem—one cohesive mental model to master. *Once you get the Cloudflare way of deploying apps to the edge, things will fall into place*. For those considering a similar migration, my advice is simple: *start small*. Deploy a lightweight API endpoint or static site first. Experiment and build familiarity step-by-step. Diving headfirst into a full migration risks drowning in configuration complexity, making issues difficult to pinpoint. --- ::div{className="text-sm,text-(--ui-text-muted)"} Special thanks to Daniel Roe for providing feedback on the Nuxt configuration section. Any (remaining) inaccuracies are my own. :: # Stop Learning Prompts, Build Expertise Instead Watch someone use AI for the first time and you'll see one of two reactions. They will either be stunned by what it can do and immediately start outsourcing their thinking to it, or they will get a bad result, call it hype, and write it off entirely. I've been watching smart people make these mistakes in opposite directions. Some expect AI to do their jobs for them. Others refuse to touch it at all. Both are just wrong. The problem isn't the tool. It's the mental model: *how people think about AI[\*]*. ## AI Has Memorized the Lecture To understand why both camps are wrong, we first need to understand what AI actually has: chauffeur knowledge. There's a story about Max Planck (probably apocryphal, but useful). After winning the Nobel Prize, he toured Germany giving the same lecture repeatedly. His chauffeur heard it so many times, he memorized it word-for-word. One day the chauffeur said, "Let me give the lecture. You wear my cap and sit in the audience." The chauffeur nailed the lecture. Then someone asked a question. "That's such a simple question," the chauffeur replied, "I'll let my chauffeur answer it." This is the difference between real knowledge and chauffeur knowledge: - **Real knowledge** has depth and nuance. It's flexible, adaptable. It applies to new situations. It understands the "why." - **Chauffeur knowledge** is surface-level. Pattern matching. It fails under pressure. Can't explain reasoning. AI has chauffeur knowledge. It's sophisticated pattern-matching at scale, an advanced autocomplete trained on massive data. This means it *sounds smart but lacks depth*. It can recite the lecture but can't answer the unexpected question. ## The Two Trends (and the Gap) Understanding chauffeur knowledge reveals what AI actually does well: - **Trend 1: Makes experts faster (and lets them go further)**: If you're already at 70/100 on something, AI takes you to 100 quickly and sometimes beyond what you could reach alone. Think of a senior developer with a copilot or an experienced writer with an editor. - **Trend 2: Makes beginners competent**: If you're at 0/100, AI gets you to 50 fast. Like a non-coder building a simple website or app (you've seen the demos on Twitter/X) or an amateur creating decent first drafts. - **The Gap: Zero to hero**: What AI doesn't do is take someone from 0 to 100. You still need foundational knowledge. You still need to understand the domain. You still need expertise. The all-in crowd expects AI to bridge the gap. It doesn't. They plateau at *mediocre* and often don't realize their output is mediocre because they lack the expertise to judge quality. The skeptics dismiss AI because because it's unreliable on its own. They miss that it does *something* valuable, if you bring expertise to amplify it. *Your expertise still matters* and you need to keep building it. You can't skip the hard part because **AI is a multiplier, not a replacement**. Without expertise, there's nothing to amplify. I've watched this play out many times with people early in their careers. They think AI lets them skip the hard work: the reading, the experimenting, the grunt work. It doesn't. And it's sad to watch because they don't realize they're trapping themselves in a **mediocrity loop**. No foundation to build on, no expertise to amplify, just endless 50% outputs. ## Five Mental Models for Thinking About AI If AI amplifies what you already have, you need better frameworks for thinking about it. Here are five mental models that changed how I think about AI. ### 1. High-IQ Intern Imagine a brilliant intern just walked in. Smart, eager, capable. But no context. No experience. Doesn't know your domain's quirks or your project's specifics. *That's AI.* It needs guidance, clear instructions, and, most importantly, *context you take for granted*. It'll do what you ask, but only if you're specific. Leave it unsupervised and it wanders into the weeds. ### 2. Prep Cook, Not Chef AI can chop onions. Prep ingredients. Start the sauce. It saves time on grunt work. But it can't design the menu. Can't judge the seasoning. Can't cook a great meal. *You're still the chef.* ### 3. Sophisticated Search Stop thinking you're chatting with AI. Behind the friendly chat interface, it's still token prediction. Next word, then next, then next. You're not conversing. **You're searching** through a massive database of patterns. Wrong keywords = wrong results. You can't blame Google for bad searches when you're not using the right keywords. Think of it like navigating to the right region in **knowledge space**. Your job is to prompt your way there. Give it the right signals so it autocompletes from the right place. ### 4. Roguelikes Each chat session is a fresh run. Sometimes you go down blind alleys and need to **restart**. Like roguelike games: *try, fail, retry*. But you keep what you learned between runs. When you hit a dead end, restart. Take the good parts, edit your previous prompts, start fresh. And save and reload strategically. ::callout{color="neutral" icon="i-lucide-triangle-alert"} Don't use one giant session. You're not *training* your model. You're just *bloating* context with noise. Context rot is real. Model performance drops as context length grows. :: ### 5. Context is King Prompts matter. But *context* matters more. It's not about the last message you send. It's about **managing the entire conversation space**. Your inputs are **grounded** (based on real data, domain knowledge, actual requirements). AI outputs are **ungrounded** (pattern-based guesses that need validation). Your job is to *keep the context grounded*. The quality of AI's output scales with the quality of your input context. ::callout{color="neutral" icon="i-lucide-lightbulb"} Pro tip: output quality follows a sigmoid curve. Small context improvements = big quality gains. But only up to a point. Then you hit diminishing returns. Know when to stop adding context and either move forward or restart. :: ## The Wrong Optimization Both camps make the same error: they don't understand **what AI amplifies**. The skeptics are right that it's unreliable. The believers are right that it's powerful. But neither sees that the power only works if you *bring something to the table*. This means *most people are optimizing for the wrong thing*. They're learning to prompt better when they should be building expertise. They're stuck at 50% wondering why AI isn't taking them further. The tool works. They just have nothing to amplify. *The skill isn't prompting. It's judgment.* --- ::div{className="text-sm,text-(--ui-text-muted)"} [\*] I don't love calling LLMs "AI", as they're a small part of a much broader field. But that's how most people talk about them now so for clarity, "AI" means large language models like GPT, Claude, and Gemini in this post. Much of my thinking on this topic has been shaped by [Jon Stokes' excellent writing on LLMs](https://www.jonstokes.com){rel="nofollow"}. His mental models of treating interacting with AI as search and roguelike sessions, grounded vs. ungrounded context, and the sigmoid curve of context quality have deeply influenced how I approach AI. If you found this post useful, his writing is essential reading. The chauffeur knowledge concept comes from [Sahil Bloom's post](https://x.com/SahilBloom/status/1951989953694519576){rel="nofollow"}, which crystallized something I'd been observing but couldn't name. [Oliver Kel's comprehensive guide on prompting LLMs](https://olickel.com/everything-i-know-about-prompting-llms){rel="nofollow"} informed my thinking vastly on prompting LLM models. ::