Introduction I needed a way to use Remix with Vite and Cloudflare Workers-Pages with minimal configuration. I saw other repositories, such as: The gospel stack, Girish21, Himorishige. But they had some limitations: I didn’t want to pre-build it, as I didn’t want to poison the repositories with more configuration files. Cloudflare Workers/Pages has a different target. It became tricky to target it with tsup, as packages such as Postgres would pull the node dependencies breaking when imported into Remix. I also needed a way to consume different targets (Remix-Cloudflare, Node/Bun) Nonetheless, I thank them, as they paved the way for making this possible! Be sure to read the pitfalls section at the bottom! Follow Me on Social! I’m building an automated testing platform in public to catch those 1% errors in production. I share my progress on: X/Twitter @javiasilis LinkedIn @javiasilis GitHub Repository You can access the full implementation here. Step-by-Step Requirements NodeJS PNPM Docker (Optional - For Local database example) Although this walks you through a new mono-repository, it is perfectly valid to transform an existing one into one. This will also assume that you have some mono repo knowledge. Note: “at root” refers to the beginning path of your monorepository. For this project, it will be outside the libs and packages directories. Install Turborepo Turborepo works on top of your package manager’s workspaces to manage your project’s scripts and outputs (it can even cache your output). So far, it is the only mono-repo tool besides Rush (Which I haven’t tried and don’t like) that is capable of working. NX doesn’t have Remix’s Vite support (as of this writing - Aug 28, 2024). https://turbo.build/ pnpm dlx create-turbo@latest 1. Configure PNPM Workspaces We will use PNPM’s workspace capabilities to manage dependencies. On your Monorepo directory, create a pnpm-workspace.yaml. Inside it, add: packages: - "apps/*" - "libs/*" This will tell pnpm that all the repositories will lie inside apps and libs. Note that using libs or packages (as you may have seen elsewhere) doesn’t matter. 2. Create an Empty package.json at the Root of the Project: pnpm init { "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" } Note the name:@repo/main This tells us that this is the main entry of the application. You don’t need to follow a particular convention or use the @ prefix. People use it to differentiate it from local/remote packages or to make it easy to group it into an organization. 3. Create the turbo.json File in the Root of the Project: { "$schema": "https://turbo.build/schema.json", "tasks": { "build": {}, "dev": { "cache": false, "persistent": true }, "start": { "dependsOn": ["^build"], "persistent": true }, "preview": { "cache": false, "persistent": true }, "db:migrate": {} } } The turbo.json file tells the turbo repo how to interpret our commands. Everything that lies inside the tasks key will match those found in the all package.json. Note that we define four commands. These match the ones in the script section of each repository's package.json. However, not all package.json must implement these commands. E.g: The dev command will be triggered by turbo dev , and it will execute all the packages which dev is found within package.json. If you don’t include it in turbo, it won’t execute. 4. Create an apps Folder in the Root of the Project mkdir apps 5. Create a Remix App in the apps Folder (Or Move an Existing One) npx create-remix --template edmundhung/remix-worker-template When it asks you to Install any dependencies with npm say no. 6. Rename the name of the package.json to @repo/my-remix-cloudflare-app (Or Your Name) { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" } 7. Copy the Dependencies and devDependencies From apps/<app>/package.json to the Root’s package.json E.g: <root>/package.json { "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@markdoc/markdoc": "^0.4.0", "@remix-run/cloudflare": "^2.8.1", "@remix-run/cloudflare-pages": "^2.8.1", "@remix-run/react": "^2.8.1", "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240222.0", "@octokit/types": "^12.6.0", "@playwright/test": "^1.42.1", "@remix-run/dev": "^2.8.1", "@remix-run/eslint-config": "^2.8.1", "@tailwindcss/typography": "^0.5.10", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "autoprefixer": "^10.4.18", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.0.11", "lint-staged": "^15.2.2", "msw": "^2.2.3", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.12", "rimraf": "^5.0.5", "tailwindcss": "^3.4.1", "typescript": "^5.4.2", "vite": "^5.1.5", "vite-tsconfig-paths": "^4.3.1", "wrangler": "^3.32.0" } } 8. Add Turbo as a devDependency at the Root Level (This Will Install All the Remix's Packages). Verify that turbo is inside package.json’s devDependencies. If it isn’t listed, execute the following command: pnpm add turbo -D -w The -w flag tells pnpm to install it at the workspace root. 9. Add the Following Entries to the Root package.json Add the dev command to scripts Add the packageManager to option { "name": "@repo/main", "version": "1.0.0", "scripts": { "dev": "turbo dev" }, "keywords": [], "author": "", "license": "ISC", "packageManager": "pnpm@9.1.0", "dependencies": { // omitted for brevity }, "devDependencies": { // omitted for brevity } } 10. Verify That Everything Is Working by Running pnpm dev pnpm dev 11. Create a Libs Folder at the Root of the Project. Add config, db, and utils: mkdir -p libs/config libs/db libs/utils 12. Add a src/index.ts for Each of the Packages. touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts The index.ts file will be used to export all the packages. We will use the folder as our entry point to make everything compact. This is a convention, and you can choose not to follow it. 13. Create an Empty package.json, and Add the Following to the libs/config/package.json File: { "name": "@repo/config", "version": "1.0.0", "type": "module", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } 14. Do the Same for libs/db/package.json: { "name": "@repo/db", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } 15. And libs/utils/package.json : { "name": "@repo/utils", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } We specify the “exports” field. This tells other repositories where to import the package from. We specify the “name” field. This is used to install the package and refer it to other repositories. 16. (DB) - Add Drizzle and Postgres Notes: I’m beginning to despise ORMs. I've spent over 10 years learning 6 different ones, and it’s knowledge you can’t transfer. You have problems when new technologies come out. Prisma doesn’t support Cloudflare workers out of the box. With LLMs, it’s easier than ever to write complex SQL queries. Learning SQL is a universal language and will not likely change. pnpm add drizzle-orm drizle-kit --filter=@repo/db Install Postgres at the workspace level. See the Pitfall section. pnma add postgres -w Notes: The --filter=@repo/db flag tells pnpm to add the package to the db repository. 17. Add the dotenv to the Workspace Repository pnpm add dotenv -w Notes The -w flag tells pnpm to install it at the root’s package.json 18. Add the Config Project to All the Projects. pnpm add @repo/config -r --filter=!@repo/config Notes: The -r flag tells pnpm to add the package to all the repositories. The --filter=! flag tells pnpm to exclude the config repository. Note the ! before the package name 19. (Optional) Do the Commands Above Don’t Work? Use .npmrc If pnpm is pulling the packages from the repository, we can create a .npmrc file at the root of the project. .npmrc link-workspace-packages= true prefer-workspace-packages=true This will tell pnpm to use the workspace packages first. Thanks to ZoWnx from Reddit, who helped me craft a .nprmc file 20. Configure the Shared tsconfig.json Inside Libs/Config Using the power of pnpm workspaces, you can create config files that can be shared across projects. We will create a base tsconfig.lib.json that we will use for our libraries. Inside libs/config instantiate a tsconfig.lib.json: touch "libs/config/tsconfig.base.lib.json" Then, add the following: tsconfig.base.lib.json { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "target": "ES2022", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "allowImportingTsExtensions": true, "allowJs": true, "noUncheckedIndexedAccess": true, "noEmit": true, "incremental": true, "composite": false, "declaration": true, "declarationMap": true, "inlineSources": false, "isolatedModules": true, "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "sourceMap": true, } } 21. Add the db Connection to the db Repository. // libs/db/drizzle.config.ts (Yes, this one is at root of the db package, outside the src folder) // We don't want to export this file as this is ran at setup. import "dotenv/config"; // make sure to install dotenv package import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", out: "./src/generated", schema: "./src/drizzle/schema.ts", dbCredentials: { url: process.env.DATABASE_URL!, }, // Print all statements verbose: true, // Always ask for confirmation strict: true, }); The schema file: // libs/db/src/drizzle/schema.ts export const User = pgTable("User", { userId: char("userId", { length: 26 }).primaryKey().notNull(), subId: char("subId", { length: 36 }).notNull(), // We are not making this unique to support merging accounts in later // iterations email: text("email"), loginProvider: loginProviderEnum("loginProvider").array().notNull(), createdAt: timestamp("createdAt", { precision: 3, mode: "date" }).notNull(), updatedAt: timestamp("updatedAt", { precision: 3, mode: "date" }).notNull(), }); The client file: // libs/db/src/drizzle-client.ts import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; export type DrizzleClient = PostgresJsDatabase<typeof schema>; let drizzleClient: DrizzleClient | undefined; type GetClientInput = { databaseUrl: string; env: string; mode?: "cloudflare" | "node"; }; declare var window: typeof globalThis; declare var self: typeof globalThis; export function getDrizzleClient(input: GetClientInput) { const { mode, env } = input; if (mode === "cloudflare") { return generateClient(input); } const globalObject = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : self; if (env === "production") { drizzleClient = generateClient(input); } else if (globalObject) { if (!(globalObject as any).__db__) { (globalObject as any).__db__ = generateClient(input); } drizzleClient = (globalObject as any).__db__; } else { drizzleClient = generateClient(input); } return drizzleClient; } type GenerateClientInput = { databaseUrl: string; env: string; }; function generateClient(input: GenerateClientInput) { const { databaseUrl, env } = input; const isLoggingEnabled = env === "development"; // prepare: false for serverless try { const client = postgres(databaseUrl, { prepare: false }); const db = drizzle(client, { schema, logger: isLoggingEnabled }); return db; } catch (e) { console.log("ERROR", e); return undefined!; } } The migration file: // libs/db/src/drizzle/migrate.ts import { config } from "dotenv"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import "dotenv/config"; import path from "path"; config({ path: "../../../../apps/my-remix-cloudflare-app/.dev.vars" }); const ssl = process.env.ENVIRONMENT === "development" ? undefined : "require"; const databaseUrl = drizzle( postgres(`${process.env.DATABASE_URL}`, { ssl, max: 1 }) ); // Somehow the current starting path is /libs/db // Remember to have the DB running before running this script const migration = path.resolve("./src/generated"); const main = async () => { try { await migrate(databaseUrl, { migrationsFolder: migration, }); console.log("Migration complete"); } catch (error) { console.log(error); } process.exit(0); }; main(); This should be executed after the migrations And export the client and schema in the src/index.ts file. Others are run at specific times. // libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema " In your package.json, add the drizzle-kit generate , and the code to run the migration command: { "name": "@repo/db", "version": "1.0.0", "main": "./src/index.ts", "module": "./src/index.ts", "types": "./src/index.ts", "scripts": { "db:generate": "drizzle-kit generate", "db:migrate": "dotenv tsx ./drizzle/migrate", }, "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } }, "dependencies": { "@repo/configs": "workspace:^", "drizzle-kit": "^0.24.1", "drizzle-orm": "^0.33.0", }, "devDependencies": { "@types/node": "^22.5.0" } } 22. Use the Shared tsconfig.json for libs/db and libs/utils Create a tsconfig.json for libs/db and libs/utils touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json" Then add to each: { "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], } See that @repo/configs is used as the path to refer to our tsconfig.base.lib.json. It makes our path clean. 23. Install TSX TypeScript Execute (TSX) is a library alternative to ts-node. We will use this to execute the drizzle’s migrations. pnpm add tsx -D --filter=@repo/db 24. Add an Empty .env at the libs/db Directory touch "libs/db/.env" Add the following contents: DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node" 25. Add the libs/db Repository to Our Remix Project From the root of the project, run: pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app If this doesn't work, then go to the apps/my-remix-cloudflare-app's package.json, and add the dependency manually. { "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } } Note the workspace:* in the version field. This tells pnpm to use any version of the package in the workspace. If you installed it via the CLI by using pnpm add, you will probably see something like workspace:^. It shouldn’t matter as long as you don’t increase the local package versions. If you did add this manually, then run pnpm install from the root of the project. We should be able to consume the @repo/db in our project. 26. Add Some Shared Code to Our Utils: Add this code to the libs/utils/src/index.ts file: // libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; } 27. Install the Libs/Utils to our Remix App: pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app 28. (Optional) Launch a Postgres From a Docker Container If you don’t have a Postgres instance running, we can launch one using docker-compose. Note, I am assuming you know Docker. Create a docker-compose.yml file at the root of the project. # Auto-generated docker-compose.yml file. version: '3.8' # Define services. services: postgres: image: postgres:latest restart: always environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres ports: - "5432:5432" volumes: - ./postgres-data:/var/lib/postgresql/data pgadmin: # To connect PG Admin, navigate to http://localhost:8500 use: # host.docker.internal # postgres # (username) postgres # (password) postgres image: dpage/pgadmin4 ports: - "8500:80" environment: PGADMIN_DEFAULT_EMAIL: admin@a.com PGADMIN_DEFAULT_PASSWORD: admin Then you can run: docker-compose up -d The -d flag tells docker-compose to run detached so you can have access to your terminal again. 29. Generate the DB Schema Now, navigate to the libs/db repository and run db:generate. cd `./libs/db` && pnpm db:generate Note that the db:generate is an alias for: drizzle-kit generate Verify that you have the proper .env. Additionally, this assumes that you have a Postgres instance running. 30. Run the Migrations. We need to run the migrations to scaffold all the tables within our database. Navigate to the libs/db repository (if you’re not there) and run db:generate. cd `./libs/db` && pnpm db:migrate Note that the db:migrate is an alias for: dotenv tsx ./drizzle/migrate Verify that you have the proper .env. Additionally, this assumes that you have a Postgres instance running. 31. Insert a DB Call Inside Your Remix App. // apps/my-remix-cloudflare-app/app/routes/_index.tsx import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; import { json, useLoaderData } from '@remix-run/react'; import { getDrizzleClient } from '@repo/db'; import { Markdown } from '~/components'; import { getFileContentWithCache } from '~/services/github.server'; import { parse } from '~/services/markdoc.server'; export async function loader({ context }: LoaderFunctionArgs) { const client = await getDrizzleClient({ databaseUrl: context.env.DATABASE_URL, env: 'development', mode: 'cloudflare', }); if (client) { const res = await client.query.User.findFirst(); console.log('res', res); } const content = await getFileContentWithCache(context, 'README.md'); return json( { content: parse(content), // user: firstUser, }, { headers: { 'Cache-Control': 'public, max-age=3600', }, }, ); } export default function Index() { const { content } = useLoaderData<typeof loader>(); return <Markdown content={content} />; } Note that we’re not following best practices here. I’d advise you not to make any DB calls directly within your loader, but create an abstraction that calls them. Cloudflare is challenging when it comes to setting the environment variables. They’re passed by request 32. Add in Your .dev.vars the Following: apps/my-remix-cloudflare-app/.dev.vars DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" 33. Execute the Remix Project! Launch the postgres instance (if not ready) docker-compose up -d Launch the project pnpm turbo dev Advanced Use Case - CQRS in GetLoadContext in Cloudflare Workers. In my projects, I tend to implement a CQRS pattern, 2. This is outside of the scope of this tutorial. Nonetheless, within the load context, I tend to inject a mediator (and a cookie flash message) that will decouple my entire Remix Application from my business logic. This looks something like this: export const getLoadContext: GetLoadContext = async ({ context, request }) => { const isEnvEmpty = Object.keys(context.cloudflare.env).length === 0; const env = isEnvEmpty ? process.env : context.cloudflare.env; const sessionFlashSecret = env.SESSION_FLASH_SECRET; const flashStorage = createCookieSessionStorage({ cookie: { name: "__flash", httpOnly: true, maxAge: 60, path: "/", sameSite: "lax", secrets: [sessionFlashSecret], secure: true, }, }); return { ...context, cloudflare: { ...context.cloudflare, env, }, dispatch: (await dispatchWithContext({ env: env as unknown as Record<string, string>, request, })) as Dispatch, flashStorage, }; }; Note that the dispatch code is omitted. You can find more about it in my article on how to 10x your TypeScript dev experience here. I can strip out Remix or use another consumer without altering my code. But…. There is an additional challenge when you work in a monorepo structure using turborepo. If you import a TypeScript file from a package within the load-context, let's say @repo/db Vite will return an error that the file with extension .ts is unknown, and will not know how to process it. This happens because load-context + workspaces are outside the site’s main importing graph, leaving TypeScript files outside of play. The trick is to use tsx and load it before calling Vite, which will work. This is important because it overcomes the following limitations: Cloudflare Package Dependencies. Cloudflare Package Dependencies and Pre-building First of all, that was the step that I was trying to avoid, as it meant that I had to introduce a build step for each of the packages, which meant more configuration. Fortunately, this didn’t work for Cloudflare Pages. Specific libraries, such as Postgres, will detect the runtime and pull the required package. There’s a workaround: We can use tsx to load all the TypeScript files and transpile them before we execute. You can argue this is a pre-build step, but since it’s still at the remix’s repository level, I don’t see significant issues with this approach. To solve this, we add tsx as a dependency: pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app And then, we need to modify our package.json and add the tsx process to each one of our remix scripts: { "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "scripts": { // Other scripts omitted "build": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:build", "dev": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:dev", "start": "NODE_OPTIONS=\"--import tsx/esm\" wrangler pages dev ./build/client" } } Extras Creating a .npmrc File In case you're having issues while adding your local packages with the command line, you can create a .npmrc file in the root of the project. .npmrc link-workspace-packages= true prefer-workspace-packages=true This will tell pnpm to use the workspace packages first. Thanks to ZoWnx from Reddit who helped me craft a .nprmc file Pitfalls - Careful with naming .client and .server in your files. Even if it's in a separate library. Remix uses these to determine if it's a client or server file. The project isn't compiled per repository so it will throw an import error! If you’re having problems with multi-platform packages such as Postgres, installing it at the workspace level is better. It will detect the proper import. Installing it directly in the @repo/db repository will break when importing it to Remix. That’s it, folks!!! GitHub Repository You can access the full implementation here. Follow Me on Social! I’m building an automated testing engineer in public to catch those 1% errors in production. I share my progress on: X/Twitter @javiasilis LinkedIn @javiasilis Introduction I needed a way to use Remix with Vite and Cloudflare Workers-Pages with minimal configuration. I saw other repositories, such as: The gospel stack, Girish21, Himorishige. The gospel stack , gospel stack Girish21 , Girish21 Himorishige . Himorishige But they had some limitations: I didn’t want to pre-build it, as I didn’t want to poison the repositories with more configuration files. Cloudflare Workers/Pages has a different target. It became tricky to target it with tsup, as packages such as Postgres would pull the node dependencies breaking when imported into Remix. I also needed a way to consume different targets (Remix-Cloudflare, Node/Bun) I didn’t want to pre-build it, as I didn’t want to poison the repositories with more configuration files. I didn’t want to pre-build it, as I didn’t want to poison the repositories with more configuration files. Cloudflare Workers/Pages has a different target. It became tricky to target it with tsup, as packages such as Postgres would pull the node dependencies breaking when imported into Remix. Cloudflare Workers/Pages has a different target. It became tricky to target it with tsup, as packages such as Postgres would pull the node dependencies breaking when imported into Remix. I also needed a way to consume different targets (Remix-Cloudflare, Node/Bun) I also needed a way to consume different targets (Remix-Cloudflare, Node/Bun) Nonetheless, I thank them, as they paved the way for making this possible! Be sure to read the pitfalls section at the bottom! Be sure to read the pitfalls section at the bottom! Follow Me on Social! Follow Me on Social! I’m building an automated testing platform in public to catch those 1% errors in production. I share my progress on: X/Twitter @javiasilis X/Twitter @javiasilis LinkedIn @javiasilis LinkedIn @javiasilis GitHub Repository You can access the full implementation here . full implementation here Step-by-Step Requirements NodeJS PNPM Docker (Optional - For Local database example) NodeJS PNPM Docker (Optional - For Local database example) Although this walks you through a new mono-repository, it is perfectly valid to transform an existing one into one. This will also assume that you have some mono repo knowledge. Note: Note: “at root” refers to the beginning path of your monorepository. For this project, it will be outside the libs and packages directories. “at root” refers to the beginning path of your monorepository. For this project, it will be outside the libs and packages directories. libs packages Install Turborepo Turborepo works on top of your package manager’s workspaces to manage your project’s scripts and outputs (it can even cache your output). So far, it is the only mono-repo tool besides Rush (Which I haven’t tried and don’t like) that is capable of working. NX doesn’t have Remix’s Vite support (as of this writing - Aug 28, 2024). https://turbo.build/ https://turbo.build/ pnpm dlx create-turbo@latest pnpm dlx create-turbo@latest 1. Configure PNPM Workspaces We will use PNPM’s workspace capabilities to manage dependencies. On your Monorepo directory, create a pnpm-workspace.yaml . pnpm-workspace.yaml Inside it, add: packages: - "apps/*" - "libs/*" packages: - "apps/*" - "libs/*" This will tell pnpm that all the repositories will lie inside apps and libs . Note that using libs or packages (as you may have seen elsewhere) doesn’t matter. apps libs libs packages 2. Create an Empty package.json at the Root of the Project: pnpm init pnpm init { "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" } { "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC" } Note the name:@repo/main This tells us that this is the main entry of the application. You don’t need to follow a particular convention or use the @ prefix. People use it to differentiate it from local/remote packages or to make it easy to group it into an organization. name:@repo/main @ 3. Create the turbo.json File in the Root of the Project: turbo.json { "$schema": "https://turbo.build/schema.json", "tasks": { "build": {}, "dev": { "cache": false, "persistent": true }, "start": { "dependsOn": ["^build"], "persistent": true }, "preview": { "cache": false, "persistent": true }, "db:migrate": {} } } { "$schema": "https://turbo.build/schema.json", "tasks": { "build": {}, "dev": { "cache": false, "persistent": true }, "start": { "dependsOn": ["^build"], "persistent": true }, "preview": { "cache": false, "persistent": true }, "db:migrate": {} } } The turbo.json file tells the turbo repo how to interpret our commands. Everything that lies inside the tasks key will match those found in the all package.json. tasks Note that we define four commands. These match the ones in the script section of each repository's package.json. However, not all package.json must implement these commands. E.g: The dev command will be triggered by turbo dev , and it will execute all the packages which dev is found within package.json. If you don’t include it in turbo, it won’t execute. dev turbo dev dev 4. Create an apps Folder in the Root of the Project apps mkdir apps mkdir apps 5. Create a Remix App in the apps Folder (Or Move an Existing One) apps npx create-remix --template edmundhung/remix-worker-template npx create-remix --template edmundhung/remix-worker-template When it asks you to Install any dependencies with npm say no. Install any dependencies with npm 6. Rename the name of the package.json to @repo/my-remix-cloudflare-app (Or Your Name) name @repo/my-remix-cloudflare-app { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" } { - "name": "my-remix-cloudflare-app", + "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "keywords": [], "author": "", "license": "ISC" } 7. Copy the Dependencies and devDependencies From apps/<app>/package.json to the Root’s package.json apps/<app>/package.json package.json E.g: <root>/package.json { "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@markdoc/markdoc": "^0.4.0", "@remix-run/cloudflare": "^2.8.1", "@remix-run/cloudflare-pages": "^2.8.1", "@remix-run/react": "^2.8.1", "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240222.0", "@octokit/types": "^12.6.0", "@playwright/test": "^1.42.1", "@remix-run/dev": "^2.8.1", "@remix-run/eslint-config": "^2.8.1", "@tailwindcss/typography": "^0.5.10", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "autoprefixer": "^10.4.18", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.0.11", "lint-staged": "^15.2.2", "msw": "^2.2.3", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.12", "rimraf": "^5.0.5", "tailwindcss": "^3.4.1", "typescript": "^5.4.2", "vite": "^5.1.5", "vite-tsconfig-paths": "^4.3.1", "wrangler": "^3.32.0" } } { "name": "@repo/main", "version": "1.0.0", "scripts": {}, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@markdoc/markdoc": "^0.4.0", "@remix-run/cloudflare": "^2.8.1", "@remix-run/cloudflare-pages": "^2.8.1", "@remix-run/react": "^2.8.1", "isbot": "^3.6.5", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@cloudflare/workers-types": "^4.20240222.0", "@octokit/types": "^12.6.0", "@playwright/test": "^1.42.1", "@remix-run/dev": "^2.8.1", "@remix-run/eslint-config": "^2.8.1", "@tailwindcss/typography": "^0.5.10", "@types/react": "^18.2.64", "@types/react-dom": "^18.2.21", "autoprefixer": "^10.4.18", "concurrently": "^8.2.2", "cross-env": "^7.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.0.11", "lint-staged": "^15.2.2", "msw": "^2.2.3", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-tailwindcss": "^0.5.12", "rimraf": "^5.0.5", "tailwindcss": "^3.4.1", "typescript": "^5.4.2", "vite": "^5.1.5", "vite-tsconfig-paths": "^4.3.1", "wrangler": "^3.32.0" } } 8. Add Turbo as a devDependency at the Root Level (This Will Install All the Remix's Packages). Verify that turbo is inside package.json’s devDependencies. If it isn’t listed, execute the following command: pnpm add turbo -D -w pnpm add turbo -D -w The -w flag tells pnpm to install it at the workspace root. -w 9. Add the Following Entries to the Root package.json Add the dev command to scripts Add the packageManager to option Add the dev command to scripts Add the dev command to scripts dev scripts Add the packageManager to option Add the packageManager to option packageManager { "name": "@repo/main", "version": "1.0.0", "scripts": { "dev": "turbo dev" }, "keywords": [], "author": "", "license": "ISC", "packageManager": "pnpm@9.1.0", "dependencies": { // omitted for brevity }, "devDependencies": { // omitted for brevity } } { "name": "@repo/main", "version": "1.0.0", "scripts": { "dev": "turbo dev" }, "keywords": [], "author": "", "license": "ISC", "packageManager": "pnpm@9.1.0", "dependencies": { // omitted for brevity }, "devDependencies": { // omitted for brevity } } 10. Verify That Everything Is Working by Running pnpm dev pnpm dev pnpm dev pnpm dev 11. Create a Libs Folder at the Root of the Project. Add config, db, and utils: mkdir -p libs/config libs/db libs/utils mkdir -p libs/config libs/db libs/utils 12. Add a src/index.ts for Each of the Packages. src/index.ts touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts touch libs/config/src/index.ts libs/db/src/index.ts libs/utils/src/index.ts The index.ts file will be used to export all the packages. We will use the folder as our entry point to make everything compact. This is a convention, and you can choose not to follow it. The index.ts file will be used to export all the packages. We will use the folder as our entry point to make everything compact. This is a convention, and you can choose not to follow it. 13. Create an Empty package.json, and Add the Following to the libs/config/package.json File: libs/config/package.json { "name": "@repo/config", "version": "1.0.0", "type": "module", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } { "name": "@repo/config", "version": "1.0.0", "type": "module", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } 14. Do the Same for libs/db/package.json : libs/db/package.json { "name": "@repo/db", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } { "name": "@repo/db", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } 15. And libs/utils/package.json : libs/utils/package.json { "name": "@repo/utils", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } { "name": "@repo/utils", "version": "1.0.0", "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } } } We specify the “exports” field. This tells other repositories where to import the package from. We specify the “name” field. This is used to install the package and refer it to other repositories. We specify the “exports” field. This tells other repositories where to import the package from. We specify the “name” field. This is used to install the package and refer it to other repositories. 16. (DB) - Add Drizzle and Postgres Notes: Notes: I’m beginning to despise ORMs. I've spent over 10 years learning 6 different ones, and it’s knowledge you can’t transfer. You have problems when new technologies come out. Prisma doesn’t support Cloudflare workers out of the box. With LLMs, it’s easier than ever to write complex SQL queries. Learning SQL is a universal language and will not likely change. I’m beginning to despise ORMs. I've spent over 10 years learning 6 different ones, and it’s knowledge you can’t transfer. I’m beginning to despise ORMs. I've spent over 10 years learning 6 different ones, and it’s knowledge you can’t transfer. You have problems when new technologies come out. Prisma doesn’t support Cloudflare workers out of the box. You have problems when new technologies come out. Prisma doesn’t support Cloudflare workers out of the box. With LLMs, it’s easier than ever to write complex SQL queries. With LLMs, it’s easier than ever to write complex SQL queries. Learning SQL is a universal language and will not likely change. Learning SQL is a universal language and will not likely change. pnpm add drizzle-orm drizle-kit --filter=@repo/db pnpm add drizzle-orm drizle-kit --filter=@repo/db Install Postgres at the workspace level. See the Pitfall section . See the Pitfall section pnma add postgres -w pnma add postgres -w Notes: Notes: The --filter=@repo/db flag tells pnpm to add the package to the db repository. The --filter=@repo/db flag tells pnpm to add the package to the db repository. --filter=@repo/db 17. Add the dotenv to the Workspace Repository pnpm add dotenv -w pnpm add dotenv -w Notes The -w flag tells pnpm to install it at the root’s package.json The -w flag tells pnpm to install it at the root’s package.json -w 18. Add the Config Project to All the Projects. pnpm add @repo/config -r --filter=!@repo/config pnpm add @repo/config -r --filter=!@repo/config Notes : Notes The -r flag tells pnpm to add the package to all the repositories. The --filter=! flag tells pnpm to exclude the config repository. Note the ! before the package name The -r flag tells pnpm to add the package to all the repositories. -r The --filter=! flag tells pnpm to exclude the config repository. --filter=! Note the ! before the package name ! 19. (Optional) Do the Commands Above Don’t Work? Use .npmrc 19. (Optional) Do the Commands Above Don’t Work? Use .npmrc If pnpm is pulling the packages from the repository, we can create a .npmrc file at the root of the project. .npmrc .npmrc link-workspace-packages= true prefer-workspace-packages=true link-workspace-packages= true prefer-workspace-packages=true This will tell pnpm to use the workspace packages first. Thanks to ZoWnx from Reddit, who helped me craft a .nprmc file This will tell pnpm to use the workspace packages first. Thanks to ZoWnx from Reddit, who helped me craft a .nprmc file ZoWnx 20. Configure the Shared tsconfig.json Inside Libs/Config tsconfig.json Using the power of pnpm workspaces, you can create config files that can be shared across projects. We will create a base tsconfig.lib.json that we will use for our libraries. Inside libs/config instantiate a tsconfig.lib.json : libs/config tsconfig.lib.json touch "libs/config/tsconfig.base.lib.json" touch "libs/config/tsconfig.base.lib.json" Then, add the following: tsconfig.base.lib.json { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "target": "ES2022", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "allowImportingTsExtensions": true, "allowJs": true, "noUncheckedIndexedAccess": true, "noEmit": true, "incremental": true, "composite": false, "declaration": true, "declarationMap": true, "inlineSources": false, "isolatedModules": true, "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "sourceMap": true, } } { "$schema": "https://json.schemastore.org/tsconfig", "compilerOptions": { "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "Bundler", "resolveJsonModule": true, "target": "ES2022", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "allowImportingTsExtensions": true, "allowJs": true, "noUncheckedIndexedAccess": true, "noEmit": true, "incremental": true, "composite": false, "declaration": true, "declarationMap": true, "inlineSources": false, "isolatedModules": true, "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, "sourceMap": true, } } 21. Add the db Connection to the db Repository. // libs/db/drizzle.config.ts (Yes, this one is at root of the db package, outside the src folder) // We don't want to export this file as this is ran at setup. import "dotenv/config"; // make sure to install dotenv package import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", out: "./src/generated", schema: "./src/drizzle/schema.ts", dbCredentials: { url: process.env.DATABASE_URL!, }, // Print all statements verbose: true, // Always ask for confirmation strict: true, }); // libs/db/drizzle.config.ts (Yes, this one is at root of the db package, outside the src folder) // We don't want to export this file as this is ran at setup. import "dotenv/config"; // make sure to install dotenv package import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", out: "./src/generated", schema: "./src/drizzle/schema.ts", dbCredentials: { url: process.env.DATABASE_URL!, }, // Print all statements verbose: true, // Always ask for confirmation strict: true, }); The schema file: // libs/db/src/drizzle/schema.ts export const User = pgTable("User", { userId: char("userId", { length: 26 }).primaryKey().notNull(), subId: char("subId", { length: 36 }).notNull(), // We are not making this unique to support merging accounts in later // iterations email: text("email"), loginProvider: loginProviderEnum("loginProvider").array().notNull(), createdAt: timestamp("createdAt", { precision: 3, mode: "date" }).notNull(), updatedAt: timestamp("updatedAt", { precision: 3, mode: "date" }).notNull(), }); // libs/db/src/drizzle/schema.ts export const User = pgTable("User", { userId: char("userId", { length: 26 }).primaryKey().notNull(), subId: char("subId", { length: 36 }).notNull(), // We are not making this unique to support merging accounts in later // iterations email: text("email"), loginProvider: loginProviderEnum("loginProvider").array().notNull(), createdAt: timestamp("createdAt", { precision: 3, mode: "date" }).notNull(), updatedAt: timestamp("updatedAt", { precision: 3, mode: "date" }).notNull(), }); The client file: // libs/db/src/drizzle-client.ts import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; export type DrizzleClient = PostgresJsDatabase<typeof schema>; let drizzleClient: DrizzleClient | undefined; type GetClientInput = { databaseUrl: string; env: string; mode?: "cloudflare" | "node"; }; declare var window: typeof globalThis; declare var self: typeof globalThis; export function getDrizzleClient(input: GetClientInput) { const { mode, env } = input; if (mode === "cloudflare") { return generateClient(input); } const globalObject = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : self; if (env === "production") { drizzleClient = generateClient(input); } else if (globalObject) { if (!(globalObject as any).__db__) { (globalObject as any).__db__ = generateClient(input); } drizzleClient = (globalObject as any).__db__; } else { drizzleClient = generateClient(input); } return drizzleClient; } type GenerateClientInput = { databaseUrl: string; env: string; }; function generateClient(input: GenerateClientInput) { const { databaseUrl, env } = input; const isLoggingEnabled = env === "development"; // prepare: false for serverless try { const client = postgres(databaseUrl, { prepare: false }); const db = drizzle(client, { schema, logger: isLoggingEnabled }); return db; } catch (e) { console.log("ERROR", e); return undefined!; } } // libs/db/src/drizzle-client.ts import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import * as schema from "./schema"; export type DrizzleClient = PostgresJsDatabase<typeof schema>; let drizzleClient: DrizzleClient | undefined; type GetClientInput = { databaseUrl: string; env: string; mode?: "cloudflare" | "node"; }; declare var window: typeof globalThis; declare var self: typeof globalThis; export function getDrizzleClient(input: GetClientInput) { const { mode, env } = input; if (mode === "cloudflare") { return generateClient(input); } const globalObject = typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : self; if (env === "production") { drizzleClient = generateClient(input); } else if (globalObject) { if (!(globalObject as any).__db__) { (globalObject as any).__db__ = generateClient(input); } drizzleClient = (globalObject as any).__db__; } else { drizzleClient = generateClient(input); } return drizzleClient; } type GenerateClientInput = { databaseUrl: string; env: string; }; function generateClient(input: GenerateClientInput) { const { databaseUrl, env } = input; const isLoggingEnabled = env === "development"; // prepare: false for serverless try { const client = postgres(databaseUrl, { prepare: false }); const db = drizzle(client, { schema, logger: isLoggingEnabled }); return db; } catch (e) { console.log("ERROR", e); return undefined!; } } The migration file: // libs/db/src/drizzle/migrate.ts import { config } from "dotenv"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import "dotenv/config"; import path from "path"; config({ path: "../../../../apps/my-remix-cloudflare-app/.dev.vars" }); const ssl = process.env.ENVIRONMENT === "development" ? undefined : "require"; const databaseUrl = drizzle( postgres(`${process.env.DATABASE_URL}`, { ssl, max: 1 }) ); // Somehow the current starting path is /libs/db // Remember to have the DB running before running this script const migration = path.resolve("./src/generated"); const main = async () => { try { await migrate(databaseUrl, { migrationsFolder: migration, }); console.log("Migration complete"); } catch (error) { console.log(error); } process.exit(0); }; main(); // libs/db/src/drizzle/migrate.ts import { config } from "dotenv"; import { migrate } from "drizzle-orm/postgres-js/migrator"; import postgres from "postgres"; import { drizzle } from "drizzle-orm/postgres-js"; import "dotenv/config"; import path from "path"; config({ path: "../../../../apps/my-remix-cloudflare-app/.dev.vars" }); const ssl = process.env.ENVIRONMENT === "development" ? undefined : "require"; const databaseUrl = drizzle( postgres(`${process.env.DATABASE_URL}`, { ssl, max: 1 }) ); // Somehow the current starting path is /libs/db // Remember to have the DB running before running this script const migration = path.resolve("./src/generated"); const main = async () => { try { await migrate(databaseUrl, { migrationsFolder: migration, }); console.log("Migration complete"); } catch (error) { console.log(error); } process.exit(0); }; main(); This should be executed after the migrations And export the client and schema in the src/index.ts file. Others are run at specific times. src/index.ts // libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema " // libs/db/src/index.ts export * from "./drizzle/drizzle-client"; export * from "./drizzle/schema " In your package.json , add the drizzle-kit generate , and the code to run the migration command: package.json drizzle-kit generate { "name": "@repo/db", "version": "1.0.0", "main": "./src/index.ts", "module": "./src/index.ts", "types": "./src/index.ts", "scripts": { "db:generate": "drizzle-kit generate", "db:migrate": "dotenv tsx ./drizzle/migrate", }, "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } }, "dependencies": { "@repo/configs": "workspace:^", "drizzle-kit": "^0.24.1", "drizzle-orm": "^0.33.0", }, "devDependencies": { "@types/node": "^22.5.0" } } { "name": "@repo/db", "version": "1.0.0", "main": "./src/index.ts", "module": "./src/index.ts", "types": "./src/index.ts", "scripts": { "db:generate": "drizzle-kit generate", "db:migrate": "dotenv tsx ./drizzle/migrate", }, "exports": { ".": { "import": "./src/index.ts", "default": "./src/index.ts" } }, "dependencies": { "@repo/configs": "workspace:^", "drizzle-kit": "^0.24.1", "drizzle-orm": "^0.33.0", }, "devDependencies": { "@types/node": "^22.5.0" } } 22. Use the Shared tsconfig.json for libs/db and libs/utils tsconfig.json libs/db libs/utils Create a tsconfig.json for libs/db and libs/utils libs/db libs/utils touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json" touch "libs/db/tsconfig.json" "libs/utils/tsconfig.json" Then add to each: { "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], } { "extends": "@repo/configs/tsconfig.base.lib.json", "include": ["./src"], } See that @repo/configs is used as the path to refer to our tsconfig.base.lib.json. It makes our path clean. See that @repo/configs is used as the path to refer to our tsconfig.base.lib.json. @repo/configs It makes our path clean. 23. Install TSX TypeScript Execute (TSX) is a library alternative to ts-node. We will use this to execute the drizzle’s migrations. pnpm add tsx -D --filter=@repo/db pnpm add tsx -D --filter=@repo/db 24. Add an Empty .env at the libs/db Directory libs/db touch "libs/db/.env" touch "libs/db/.env" Add the following contents: DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node" DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" NODE_ENV="development" MODE="node" 25. Add the libs/db Repository to Our Remix Project libs/db From the root of the project, run: pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app If this doesn't work, then go to the apps/my-remix-cloudflare-app 's package.json, and add the dependency manually. apps/my-remix-cloudflare-app { "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } } { "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "dependencies": { "@repo/db": "workspace:*" } } Note the workspace:* in the version field. This tells pnpm to use any version of the package in the workspace. workspace:* If you installed it via the CLI by using pnpm add, you will probably see something like workspace:^ . It shouldn’t matter as long as you don’t increase the local package versions. pnpm add, workspace:^ If you did add this manually, then run pnpm install from the root of the project. pnpm install We should be able to consume the @repo/db in our project. 26. Add Some Shared Code to Our Utils: Add this code to the libs/utils/src/index.ts file: libs/utils/src/index.ts // libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; } // libs/utils/src/index.ts export function hellowWorld() { return "Hello World!"; } 27. Install the Libs/Utils to our Remix App: pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app pnpm add @repo/db --filter=@repo/my-remix-cloudflare-app 28. (Optional) Launch a Postgres From a Docker Container If you don’t have a Postgres instance running, we can launch one using docker-compose. Note, I am assuming you know Docker. Create a docker-compose.yml file at the root of the project. docker-compose.yml # Auto-generated docker-compose.yml file. version: '3.8' # Define services. services: postgres: image: postgres:latest restart: always environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres ports: - "5432:5432" volumes: - ./postgres-data:/var/lib/postgresql/data pgadmin: # To connect PG Admin, navigate to http://localhost:8500 use: # host.docker.internal # postgres # (username) postgres # (password) postgres image: dpage/pgadmin4 ports: - "8500:80" environment: PGADMIN_DEFAULT_EMAIL: admin@a.com PGADMIN_DEFAULT_PASSWORD: admin # Auto-generated docker-compose.yml file. version: '3.8' # Define services. services: postgres: image: postgres:latest restart: always environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres ports: - "5432:5432" volumes: - ./postgres-data:/var/lib/postgresql/data pgadmin: # To connect PG Admin, navigate to http://localhost:8500 use: # host.docker.internal # postgres # (username) postgres # (password) postgres image: dpage/pgadmin4 ports: - "8500:80" environment: PGADMIN_DEFAULT_EMAIL: admin@a.com PGADMIN_DEFAULT_PASSWORD: admin Then you can run: docker-compose up -d docker-compose up -d The -d flag tells docker-compose to run detached so you can have access to your terminal again. -d 29. Generate the DB Schema Now, navigate to the libs/db repository and run db:generate . db:generate cd `./libs/db` && pnpm db:generate cd `./libs/db` && pnpm db:generate Note that the db:generate is an alias for: drizzle-kit generate Verify that you have the proper .env. Additionally, this assumes that you have a Postgres instance running. Note that the db:generate is an alias for: drizzle-kit generate db:generate drizzle-kit generate Verify that you have the proper .env. Additionally, this assumes that you have a Postgres instance running. 30. Run the Migrations. We need to run the migrations to scaffold all the tables within our database. Navigate to the libs/db repository (if you’re not there) and run db:generate . db:generate cd `./libs/db` && pnpm db:migrate cd `./libs/db` && pnpm db:migrate Note that the db:migrate is an alias for: dotenv tsx ./drizzle/migrate Verify that you have the proper .env. Additionally, this assumes that you have a Postgres instance running. Note that the db:migrate is an alias for: dotenv tsx ./drizzle/migrate db:migrate dotenv tsx ./drizzle/migrate Verify that you have the proper .env. Additionally, this assumes that you have a Postgres instance running. 31. Insert a DB Call Inside Your Remix App. // apps/my-remix-cloudflare-app/app/routes/_index.tsx import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; import { json, useLoaderData } from '@remix-run/react'; import { getDrizzleClient } from '@repo/db'; import { Markdown } from '~/components'; import { getFileContentWithCache } from '~/services/github.server'; import { parse } from '~/services/markdoc.server'; export async function loader({ context }: LoaderFunctionArgs) { const client = await getDrizzleClient({ databaseUrl: context.env.DATABASE_URL, env: 'development', mode: 'cloudflare', }); if (client) { const res = await client.query.User.findFirst(); console.log('res', res); } const content = await getFileContentWithCache(context, 'README.md'); return json( { content: parse(content), // user: firstUser, }, { headers: { 'Cache-Control': 'public, max-age=3600', }, }, ); } export default function Index() { const { content } = useLoaderData<typeof loader>(); return <Markdown content={content} />; } // apps/my-remix-cloudflare-app/app/routes/_index.tsx import type { LoaderFunctionArgs } from '@remix-run/cloudflare'; import { json, useLoaderData } from '@remix-run/react'; import { getDrizzleClient } from '@repo/db'; import { Markdown } from '~/components'; import { getFileContentWithCache } from '~/services/github.server'; import { parse } from '~/services/markdoc.server'; export async function loader({ context }: LoaderFunctionArgs) { const client = await getDrizzleClient({ databaseUrl: context.env.DATABASE_URL, env: 'development', mode: 'cloudflare', }); if (client) { const res = await client.query.User.findFirst(); console.log('res', res); } const content = await getFileContentWithCache(context, 'README.md'); return json( { content: parse(content), // user: firstUser, }, { headers: { 'Cache-Control': 'public, max-age=3600', }, }, ); } export default function Index() { const { content } = useLoaderData<typeof loader>(); return <Markdown content={content} />; } Note that we’re not following best practices here. I’d advise you not to make any DB calls directly within your loader, but create an abstraction that calls them. Cloudflare is challenging when it comes to setting the environment variables. They’re passed by request Note that we’re not following best practices here. I’d advise you not to make any DB calls directly within your loader, but create an abstraction that calls them. Cloudflare is challenging when it comes to setting the environment variables. They’re passed by request 32. Add in Your .dev.vars the Following: apps/my-remix-cloudflare-app/.dev.vars DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" 33. Execute the Remix Project! Launch the postgres instance (if not ready) docker-compose up -d docker-compose up -d Launch the project pnpm turbo dev pnpm turbo dev Advanced Use Case - CQRS in GetLoadContext in Cloudflare Workers. In my projects, I tend to implement a CQRS pattern , 2 . This is outside of the scope of this tutorial. CQRS pattern 2 Nonetheless, within the load context, I tend to inject a mediator (and a cookie flash message) that will decouple my entire Remix Application from my business logic. This looks something like this: export const getLoadContext: GetLoadContext = async ({ context, request }) => { const isEnvEmpty = Object.keys(context.cloudflare.env).length === 0; const env = isEnvEmpty ? process.env : context.cloudflare.env; const sessionFlashSecret = env.SESSION_FLASH_SECRET; const flashStorage = createCookieSessionStorage({ cookie: { name: "__flash", httpOnly: true, maxAge: 60, path: "/", sameSite: "lax", secrets: [sessionFlashSecret], secure: true, }, }); return { ...context, cloudflare: { ...context.cloudflare, env, }, dispatch: (await dispatchWithContext({ env: env as unknown as Record<string, string>, request, })) as Dispatch, flashStorage, }; }; export const getLoadContext: GetLoadContext = async ({ context, request }) => { const isEnvEmpty = Object.keys(context.cloudflare.env).length === 0; const env = isEnvEmpty ? process.env : context.cloudflare.env; const sessionFlashSecret = env.SESSION_FLASH_SECRET; const flashStorage = createCookieSessionStorage({ cookie: { name: "__flash", httpOnly: true, maxAge: 60, path: "/", sameSite: "lax", secrets: [sessionFlashSecret], secure: true, }, }); return { ...context, cloudflare: { ...context.cloudflare, env, }, dispatch: (await dispatchWithContext({ env: env as unknown as Record<string, string>, request, })) as Dispatch, flashStorage, }; }; Note that the dispatch code is omitted. You can find more about it in my article on how to 10x your TypeScript dev experience here . here I can strip out Remix or use another consumer without altering my code. But…. There is an additional challenge when you work in a monorepo structure using turborepo. If you import a TypeScript file from a package within the load-context, let's say @repo/db Vite will return an error that the file with extension .ts is unknown, and will not know how to process it. @repo/db .ts This happens because load-context + workspaces are outside the site’s main importing graph, leaving TypeScript files outside of play. The trick is to use tsx and load it before calling Vite, which will work. This is important because it overcomes the following limitations: tsx before Cloudflare Package Dependencies. Cloudflare Package Dependencies and Pre-building Cloudflare Package Dependencies and Pre-building First of all, that was the step that I was trying to avoid, as it meant that I had to introduce a build step for each of the packages, which meant more configuration. Fortunately, this didn’t work for Cloudflare Pages. Specific libraries, such as Postgres, will detect the runtime and pull the required package. There’s a workaround: We can use tsx to load all the TypeScript files and transpile them before we execute. You can argue this is a pre-build step, but since it’s still at the remix’s repository level, I don’t see significant issues with this approach. To solve this, we add tsx as a dependency: pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app pnpm add tsx -D --filter=@repo/my-remix-cloudflare-app And then, we need to modify our package.json and add the tsx process to each one of our remix scripts: package.json { "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "scripts": { // Other scripts omitted "build": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:build", "dev": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:dev", "start": "NODE_OPTIONS=\"--import tsx/esm\" wrangler pages dev ./build/client" } } { "name": "@repo/my-remix-cloudflare-app", "version": "1.0.0", "scripts": { // Other scripts omitted "build": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:build", "dev": "NODE_OPTIONS=\"--import tsx/esm\" remix vite:dev", "start": "NODE_OPTIONS=\"--import tsx/esm\" wrangler pages dev ./build/client" } } Extras Creating a .npmrc File .npmrc In case you're having issues while adding your local packages with the command line, you can create a .npmrc file in the root of the project. .npmrc .npmrc link-workspace-packages= true prefer-workspace-packages=true link-workspace-packages= true prefer-workspace-packages=true This will tell pnpm to use the workspace packages first. Thanks to ZoWnx from Reddit who helped me craft a .nprmc file ZoWnx Pitfalls - Careful with naming .client and .server in your files. Even if it's in a separate library. Remix uses these to determine if it's a client or server file. The project isn't compiled per repository so it will throw an import error! If you’re having problems with multi-platform packages such as Postgres, installing it at the workspace level is better. It will detect the proper import. Installing it directly in the @repo/db repository will break when importing it to Remix. Careful with naming .client and .server in your files. Even if it's in a separate library. Remix uses these to determine if it's a client or server file. The project isn't compiled per repository so it will throw an import error! Careful with naming .client and .server in your files. Even if it's in a separate library. Remix uses these to determine if it's a client or server file. The project isn't compiled per repository so it will throw an import error! .client .server If you’re having problems with multi-platform packages such as Postgres, installing it at the workspace level is better. It will detect the proper import. Installing it directly in the @repo/db repository will break when importing it to Remix. If you’re having problems with multi-platform packages such as Postgres, installing it at the workspace level is better. It will detect the proper import. Installing it directly in the @repo/db repository will break when importing it to Remix. That’s it, folks!!! That’s it, folks!!! GitHub Repository You can access the full implementation here . full implementation here Follow Me on Social! Follow Me on Social! I’m building an automated testing engineer in public to catch those 1% errors in production. I share my progress on: X/Twitter @javiasilis X/Twitter @javiasilis LinkedIn @javiasilis LinkedIn @javiasilis