remix-run / remix

Build Better Websites. Create modern, resilient user experiences with web fundamentals.
https://remix.run
MIT License
29.42k stars 2.48k forks source link

Environment variables on Cloudflare Pages #6868

Open raulrpearson opened 1 year ago

raulrpearson commented 1 year ago

What version of Remix are you using?

1.18.1

Are all your remix dependencies & dev-dependencies using the same version?

Steps to Reproduce

I try accessing env vars following instructions from the docs:

export const loader = async ({ context }: LoaderArgs) => {
  console.log(context.SOME_SECRET);
};

This didn't work, and debugging led me to realise that I could find env vars under context.env.SOME_SECRET instead.

I considered just submitting a PR to the docs page, but I'm not sure if there's other changes required and thought it'd be better to first open up an issue. I'm typing my action function like this:

import type { ActionFunction } from "@remix-run/cloudflare"
//...
export const action: ActionFunction = async ({ request, context }) => {
//...
}

But get I get the following type error when trying to access context.env.SOME_SECRET, which makes me think this might warrant more than just a docs fix:

'context.env' is of type 'unknown'.ts(18046)
(parameter) context: AppLoadContext

Finally, this seems connected to the code coming from the Cloudflare Pages starter server.ts (which I used), which on top of that references process.env.NODE_ENV with process being undefined:

if (process.env.NODE_ENV === "development") {
  logDevReady(build);
}

export const onRequest = createPagesFunctionHandler({
  build,
  getLoadContext: (context) => ({ env: context.env }),
  mode: process.env.NODE_ENV,
});

Should this be changed to something using CF_PAGES or some other custom env var to determine dev vs prod?

I'd be happy to open PR if that'd be helpful.

Expected Behavior

Correct docs and types.

Actual Behavior

Incorrect docs and types.

arjunyel commented 1 year ago

EDITED:

In remix.env.d.ts try adding:

/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/cloudflare" />
/// <reference types="@cloudflare/workers-types" />
import type { AppLoadContext as OriginalAppLoadContext } from "@remix-run/server-runtime";

declare module "@remix-run/server-runtime" {
  export interface AppLoadContext extends OriginalAppLoadContext {
    env: {
      SOME_SECRET: string;
    };
  }
}

From: https://sergiodxa.com/tutorials/measure-performance-with-the-server-timing-header-in-remix

mjackson commented 1 year ago

Seems like this is a 2 part fix:

dhushyanth-s commented 1 year ago

Using other Cloudflare offerings like R2, KV etc also involves setting up bindings which finally end up in context.env and suffer from the same problems. Maybe a more general solution for all bindings will be better?

aaronadamsCA commented 1 year ago

My current solution for this is to use Zod to parse context.env. This actually validates the Cloudflare environment instead of naively typing it:

// server.ts
import { logDevReady } from "@remix-run/cloudflare";
import { createPagesFunctionHandler } from "@remix-run/cloudflare-pages";
import * as build from "@remix-run/dev/server-build";
import { z } from "zod";

export const AppEnvSchema = z.object({
  SOME_SECRET: z.string(),
  SOME_BINDING: z.custom<KVNamespace>((value) => value != null),
});

declare module "@remix-run/cloudflare" {
  interface AppLoadContext {
    env: z.output<typeof AppEnvSchema>;
  }
}

if (process.env.NODE_ENV === "development") {
  logDevReady(build);
}

export const onRequest = createPagesFunctionHandler({
  build,
  getLoadContext: (context) => {
    const env = AppEnvSchema.parse(context.env);
    return { env };
  },
  mode: process.env.NODE_ENV,
});

This is, for us, a robust solution, with fully typed context available in loaders and actions. If the environment is invalid, the app breaks with an obvious error, instead of the unpredictable, hard-to-diagnose failures that can come from incorrectly typed values.

process being undefined is indeed a problem right now, which is how I found this issue just now. The workaround is to add this to your server.ts:

declare global {
  const process: {
    env: ProcessEnv;
  };
}

I'll file a separate issue for this last part now (edit: #7382).

IMO the rest really just comes down to documentation, as the current capabilities are robust enough once you figure out how to fit them together.

dhushyanth-s commented 1 year ago

Using Zod seems like a really good solution for this problem, especially since all the bindings are already there and the typings required depends on the particular use-cases and cannot be generalised. Thank you, Ill be using your method.

lovelidevs commented 1 year ago

In remix.env.d.ts try adding:

declare module "@remix-run/server-runtime" {
  export interface AppLoadContext {
    SOME_SECRET: string;
  }
}

From: https://sergiodxa.com/tutorials/measure-performance-with-the-server-timing-header-in-remix

This seems to do the trick?

declare module "@remix-run/server-runtime" {
  export interface AppLoadContext {
    env: {
      SOME_SECRET: string;
    };
  }
}
arjunyel commented 9 months ago

This is what I currently have in remix.env.d.ts

/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/cloudflare" />
/// <reference types="@cloudflare/workers-types" />
import type { AppLoadContext as OriginalAppLoadContext } from "@remix-run/server-runtime";

declare module "@remix-run/server-runtime" {
  export interface AppLoadContext extends OriginalAppLoadContext {
    env: {
      SOME_SECRET: string;
    };
  }
}
aaronadamsCA commented 7 months ago

Just to help anyone with the v2.7.0 upgrade, note there was a breaking change and you'll need to update your server.ts, for example:

Details—fixed in v2.7.1 ```diff export const onRequest = createPagesFunctionHandler({ build, - getLoadContext: (context) => { - const env = AppEnvSchema.parse(context.env); + getLoadContext: ({ context }) => { + const env = AppEnvSchema.parse(context.cloudflare.env); return { env }; }, mode: process.env.NODE_ENV, }); ```

Once you're ready to make the leap to Vite, I'd suggest migrating your server.ts file to a getLoadContext.ts file, then reusing it across both functions/[[path]].ts and vite.config.ts, as per the migration docs.

That could look something like this example, which also sets up strongly typed session storage:

Details—updated to correct types ```ts import type { AppLoadContext, SessionStorage } from "@remix-run/cloudflare"; import { createWorkersKVSessionStorage } from "@remix-run/cloudflare"; import type { PlatformProxy } from "wrangler"; import { z } from "zod"; declare module "@remix-run/cloudflare" { interface AppLoadContext { env: { SOME_SECRET: string; }; sessionStorage: SessionStorage< { someSessionData: string }, { someFlashData: string } >; } } const schema = z.object({ SOME_BINDING: z.custom((value) => value != null), SOME_SECRET: z.string(), }); interface CloudflareLoadContext { context: { cloudflare: Omit, "dispose">; }; request: Request; } export function getLoadContext({ context, }: CloudflareLoadContext): AppLoadContext { const { SOME_BINDING, SOME_SECRET } = schema.parse(context.cloudflare.env); const sessionStorage = createWorkersKVSessionStorage({ kv: SOME_BINDING }); return { env: { SOME_SECRET }, sessionStorage, }; } ```

Do note you'll get a type mismatch when you pass this getLoadContext to cloudflareDevProxyVitePlugin; for some reason the types don't quite match up yet between the two Remix libraries.

Still, this already feels like a huge leap forward! Really appreciate all the effort that's gone into making Remix + Cloudflare + Vite work well together.

pcattori commented 7 months ago

We've added a backcompat fix in 2.7.1 so that upgrading from an older version to 2.7.1 isn't a breaking change for createPagesFunctionHandler

pcattori commented 7 months ago

Do note you'll get a type mismatch when you pass this getLoadContext to cloudflareDevProxyVitePlugin; for some reason the types don't quite match up yet between the two Remix libraries.

@aaronadamsCA the getLoadContextFunction from @remix-run/cloudflare-pages is a wide, general type so type errors are expected if you use it directly. That's why the load-context.ts file from the Vite Cloudflare template defines its own GetLoadContext type that is aware of the types generated by Wrangler.

aaronadamsCA commented 7 months ago

@pcattori Thanks for that, I definitely missed this detail! I've corrected my prior post.

om154 commented 7 months ago

I'm currently using Remix/Vite with Cloudflare Pages and I'm managing sessions in a sessions.server.ts file as suggested in the docs and I intend to store the session secret in my environment variables.

I'm wondering: how can I access the load context outside of a loader/action?

I did see these docs on how to get them, but it only includes in loaders/actions. Any tips?

Thanks and nice work on shipping Vite support!

pcattori commented 7 months ago

@pcattori Thanks for that, I definitely missed this detail! I've corrected my prior post.

No, thank you @aaronadamsCA ! It was your detailed and early bug report for the breaking change that let us fix it so quickly! 🙏

pcattori commented 7 months ago

@om154 Can you share where/how you want to access the context? Context is a server-side concept so it wouldn't be safe to use it directly in shared client/server code like the component.

shaqq commented 6 months ago

I have the same issue as @om154. I want to instantiate an API client with environment variables, and instantiate them only once. It's easy to do with process.env, but with the Cloudflare context only available during a request, I now have to memoize them between requests (previously I did not have to do that).

sergiodxa commented 6 months ago

@shaqq there's no need to memoize them between requests, because in CF every request will be a new instance of the worker with separate memory, so creating the instance one at module level or per request is the same.

arjunyel commented 6 months ago

@shaqq there's no need to memoize them between requests, because in CF every request will be a new instance of the worker with separate memory, so creating the instance one at module level or per request is the same.

This is generally true but actually a Cloudflare Worker (and Demo Deploy) isolate can be reused if it's still in memory when a new request comes in. So sometimes it can reuse the same instance and memoization can help. Fastly compute @ edge is the only platform I am aware of where each request is guaranteed a new instance. From here: https://hono.dev/api/presets#which-preset-should-i-use

shaqq commented 6 months ago

In general it feels like the point is moot - Cloudflare injects the context at request time. So I don't think there's anything we can do here outside of giving this feedback to Cloudflare engineers.

ThisIsRahmat commented 5 months ago

I'm currently using Remix/Vite with Cloudflare Pages and I'm managing sessions in a sessions.server.ts file as suggested in the docs and I intend to store the session secret in my environment variables.

I'm wondering: how can I access the load context outside of a loader/action?

I did see these docs on how to get them, but it only includes in loaders/actions. Any tips?

Thanks and nice work on shipping Vite support!

Hi @om154 did you find a workaround for this?

I am having the similar porblem. I am using cloudflare pages with Supabase for authenthication and I have a supabase.server.ts file with all the standard details (see below) but I don't know how to fetch the environment variables from context outside of using loaders and functions.


`import { createServerClient, parse, serialize } from "@supabase/ssr";

export function createClient(request: Request) {
  const cookies = parse(request.headers.get("Cookie") ?? "");
  const supabase = {
    headers: new Headers(),
    client: createServerClient(
      context.cloudflare.env.SUPABASE_URL!,
      context.cloudflare.env.SUPABASE_ANON_KEY!,
      {
        cookies: {
          get(key) {
            return cookies[key];
          },
          set(key, value, options) {
            supabase.headers.append(
              "Set-Cookie",
              serialize(key, value, options)
            );
          },
          remove(key, options) {
            supabase.headers.append("Set-Cookie", serialize(key, "", options));
          },
        },
      }
    ),
  };

  return supabase;
}`
predaytor commented 5 months ago

@ThisIsRahmat, you can add a second argument to pass the context:

import type { AppLoadContext } from '@remix-run/cloudflare';

export function createClient(request: Request, context: AppLoadContext) {
    // ...
}

but it's generally better to initialize the supabase client in getLoadContext directly to access it later through the context itself:

import { type PlatformProxy } from 'wrangler';

// When using `wrangler.toml` to configure bindings,
// `wrangler types` will generate types for those bindings
// into the global `Env` interface.

type Cloudflare = Omit<PlatformProxy<Env>, 'dispose'>;

type GetLoadContextArgs = { request: Request; context: { cloudflare: Cloudflare } };

export function getLoadContext({ request, context }: GetLoadContextArgs) {  
        const supabase = createClient(request, context);

    return {
        ...context,
        supabase,
    };
}

declare module '@remix-run/cloudflare' {
    interface AppLoadContext extends ReturnType<typeof getLoadContext> {}
}
anyuruf commented 4 months ago

@om154 Create an extra abstraction and include context as an input in the createSession function you have created then call it from a loader function. That is what I did check my /lib/sessions.server.ts & root.tsx from https://github.com/anyuruf/kinstree-remix

export function getSessionStorage(env: Env) {
  if (!env.SESSION_SECRET) throw new Error("SESSION_SECRET is not defined");

  return createCookieSessionStorage({
    cookie: {
      httpOnly: true,
      name: "theme",
      path: "/",
      sameSite: "lax",
      secrets: [env.SESSION_SECRET],
       // Set domain and secure only if in production
    ...(isProduction
      ? { domain: "Kinstree.com", secure: true }
      : {}),
    },
  });
export function getThemeSessionResolver (env:Env) {
  return createThemeSessionResolver(getSessionStorage(env))
}
// Return the theme from the session storage using the loader
export async function loader({ context, request }: LoaderFunctionArgs) {
    const themeSessionResolver = getThemeSessionResolver(context.env);
    const { getTheme } = await themeSessionResolver(request);
    return {
        theme: getTheme(),
    };
}
Sushant-Borsania commented 4 months ago

I do have this code and would like to understand how to access the environment variable in this function on cloudflare pages. Can somebody please help?

import { createCookieSessionStorage } from "@remix-run/cloudflare";

export const { getSession, commitSession, destroySession } = createCookieSessionStorage({
  cookie: {
    name: "remix_session",
    secrets: [`${process.env.SESSION_SECRET}`],
    sameSite: "lax",
    path: "/",
    secure: process.env.NODE_ENV === "production",
    httpOnly: true,
    maxAge: 60 * 60 * 24 * 30, // 30 days
  },
});

Thank you.

sergiodxa commented 4 months ago

@Sushant-Borsania you will need to call createCookieSessionStorage in a function that receives the session secret and then call that in the loader or action that needs to use the session storage object.