diff --git a/accelerate/remix-starter/.env.example b/accelerate/remix-starter/.env.example
new file mode 100644
index 000000000000..5c8813b77c04
--- /dev/null
+++ b/accelerate/remix-starter/.env.example
@@ -0,0 +1,3 @@
+DIRECT_DATABASE_URL="__YOUR_DATABASE_CONNECTION_STRING__"
+DATABASE_URL="__YOUR_ACCELERATE_CONNECTION_STRING__"
+NODE_ENV="development"
\ No newline at end of file
diff --git a/accelerate/remix-starter/.eslintignore b/accelerate/remix-starter/.eslintignore
new file mode 100644
index 000000000000..67191efbb0b9
--- /dev/null
+++ b/accelerate/remix-starter/.eslintignore
@@ -0,0 +1,7 @@
+.cache
+build
+public/build
+app/styles
+dist/
+node_modules
+api/
diff --git a/accelerate/remix-starter/.eslintrc b/accelerate/remix-starter/.eslintrc
new file mode 100644
index 000000000000..c22f538460f1
--- /dev/null
+++ b/accelerate/remix-starter/.eslintrc
@@ -0,0 +1,68 @@
+{
+ "parser": "@typescript-eslint/parser",
+ "globals": {
+ "module": true,
+ "require": true,
+ "process": true,
+ "exports": true
+ },
+ "settings": {
+ "react": {
+ "version": "detect"
+ },
+ "formComponents": ["Form"],
+ "linkComponents": [
+ { "name": "Link", "linkAttribute": "to" },
+ { "name": "NavLink", "linkAttribute": "to" }
+ ]
+ },
+ "plugins": [
+ "prettier",
+ "@typescript-eslint",
+ "jsx-a11y",
+ "import",
+ "react",
+ "react-hooks",
+ "lodash"
+ ],
+ "extends": [
+ "eslint:recommended",
+ "prettier",
+ "plugin:prettier/recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:jsx-a11y/recommended",
+ "plugin:import/recommended",
+ "plugin:import/typescript",
+ "plugin:react/jsx-runtime",
+ "plugin:react/recommended",
+ "plugin:react-hooks/recommended",
+ "@remix-run/eslint-config/node"
+ ],
+ "rules": {
+ "lodash/import-scope": [2, "method"],
+ "prettier/prettier": "error",
+ "import/no-unresolved": "off",
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }
+ ],
+ "jsx-a11y/anchor-is-valid": [
+ "error",
+ {
+ "components": ["Link", "NavLink"],
+ "specialLink": ["to"]
+ }
+ ],
+ "react/boolean-prop-naming": "error",
+ "react/react-in-jsx-scope": "off",
+ "react/button-has-type": "error",
+ "react/jsx-no-target-blank": [
+ "error",
+ {
+ "warnOnSpreadAttributes": true,
+ "links": true,
+ "forms": true
+ }
+ ]
+ }
+}
diff --git a/accelerate/remix-starter/.gitignore b/accelerate/remix-starter/.gitignore
new file mode 100644
index 000000000000..1e76147d5ced
--- /dev/null
+++ b/accelerate/remix-starter/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+.cache
+.output
+build
+public/build
+api/*
+.env
+test-results/
+playwright-report/
+.eslintcache
+
+!.env.example
\ No newline at end of file
diff --git a/accelerate/remix-starter/.npmrc b/accelerate/remix-starter/.npmrc
new file mode 100644
index 000000000000..dc48c97ba2c6
--- /dev/null
+++ b/accelerate/remix-starter/.npmrc
@@ -0,0 +1,2 @@
+auto-install-peers=false
+shamefully-hoist=true
diff --git a/accelerate/remix-starter/.prettierrc b/accelerate/remix-starter/.prettierrc
new file mode 100644
index 000000000000..bf357fbbc081
--- /dev/null
+++ b/accelerate/remix-starter/.prettierrc
@@ -0,0 +1,3 @@
+{
+ "trailingComma": "all"
+}
diff --git a/accelerate/remix-starter/.tool-versions b/accelerate/remix-starter/.tool-versions
new file mode 100644
index 000000000000..02fda068233c
--- /dev/null
+++ b/accelerate/remix-starter/.tool-versions
@@ -0,0 +1 @@
+nodejs 20.14.0
diff --git a/accelerate/remix-starter/Prisma-Studio-Image.png b/accelerate/remix-starter/Prisma-Studio-Image.png
new file mode 100644
index 000000000000..9180399bd1c7
Binary files /dev/null and b/accelerate/remix-starter/Prisma-Studio-Image.png differ
diff --git a/accelerate/remix-starter/README.md b/accelerate/remix-starter/README.md
new file mode 100644
index 000000000000..c1c7af8c5ce5
--- /dev/null
+++ b/accelerate/remix-starter/README.md
@@ -0,0 +1,99 @@
+# Prisma Accelerate Example: Remix Starter
+
+This project showcases how to use Prisma ORM with Prisma Accelerate in a Remix application. It [demonstrates](./app/routes/_index.tsx#L10-13) every available [caching strategy in Accelerate](https://www.prisma.io/docs/data-platform/accelerate/concepts#cache-strategies).
+
+## Prerequisites
+
+To successfully run the project, you will need the following:
+
+- The **connection string** of a publicly accessible database.
+- Your **Accelerate connection string** (containing your **Accelerate API key**) which you can get by enabling Accelerate in a project in your [Prisma Data Platform](https://pris.ly/pdp) account (learn more in the [docs](https://www.prisma.io/docs/platform/concepts/environments#api-keys)).
+
+## Getting started
+
+### 1. Clone the repository
+
+Clone the repository, navigate into it and install dependencies:
+
+```
+git clone git@github.com:prisma/prisma-examples.git --depth=1
+cd prisma-examples/accelerate/remix-starter
+npm install
+```
+
+### 2. Configure environment variables
+
+Copy the `.env.example` env file in the root of the project directory:
+
+```bash
+cp .env.example .env
+```
+
+Now, open the `.env` file and set the `DIRECT_DATABASE_URL` and `DATABASE_URL` environment variables with the values of your connection string and your Accelerate connection string respectively:
+
+```bash
+# .env
+
+# Accelerate connection string (used for queries by Prisma Client)
+DATABASE_URL="__YOUR_ACCELERATE_CONNECTION_STRING__"
+
+# Database connection string (used for migrations by Prisma Migrate)
+DIRECT_DATABASE_URL="__YOUR_DATABASE_CONNECTION_STRING__"
+
+```
+
+Note that `__YOUR_DATABASE_CONNECTION_STRING__` and `__YOUR_ACCELERATE_CONNECTION_STRING__` are placeholder values that you need to replace with the values of your database and Accelerate connection strings. Notice that the Accelerate connection string has the following structure: `prisma://accelerate.prisma-data.net/?api_key=__YOUR_ACCELERATE_API_KEY__`.
+
+### 3. Run a migration to create the `Quotes` table and seed the database
+
+The Prisma schema file contains a single `Quotes` model. You can map this model to the database and create the corresponding `Quotes` table using the following command:
+
+```
+npx prisma migrate dev --name init
+```
+
+You now have an empty `Quotes` table in your database. Next, run the [seed script](./prisma/seed.ts) to create some sample records in the table:
+
+```
+npx prisma db seed
+```
+
+### 4. Generate Prisma Client for Accelerate
+
+When using Accelerate, Prisma Client doesn't need a query engine. That's why you should generate it as follows:
+
+```
+npx prisma generate --no-engine
+```
+
+### 5. Start the app
+
+You can run the app with the following command:
+
+```
+npm run dev
+```
+
+The application will start on PORT 5173 and you should be able to see the performance and other stats (e.g. cache/hit) for the different Accelerate cache strategies at the bottom of the UI:
+
+
+
+This application queries the most recent Quote with all the different cache strategies available in Accelerate.
+
+Optionally, to add your own quote and see the caching strategies in action, you can add a new quote through Prisma Studio by running the following command:
+
+```
+npx prisma studio
+```
+
+Once the Prisma Studio is running, you can add a new quote by clicking on the `Quotes` table and then the `Add Record` button as shown in the screenshot below.
+
+
+
+After adding a new record, you can refresh the Remix application to see the new quote and the caching strategies in action.
+
+## Resources
+
+- [Accelerate Speed Test](https://accelerate-speed-test.vercel.app/)
+- [Accelerate documentation](https://www.prisma.io/docs/accelerate)
+- [Prisma Discord](https://pris.ly/discord)
diff --git a/accelerate/remix-starter/Remix-accelerate.gif b/accelerate/remix-starter/Remix-accelerate.gif
new file mode 100644
index 000000000000..e5095d57df55
Binary files /dev/null and b/accelerate/remix-starter/Remix-accelerate.gif differ
diff --git a/accelerate/remix-starter/app/components/Quote/Quote.tsx b/accelerate/remix-starter/app/components/Quote/Quote.tsx
new file mode 100644
index 000000000000..b71c1cee2b77
--- /dev/null
+++ b/accelerate/remix-starter/app/components/Quote/Quote.tsx
@@ -0,0 +1,77 @@
+/* eslint-disable react/prop-types */
+import { QuoteWrapper } from "./QuoteWrapper";
+import { QuoteCacheType, QuoteResult } from "./../../lib/types";
+import pkg from "openflights-cached";
+const { findIATA } = pkg;
+
+export const Quote: React.FC<{
+ title: string;
+ type: QuoteCacheType;
+ result: QuoteResult;
+}> = ({ title, type, result }) => {
+ const [{ id, quote, createdAt }, { cacheStatus, region, lastModified }] = [
+ result.data,
+ result.info,
+ ];
+
+ return (
+
+
+
+ ID {id} ⸺ {'"'}
+ {quote}
+ {'"'}
+
+
+
+ Created At ⸺
+ {new Date(createdAt).toLocaleString("en-US")}
+
+
+
+
+
+
+
+ Cache Node Region ⸺
+
+ {findIATA(region)?.city ?? region}
+
+
+
+
+ Cached Modified at ⸺
+
+ {new Date(lastModified).toLocaleString("en-US")}
+
+
+
+
+ Cache status ⸺
+
+ {cacheStatus
+ .toUpperCase()
+ .concat(
+ cacheStatus === "swr" || cacheStatus === "ttl"
+ ? " CACHE HIT"
+ : "",
+ )}
+
+
+
+
+ Time taken: {result.time}ms
+
+
+
+
+
+ );
+};
diff --git a/accelerate/remix-starter/app/components/Quote/QuoteWrapper.tsx b/accelerate/remix-starter/app/components/Quote/QuoteWrapper.tsx
new file mode 100644
index 000000000000..97e731d18e03
--- /dev/null
+++ b/accelerate/remix-starter/app/components/Quote/QuoteWrapper.tsx
@@ -0,0 +1,24 @@
+/* eslint-disable react/prop-types */
+import { QuoteCacheType } from "../../lib/types";
+import { ReactNode } from "react";
+
+export const QuoteWrapper: React.FC<{
+ title: string;
+ type: QuoteCacheType;
+ children: ReactNode;
+}> = ({ title, type, children }) => {
+ return (
+
+
+
+ {title}
+
+
+
+ {type}
+
+
+
{children}
+
+ );
+};
diff --git a/accelerate/remix-starter/app/components/Quote/Quotes.tsx b/accelerate/remix-starter/app/components/Quote/Quotes.tsx
new file mode 100644
index 000000000000..83e8e3ef94ce
--- /dev/null
+++ b/accelerate/remix-starter/app/components/Quote/Quotes.tsx
@@ -0,0 +1,21 @@
+// app/components/Quotes.tsx
+import { Quote } from "./Quote";
+import { QuoteResult } from "~/lib/types";
+
+interface QuotesProps {
+ ttl: QuoteResult;
+ swr: QuoteResult;
+ both: QuoteResult;
+ none: QuoteResult;
+}
+
+export default function Quotes({ ttl, swr, both, none }: QuotesProps) {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/accelerate/remix-starter/app/components/Quote/RefreshQuote.tsx b/accelerate/remix-starter/app/components/Quote/RefreshQuote.tsx
new file mode 100644
index 000000000000..8a9ffd66e3d5
--- /dev/null
+++ b/accelerate/remix-starter/app/components/Quote/RefreshQuote.tsx
@@ -0,0 +1,17 @@
+import { Toaster } from "react-hot-toast";
+
+export const RefreshQuote = () => {
+ return (
+ <>
+
+
+ window.location.reload()}
+ className="focus:outline-none text-white bg-green-700 hover:bg-green-800 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 mr-2 mb-2 dark:bg-green-600 dark:hover:bg-green-700 dark:focus:ring-green-800"
+ >
+ Refresh
+
+ >
+ );
+};
diff --git a/accelerate/remix-starter/app/data/query.server.ts b/accelerate/remix-starter/app/data/query.server.ts
new file mode 100644
index 000000000000..51b58f7914ae
--- /dev/null
+++ b/accelerate/remix-starter/app/data/query.server.ts
@@ -0,0 +1,23 @@
+import prisma from "./utils/prisma.server";
+import { CacheStrategy } from "../lib/types";
+
+export const getQuotes = async (strategy?: CacheStrategy) => {
+ const start = Date.now();
+
+ const result = await prisma.quotes
+ .findMany({
+ // You can find the `cacheStrategy` options [here](https://www.prisma.io/docs/accelerate/caching#cache-strategies). The `cacheStrategy` can also be undefined, which would mean only connection pooling is being used.
+ cacheStrategy: strategy,
+ orderBy: {
+ id: "desc",
+ },
+ take: 1,
+ })
+ .withAccelerateInfo();
+
+ return {
+ data: result?.data?.[0],
+ info: result.info,
+ time: Date.now() - start,
+ };
+};
diff --git a/accelerate/remix-starter/app/data/quotes.server.ts b/accelerate/remix-starter/app/data/quotes.server.ts
new file mode 100644
index 000000000000..2d6d019173a2
--- /dev/null
+++ b/accelerate/remix-starter/app/data/quotes.server.ts
@@ -0,0 +1,28 @@
+import { getQuotes } from "./query.server";
+
+export async function getQuotesFromDB(cacheStrategy: string) {
+ const map = new Map();
+
+ // When TTL is selected
+ map.set("TTL", {
+ ttl: 30,
+ });
+
+ // When SWR is selected
+ map.set("SWR", {
+ swr: 30,
+ });
+
+ // When TTL + SWR is selected
+ map.set("BOTH", {
+ ttl: 30,
+ swr: 60,
+ });
+
+ // This ensures no caching is performed and only the Accelerate connection pool is used
+ map.set("NONE", undefined);
+
+ const data = await getQuotes(map.get(cacheStrategy));
+
+ return data;
+}
diff --git a/accelerate/remix-starter/app/data/utils/formatZodErrors.server.ts b/accelerate/remix-starter/app/data/utils/formatZodErrors.server.ts
new file mode 100644
index 000000000000..bb6c785a64d7
--- /dev/null
+++ b/accelerate/remix-starter/app/data/utils/formatZodErrors.server.ts
@@ -0,0 +1,11 @@
+import capitalize from "lodash/capitalize.js";
+import type { ZodError } from "zod";
+
+export function formatZodErrors(zodError: ZodError) {
+ return Object.fromEntries(
+ zodError.issues.map((errorObj) => [
+ errorObj.path,
+ capitalize(errorObj.message),
+ ]),
+ );
+}
diff --git a/accelerate/remix-starter/app/data/utils/prisma.server.ts b/accelerate/remix-starter/app/data/utils/prisma.server.ts
new file mode 100644
index 000000000000..293f661397bd
--- /dev/null
+++ b/accelerate/remix-starter/app/data/utils/prisma.server.ts
@@ -0,0 +1,38 @@
+/* eslint-disable no-var */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { PrismaClient } from "@prisma/client";
+import { withAccelerate } from "@prisma/extension-accelerate";
+import { SERVER_ENV } from "~/env/envFlags.server";
+
+function buildClient() {
+ const client = new PrismaClient().$extends(withAccelerate());
+
+ return client;
+}
+
+/**
+ * The type of the PrismaClient with extensions
+ */
+export type PrismaClientType = ReturnType;
+
+let prisma: PrismaClientType;
+
+declare global {
+ var __prisma: PrismaClientType | undefined;
+}
+
+// This is needed because in development we don't want to restart
+// the server with every change, but we want to make sure we don't
+// create a new connection to the DB with every change either.
+if (SERVER_ENV.NODE_ENV === "production") {
+ prisma = buildClient();
+ prisma.$connect();
+} else {
+ if (!global.__prisma) {
+ global.__prisma = buildClient();
+ global.__prisma.$connect();
+ }
+ prisma = global.__prisma;
+}
+
+export default prisma;
diff --git a/accelerate/remix-starter/app/data/utils/types.ts b/accelerate/remix-starter/app/data/utils/types.ts
new file mode 100644
index 000000000000..66818d7a67de
--- /dev/null
+++ b/accelerate/remix-starter/app/data/utils/types.ts
@@ -0,0 +1,5 @@
+export type GenericDataError = Record;
+
+export type DataResult =
+ | { data: DataType; errors: null }
+ | { data: null; errors: GenericDataError };
diff --git a/accelerate/remix-starter/app/entry.client.tsx b/accelerate/remix-starter/app/entry.client.tsx
new file mode 100644
index 000000000000..442da4f8a80d
--- /dev/null
+++ b/accelerate/remix-starter/app/entry.client.tsx
@@ -0,0 +1,12 @@
+import { RemixBrowser } from "@remix-run/react";
+import { startTransition, StrictMode } from "react";
+import { hydrateRoot } from "react-dom/client";
+
+startTransition(() => {
+ hydrateRoot(
+ document,
+
+
+ ,
+ );
+});
diff --git a/accelerate/remix-starter/app/entry.server.tsx b/accelerate/remix-starter/app/entry.server.tsx
new file mode 100644
index 000000000000..38476546d30e
--- /dev/null
+++ b/accelerate/remix-starter/app/entry.server.tsx
@@ -0,0 +1,147 @@
+import { PassThrough } from "node:stream";
+
+import type {
+ AppLoadContext,
+ EntryContext,
+ HandleErrorFunction,
+} from "@remix-run/node";
+import { createReadableStreamFromReadable } from "@remix-run/node";
+import { RemixServer } from "@remix-run/react";
+import { isbot } from "isbot";
+import { renderToPipeableStream } from "react-dom/server";
+
+const ABORT_DELAY = 5_000;
+
+export const handleError: HandleErrorFunction = (
+ error,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ { request, params, context },
+) => {
+ if (!request.signal.aborted) {
+ // Here you can log the error to your preferred error handling service
+ // handleError(error)
+ }
+};
+
+export default function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ loadContext: AppLoadContext,
+) {
+ return isbot(request.headers.get("user-agent"))
+ ? handleBotRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext,
+ )
+ : handleBrowserRequest(
+ request,
+ responseStatusCode,
+ responseHeaders,
+ remixContext,
+ );
+}
+
+function handleBotRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onAllReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ },
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
+
+function handleBrowserRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext,
+) {
+ return new Promise((resolve, reject) => {
+ let shellRendered = false;
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ onShellReady() {
+ shellRendered = true;
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ }),
+ );
+
+ pipe(body);
+ },
+ onShellError(error: unknown) {
+ reject(error);
+ },
+ onError(error: unknown) {
+ responseStatusCode = 500;
+ // Log streaming rendering errors from inside the shell. Don't log
+ // errors encountered during initial shell rendering since they'll
+ // reject and get logged in handleDocumentRequest.
+ if (shellRendered) {
+ console.error(error);
+ }
+ },
+ },
+ );
+
+ setTimeout(abort, ABORT_DELAY);
+ });
+}
diff --git a/accelerate/remix-starter/app/env/envFlags.server.ts b/accelerate/remix-starter/app/env/envFlags.server.ts
new file mode 100644
index 000000000000..dac50a0581ca
--- /dev/null
+++ b/accelerate/remix-starter/app/env/envFlags.server.ts
@@ -0,0 +1,28 @@
+import z from "zod";
+import { generateErrorMessage } from "zod-error";
+
+export const serverEnvSchema = z.object({
+ DIRECT_DATABASE_URL: z.string(),
+ DATABASE_URL: z.string(),
+ NODE_ENV: z.string(),
+});
+
+export type ServerEnv = z.infer;
+
+/** Zod will filter all the keys not specified on the schema */
+function buildEnv(): ServerEnv {
+ try {
+ return serverEnvSchema.parse(process.env);
+ } catch (error: unknown) {
+ console.error("Warning: invalid server env vars!");
+ console.error(
+ generateErrorMessage((error as z.ZodError).issues, {
+ delimiter: { error: "\n" },
+ }),
+ );
+
+ return {} as ServerEnv;
+ }
+}
+
+export const SERVER_ENV = buildEnv();
diff --git a/accelerate/remix-starter/app/env/globalEnv.ts b/accelerate/remix-starter/app/env/globalEnv.ts
new file mode 100644
index 000000000000..341ff02ea751
--- /dev/null
+++ b/accelerate/remix-starter/app/env/globalEnv.ts
@@ -0,0 +1,41 @@
+import z from "zod";
+import { generateErrorMessage } from "zod-error";
+
+export const globalEnvSchema = z.object({
+ PUBLIC_EXAMPLE: z.string().optional(),
+ EXAMPLE_GLOBAL_FEATURE_FLAG: z
+ .enum(["true", "false"])
+ .optional()
+ .transform((v) => v === "true"),
+});
+
+export type GlobalEnv = z.infer;
+
+/** Zod will filter all the keys not specified on the schema */
+function buildEnv(): GlobalEnv {
+ try {
+ return globalEnvSchema.parse(process.env);
+ } catch (error: unknown) {
+ console.error("Warning: invalid client env vars!");
+ console.error(
+ generateErrorMessage((error as z.ZodError).issues, {
+ delimiter: { error: "\n" },
+ }),
+ );
+
+ return {} as GlobalEnv;
+ }
+}
+
+/**
+ * If we are on a browser environment, we get the vars from the `window` object.
+ * We set this on the root.tsx file on around line 58.
+ *
+ * If we are on a server environment, we just read it from process.env.
+ *
+ * Remember that CLIENT_ENV vars can be accessed from any context.
+ */
+export const GLOBAL_ENV =
+ typeof window === "undefined"
+ ? buildEnv()
+ : (window as unknown as { ENV: GlobalEnv }).ENV;
diff --git a/accelerate/remix-starter/app/env/useFeatureFlags.tsx b/accelerate/remix-starter/app/env/useFeatureFlags.tsx
new file mode 100644
index 000000000000..67c9a46ca3f5
--- /dev/null
+++ b/accelerate/remix-starter/app/env/useFeatureFlags.tsx
@@ -0,0 +1,29 @@
+import { useContext, useMemo } from "react";
+
+import { UserContext } from "../hooks/useUser";
+import { UserFeatureFlags } from "./userFeatureFlags.server";
+import { GLOBAL_ENV, GlobalEnv } from "./globalEnv";
+
+export default function useFeatureFlags() {
+ const userContext = useContext(UserContext);
+
+ return useMemo(
+ () => ({
+ // Check the CLIENT_ENV object for feature flags
+ hasGlobalFeatureFlag: (flag: keyof GlobalEnv): boolean =>
+ !!GLOBAL_ENV[flag],
+ // Get a value from CLIENT_ENV object
+ getGlobalFeatureFlag: (
+ flag: T,
+ ): GlobalEnv[T] => GLOBAL_ENV[flag],
+ // Check the current user feature flags. If there's no user, this returns false, always
+ hasUserFeatureFlag: (flag: keyof UserFeatureFlags): boolean =>
+ !!userContext?.featureFlags?.[flag],
+ // Get the current user feature flag value. If there's no user, this returns undefined, always
+ getUserFeatureFlag: (
+ flag: T,
+ ): UserFeatureFlags[T] | undefined => userContext?.featureFlags?.[flag],
+ }),
+ [userContext?.featureFlags],
+ );
+}
diff --git a/accelerate/remix-starter/app/env/userFeatureFlags.server.ts b/accelerate/remix-starter/app/env/userFeatureFlags.server.ts
new file mode 100644
index 000000000000..bfa1f2d4b5bc
--- /dev/null
+++ b/accelerate/remix-starter/app/env/userFeatureFlags.server.ts
@@ -0,0 +1,10 @@
+import { z } from "zod";
+
+export const UserFeatureFlagsSchema = z
+ .object({
+ EXAMPLE_FEATURE_FLAG: z.boolean().optional(),
+ EXAMPLE_FEATURE_FLAG_STRING: z.string().optional(),
+ })
+ .default({});
+
+export type UserFeatureFlags = z.infer;
diff --git a/accelerate/remix-starter/app/hooks/useRootLoaderData.tsx b/accelerate/remix-starter/app/hooks/useRootLoaderData.tsx
new file mode 100644
index 000000000000..8afaec415576
--- /dev/null
+++ b/accelerate/remix-starter/app/hooks/useRootLoaderData.tsx
@@ -0,0 +1,9 @@
+import { SerializeFrom } from "@remix-run/node";
+import { useRouteLoaderData } from "@remix-run/react";
+import { loader } from "~/root";
+
+export type RootLoaderType = SerializeFrom>>;
+
+export function useRootLoaderData() {
+ return useRouteLoaderData("root")!;
+}
diff --git a/accelerate/remix-starter/app/lib/types/index.tsx b/accelerate/remix-starter/app/lib/types/index.tsx
new file mode 100644
index 000000000000..1fda962ee16b
--- /dev/null
+++ b/accelerate/remix-starter/app/lib/types/index.tsx
@@ -0,0 +1,33 @@
+export type CacheStrategy =
+ | {
+ ttl: number;
+ swr: number;
+ }
+ | {
+ ttl: number;
+ }
+ | {
+ swr: number;
+ };
+
+export type AccelerateInfo = {
+ cacheStatus: "ttl" | "swr" | "miss" | "none";
+ lastModified: string;
+ region: string;
+ requestId: string;
+ signature: string;
+};
+
+export type Quote = {
+ id: number;
+ quote: string;
+ createdAt: string;
+};
+
+export type QuoteResult = {
+ data: Quote;
+ info: AccelerateInfo;
+ time: number;
+};
+
+export type QuoteCacheType = "SWR" | "TTL" | "No caching" | "TTL + SWR";
diff --git a/accelerate/remix-starter/app/root.css b/accelerate/remix-starter/app/root.css
new file mode 100644
index 000000000000..8abdb15c9458
--- /dev/null
+++ b/accelerate/remix-starter/app/root.css
@@ -0,0 +1,76 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 222.2 84% 4.9%;
+
+ --card: 0 0% 100%;
+ --card-foreground: 222.2 84% 4.9%;
+
+ --popover: 0 0% 100%;
+ --popover-foreground: 222.2 84% 4.9%;
+
+ --primary: 222.2 47.4% 11.2%;
+ --primary-foreground: 210 40% 98%;
+
+ --secondary: 210 40% 96.1%;
+ --secondary-foreground: 222.2 47.4% 11.2%;
+
+ --muted: 210 40% 96.1%;
+ --muted-foreground: 215.4 16.3% 46.9%;
+
+ --accent: 210 40% 96.1%;
+ --accent-foreground: 222.2 47.4% 11.2%;
+
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 214.3 31.8% 91.4%;
+ --input: 214.3 31.8% 91.4%;
+ --ring: 222.2 84% 4.9%;
+
+ --radius: 0.5rem;
+ }
+
+ .dark {
+ --background: 222.2 84% 4.9%;
+ --foreground: 210 40% 98%;
+
+ --card: 222.2 84% 4.9%;
+ --card-foreground: 210 40% 98%;
+
+ --popover: 222.2 84% 4.9%;
+ --popover-foreground: 210 40% 98%;
+
+ --primary: 210 40% 98%;
+ --primary-foreground: 222.2 47.4% 11.2%;
+
+ --secondary: 217.2 32.6% 17.5%;
+ --secondary-foreground: 210 40% 98%;
+
+ --muted: 217.2 32.6% 17.5%;
+ --muted-foreground: 215 20.2% 65.1%;
+
+ --accent: 217.2 32.6% 17.5%;
+ --accent-foreground: 210 40% 98%;
+
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 210 40% 98%;
+
+ --border: 217.2 32.6% 17.5%;
+ --input: 217.2 32.6% 17.5%;
+ --ring: 212.7 26.8% 83.9%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/accelerate/remix-starter/app/root.tsx b/accelerate/remix-starter/app/root.tsx
new file mode 100644
index 000000000000..d5ffc151b210
--- /dev/null
+++ b/accelerate/remix-starter/app/root.tsx
@@ -0,0 +1,127 @@
+import "./root.css";
+
+import { unstable_defineLoader } from "@remix-run/node";
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "@remix-run/react";
+import acceptLanguage from "accept-language-parser";
+import React, { useEffect } from "react";
+import { GLOBAL_ENV } from "./env/globalEnv";
+import { useRootLoaderData } from "./hooks/useRootLoaderData";
+import { cn } from "./utils";
+import { getCurrentTheme } from "./web/theme.server";
+
+// Load the locale from the Accept-Language header to later
+// inject it on the app's context
+function localeFromRequest(request: Request): string {
+ const languages = acceptLanguage.parse(
+ request.headers.get("Accept-Language") as string,
+ );
+
+ // If somehow the header is empty, return a default locale
+ if (languages?.length < 1) return "en-us";
+
+ // If there is no region for this locale, just return the country code
+ if (!languages[0].region) return languages[0].code;
+
+ return `${languages[0].code}-${languages[0].region.toLowerCase()}`;
+}
+
+export const loader = unstable_defineLoader(async ({ request }) => {
+ return {
+ locale: localeFromRequest(request),
+ ENV: GLOBAL_ENV,
+ rootTime: new Date().toISOString(),
+ currentTheme: await getCurrentTheme(request),
+ };
+});
+
+function applySystemTheme() {
+ const theme = window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+ const cl = document.documentElement.classList;
+
+ cl.add(theme);
+}
+
+const applySystemThemeString = `
+ const theme = window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+ const cl = document.documentElement.classList;
+
+ cl.add(theme);
+`;
+
+export default function App() {
+ const { ENV, currentTheme } = useRootLoaderData();
+
+ useEffect(() => {
+ if (currentTheme === "system") applySystemTheme();
+ }, [currentTheme]);
+
+ return (
+
+
+
+
+
+ );
+}
+
+/**
+ * This will render errors without env vars or any locale
+ * info unfortunately as errors can happen on the root loader.
+ */
+export function ErrorBoundary() {
+ return (
+
+
+
+ );
+}
+
+function Document({
+ children,
+ title,
+ className,
+}: {
+ children: React.ReactNode;
+ title?: string;
+ className?: string;
+}) {
+ return (
+
+
+
+
+
+ {title ? {title} : null}
+
+
+
+
+ {children}
+
+
+
+
+
+ );
+}
diff --git a/accelerate/remix-starter/app/routes/_index.tsx b/accelerate/remix-starter/app/routes/_index.tsx
new file mode 100644
index 000000000000..8a3fc582950d
--- /dev/null
+++ b/accelerate/remix-starter/app/routes/_index.tsx
@@ -0,0 +1,47 @@
+// app/routes/index.tsx
+import { LoaderFunction, json } from "@remix-run/node";
+import { useLoaderData } from "@remix-run/react";
+import Quotes from "../components/Quote/Quotes";
+import { RefreshQuote } from "../components/Quote/RefreshQuote";
+import { QuoteResult } from "~/lib/types";
+import { getQuotesFromDB } from "../data/quotes.server";
+
+const getQuotes = async () => {
+ const ttl = await getQuotesFromDB("TTL");
+ const swr = await getQuotesFromDB("SWR");
+ const both = await getQuotesFromDB("BOTH");
+ const none = await getQuotesFromDB("NONE");
+
+ return [ttl, swr, both, none];
+};
+
+export const loader: LoaderFunction = async () => {
+ const [ttl, swr, both, none] = await getQuotes();
+ return json({ ttl, swr, both, none });
+};
+
+export default function Index() {
+ const { ttl, swr, both, none } = useLoaderData<{
+ ttl: QuoteResult;
+ swr: QuoteResult;
+ both: QuoteResult;
+ none: QuoteResult;
+ }>();
+
+ return (
+
+
+ Accelerated Quotes
+
+
+
+ Retrieves the most recently added quote with and without caching enabled
+
+
+
+
+
+
+
+ );
+}
diff --git a/accelerate/remix-starter/app/utils.ts b/accelerate/remix-starter/app/utils.ts
new file mode 100644
index 000000000000..365058cebd7d
--- /dev/null
+++ b/accelerate/remix-starter/app/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/accelerate/remix-starter/app/web/theme.server.ts b/accelerate/remix-starter/app/web/theme.server.ts
new file mode 100644
index 000000000000..8b4a163db0fb
--- /dev/null
+++ b/accelerate/remix-starter/app/web/theme.server.ts
@@ -0,0 +1,36 @@
+import { createCookie, redirect } from "@remix-run/node";
+
+export type ThemeType = "dark" | "light" | "system";
+
+const themeCookie = createCookie("theme", {
+ sameSite: "lax",
+ maxAge: 604_800, // one week,
+});
+
+export async function getCurrentTheme(request: Request): Promise {
+ const cookieHeader = request.headers.get("Cookie");
+ const currentValue = await themeCookie.parse(cookieHeader);
+
+ if (currentValue === "dark" || currentValue === "light") return currentValue;
+
+ return "system";
+}
+
+export async function setTheme(request: Request): Promise {
+ const desiredTheme = (await request.formData()).get("theme");
+ const redirectUrl = request.headers.get("Referer") || "/";
+
+ if (
+ typeof desiredTheme === "string" &&
+ !["dark", "light", "system"].includes(desiredTheme)
+ ) {
+ // Let's not do anything if the theme is invalid
+ return redirect(redirectUrl);
+ }
+
+ return redirect(redirectUrl, {
+ headers: {
+ "Set-Cookie": await themeCookie.serialize(desiredTheme),
+ },
+ });
+}
diff --git a/accelerate/remix-starter/components.json b/accelerate/remix-starter/components.json
new file mode 100644
index 000000000000..806ecf954c13
--- /dev/null
+++ b/accelerate/remix-starter/components.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.js",
+ "css": "app/root.css",
+ "baseColor": "slate",
+ "cssVariables": true
+ },
+ "aliases": {
+ "components": "~/components",
+ "utils": "~/utils"
+ }
+}
\ No newline at end of file
diff --git a/accelerate/remix-starter/package.json b/accelerate/remix-starter/package.json
new file mode 100644
index 000000000000..7f3ce52549ef
--- /dev/null
+++ b/accelerate/remix-starter/package.json
@@ -0,0 +1,92 @@
+{
+ "name": "remix-accelerate-starter",
+ "private": false,
+ "description": "A Remix Example App with Prisma Accelerate",
+ "license": "MIT",
+ "type": "module",
+ "scripts": {
+ "build": "remix vite:build",
+ "deploy": "npm run ts-run tasks/validateConfig.ts && prisma generate && prisma db push && npm run build",
+ "dev": "remix vite:dev",
+ "lint": "eslint \"**/*.{js,jsx,ts,tsx}\" --cache",
+ "prepare": "[ \"$NODE_ENV\" != \"production\" ] && husky || :",
+ "start": "remix-serve ./build/server/index.js",
+ "test:vitest": "vitest --run",
+ "test:vitest:watch": "vitest",
+ "ts-check": "tsc --project tsconfig.json --noEmit --skipLibCheck",
+ "ts-run": "dotenv ts-node",
+ "validate-env": "npm run ts-run tasks/validateConfig.ts"
+ },
+ "prisma": {
+ "seed": "tsx prisma/seed.ts"
+ },
+ "lint-staged": {
+ "*.{js,ts,jsx,tsx}": "eslint --cache --fix"
+ },
+ "dependencies": {
+ "@prisma/client": "5.15.0",
+ "@prisma/extension-accelerate": "^1.1.0",
+ "@remix-run/node": "^2.9.2",
+ "@remix-run/react": "2.9.2",
+ "@remix-run/serve": "^2.9.2",
+ "accept-language-parser": "^1.5.0",
+ "clsx": "^2.1.1",
+ "date-fns": "^3.6.0",
+ "isbot": "^5.1.9",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "openflights-cached": "^1.3.15",
+ "postcss": "^8.4.38",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-hot-toast": "^2.4.1",
+ "remix": "2.9.2",
+ "tailwind-merge": "^2.3.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^3.23.8",
+ "zod-error": "^1.5.0",
+ "zod-form-data": "^2.0.2"
+ },
+ "devDependencies": {
+ "@faker-js/faker": "^8.4.1",
+ "@remix-run/dev": "^2.9.2",
+ "@remix-run/eslint-config": "^2.9.2",
+ "@remix-run/testing": "^2.9.2",
+ "@testing-library/jest-dom": "^6.4.6",
+ "@testing-library/react": "^16.0.0",
+ "@types/accept-language-parser": "^1.5.6",
+ "@types/lodash": "^4.17.5",
+ "@types/node": "^20.14.2",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@typescript-eslint/eslint-plugin": "^7.13.0",
+ "@typescript-eslint/parser": "^7.13.0",
+ "@vitest/ui": "^1.6.0",
+ "autoprefixer": "^10.4.19",
+ "dotenv": "^16.4.5",
+ "dotenv-cli": "^7.4.2",
+ "eslint": "^8.57.0",
+ "eslint-config-prettier": "^9.1.0",
+ "eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-jsx-a11y": "^6.8.0",
+ "eslint-plugin-lodash": "^7.1.0",
+ "eslint-plugin-prettier": "^5.1.3",
+ "eslint-plugin-react": "^7.34.2",
+ "eslint-plugin-react-hooks": "^4.6.2",
+ "eslint-plugin-testing-library": "^6.2.2",
+ "jsdom": "^24.1.0",
+ "lint-staged": "^15.2.7",
+ "prettier": "^3.3.2",
+ "prisma": "5.15.0",
+ "tailwindcss": "^3.4.4",
+ "ts-node": "^10.9.2",
+ "tsx": "^4.15.7",
+ "typescript": "^5.4.5",
+ "vite": "^5.3.1",
+ "vite-tsconfig-paths": "^4.3.2",
+ "vitest": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=20.14.0"
+ }
+}
diff --git a/accelerate/remix-starter/postcss.config.cjs b/accelerate/remix-starter/postcss.config.cjs
new file mode 100644
index 000000000000..12a703d900da
--- /dev/null
+++ b/accelerate/remix-starter/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/accelerate/remix-starter/prisma/schema.prisma b/accelerate/remix-starter/prisma/schema.prisma
new file mode 100644
index 000000000000..cda0ee7afb1e
--- /dev/null
+++ b/accelerate/remix-starter/prisma/schema.prisma
@@ -0,0 +1,15 @@
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgres"
+ url = env("DATABASE_URL")
+ directUrl = env("DIRECT_DATABASE_URL")
+}
+
+model Quotes {
+ id Int @id @default(autoincrement())
+ quote String
+ createdAt DateTime @default(now())
+}
diff --git a/accelerate/remix-starter/prisma/seed.ts b/accelerate/remix-starter/prisma/seed.ts
new file mode 100644
index 000000000000..00382053bb84
--- /dev/null
+++ b/accelerate/remix-starter/prisma/seed.ts
@@ -0,0 +1,22 @@
+import { PrismaClient } from "@prisma/client";
+const prisma = new PrismaClient();
+
+async function main() {
+ console.log("Seeding started...");
+ await prisma.quotes.create({
+ data: {
+ quote:
+ "Prisma Accelerate and Remix together make a powerhouse combo, boosting performance and simplifying full-stack development with ease and efficiency.",
+ },
+ });
+ console.log("Seeding completed");
+}
+main()
+ .then(async () => {
+ await prisma.$disconnect();
+ })
+ .catch(async (e) => {
+ console.error(e);
+ await prisma.$disconnect();
+ process.exit(1);
+ });
diff --git a/accelerate/remix-starter/public/favicon.ico b/accelerate/remix-starter/public/favicon.ico
new file mode 100644
index 000000000000..8830cf6821b3
Binary files /dev/null and b/accelerate/remix-starter/public/favicon.ico differ
diff --git a/accelerate/remix-starter/tailwind.config.js b/accelerate/remix-starter/tailwind.config.js
new file mode 100644
index 000000000000..6f2c9c2c2bae
--- /dev/null
+++ b/accelerate/remix-starter/tailwind.config.js
@@ -0,0 +1,71 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ darkMode: ["class"],
+ content: ["./app/**/*.{html,js,jsx,ts,tsx}"],
+ theme: {
+ container: {
+ center: true,
+ padding: "2rem",
+ screens: {
+ "2xl": "1400px",
+ },
+ },
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ keyframes: {
+ "accordion-down": {
+ from: { height: 0 },
+ to: { height: "var(--radix-accordion-content-height)" },
+ },
+ "accordion-up": {
+ from: { height: "var(--radix-accordion-content-height)" },
+ to: { height: 0 },
+ },
+ },
+ animation: {
+ "accordion-down": "accordion-down 0.2s ease-out",
+ "accordion-up": "accordion-up 0.2s ease-out",
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+};
diff --git a/accelerate/remix-starter/tasks/validateConfig.ts b/accelerate/remix-starter/tasks/validateConfig.ts
new file mode 100644
index 000000000000..c924848b4a12
--- /dev/null
+++ b/accelerate/remix-starter/tasks/validateConfig.ts
@@ -0,0 +1,11 @@
+import { globalEnvSchema } from "../app/env/globalEnv";
+import { serverEnvSchema } from "../app/env/envFlags.server";
+
+const serverEnvResult = serverEnvSchema.safeParse(process.env);
+const clientEnvResult = globalEnvSchema.safeParse(process.env);
+
+if (!serverEnvResult.success || !clientEnvResult.success) {
+ process.exit(-1);
+}
+
+console.log("Client and server env vars are valid!");
diff --git a/accelerate/remix-starter/tsconfig.json b/accelerate/remix-starter/tsconfig.json
new file mode 100644
index 000000000000..aa8718e06d3d
--- /dev/null
+++ b/accelerate/remix-starter/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "include": ["env.d.ts", "**/*.ts", "**/*.tsx", "app/**/*"],
+ "compilerOptions": {
+ "lib": ["DOM", "DOM.Iterable", "ES2019"],
+ "isolatedModules": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "resolveJsonModule": true,
+ "target": "ES2019",
+ "strict": true,
+ "baseUrl": ".",
+ "paths": {
+ "~/*": ["./app/*"]
+ },
+ "noEmit": true,
+ "allowJs": true,
+ "forceConsistentCasingInFileNames": true,
+ "types": [
+ "@remix-run/node",
+ "vite/client",
+ "@remix-run/react/future/single-fetch.d.ts"
+ ],
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "Bundler"
+ }
+}
diff --git a/accelerate/remix-starter/vercel.json b/accelerate/remix-starter/vercel.json
new file mode 100644
index 000000000000..2798cba5710c
--- /dev/null
+++ b/accelerate/remix-starter/vercel.json
@@ -0,0 +1,27 @@
+{
+ "build": {
+ "env": {
+ "ENABLE_FILE_SYSTEM_API": "1"
+ }
+ },
+ "headers": [
+ {
+ "source": "(.*)",
+ "headers": [
+ {
+ "key": "X-Content-Type-Options",
+ "value": "nosniff"
+ },
+ {
+ "key": "X-Frame-Options",
+ "value": "DENY"
+ },
+ {
+ "key": "Content-Security-Policy",
+ "value": "default-src 'self' 'unsafe-inline' gc.zgo.at; img-src 'self' data:;"
+ },
+ { "key": "X-XSS-Protection", "value": "1" }
+ ]
+ }
+ ]
+}
diff --git a/accelerate/remix-starter/vite.config.ts b/accelerate/remix-starter/vite.config.ts
new file mode 100644
index 000000000000..6b976e2a776d
--- /dev/null
+++ b/accelerate/remix-starter/vite.config.ts
@@ -0,0 +1,21 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { installGlobals } from "@remix-run/node";
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+installGlobals({ nativeFetch: true });
+
+export default defineConfig({
+ plugins: [
+ remix({
+ ignoredRouteFiles: ["**/*.css"],
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ unstable_singleFetch: true,
+ },
+ }),
+ tsconfigPaths(),
+ ],
+});
diff --git a/accelerate/remix-starter/vitest.config.ts b/accelerate/remix-starter/vitest.config.ts
new file mode 100644
index 000000000000..01b2f4d1a572
--- /dev/null
+++ b/accelerate/remix-starter/vitest.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+export default defineConfig({
+ plugins: [tsconfigPaths()],
+ test: {
+ globals: true,
+ environment: "jsdom",
+ setupFiles: "./tests/vitest-setup.ts",
+ exclude: [
+ "./tests/e2e",
+ "**/node_modules/**",
+ "**/dist/**",
+ "**/cypress/**",
+ "**/.{idea,git,cache,output,temp}/**",
+ "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*",
+ ],
+ },
+});