Back to writing

Integrating Third-Party Services in Remix

November 23, 2023

A practical way to wire third-party services into Remix by treating runtime boundaries, environment variables, and SDK placement as separate problems.

Third-party integrations in Remix often feel harder than they should.

Usually that is not because the service itself is especially complicated. It is because the docs were written for React, Next.js, or generic Node, and Remix forces you to be more honest about where code runs.

That is the real source of most integration pain.

Once you break the problem apart, most integrations boil down to three separate questions:

  1. which SDK belongs in which runtime
  2. which config is safe to expose
  3. where the initialized client should live

Treat those as separate decisions and most integrations stop feeling mysterious.

1. Install dependencies by runtime, not by marketing copy

If a service offers a Remix-specific package, use it.

If it does not, be explicit:

  • use a server SDK for server code
  • use a browser or React SDK for client code
  • avoid pretending one package cleanly spans both if it really does not

This is where a lot of wasted time starts. People install the package that looks closest to their stack, then spend hours debugging import errors that are really runtime mismatches.

One common example is an ESM-only server dependency. In Remix, that may need bundling help through serverDependenciesToBundle. If you skip that detail, the integration can look broken long before you reach any business logic.

2. Pass only the config the browser is allowed to know

Most services give you at least two classes of configuration:

  • private keys for server-side work
  • public keys for browser initialization

Those should not be treated the same way.

Keep the secret on the server. Pass only the public value through the root loader or whatever boundary makes sense for your app.

export async function loader() {
  return json({
    ENV: {
      SOME_SERVICE_PUBLIC_KEY: process.env.SOME_SERVICE_PUBLIC_KEY,
    },
  });
}

Then expose it to the client deliberately:

<script
  dangerouslySetInnerHTML={{
    __html: `window.ENV = ${JSON.stringify(data.ENV)}`,
  }}
/>

This part is simple, but it is also the place where people most often get sloppy. The useful rule is easy to remember:

If the browser can read it, assume the whole world can read it.

That means secrets stay in loaders, actions, or server-only modules.

3. Put the SDK where its lifecycle makes sense

Once you have the right package and the right config, the last question is where the initialized SDK should live.

For server code, a dedicated .server.ts file is usually the cleanest home:

import { SomeService } from "some-service/node";
 
export const someService = new SomeService(
  process.env.SOME_SERVICE_SECRET_KEY,
);

For the client, there are usually two reasonable shapes:

  • initialize the SDK in a provider near the root if the whole app needs it
  • export a client-only instance from a .client.ts module if usage is more localized

That choice matters more than it seems. A lot of integration bugs are really just lifecycle bugs in disguise. The code works, but it is initialized in the wrong place, recreated too often, or tries to run in the wrong environment.

A practical example of the failure mode

This is the pattern I keep seeing:

  1. install one SDK
  2. import it everywhere
  3. reference process.env from shared code
  4. discover that part of the code now breaks in the browser

At that point the integration looks messy, but the root problem is still simple: server concerns and browser concerns were mixed too early.

Remix is actually helpful here because it makes that split visible.

The mental model I keep coming back to

Whenever an integration feels frustrating, I ask three questions:

  1. Which runtime does this code belong to?
  2. Which values are safe to expose?
  3. Where should this SDK instance live?

If those answers are clean, the integration is usually clean too.

Most of the pain comes from trying to answer all three at once.