> For the complete documentation index, see [llms.txt](https://fusionauth.io/docs/llms.txt)

# Remix | FusionAuth Docs

Quickstart integration of Javascript Remix web application with FusionAuth.

# Remix

[Edit on GitHub](https://github.com/FusionAuth/fusionauth-site/blob/main/astro/src/content/docs/get-started/quickstarts/web/quickstart-javascript-remix-web.mdx)

[View Markdown](https://fusionauth.io/docs/get-started/quickstarts/web/quickstart-javascript-remix-web.md)

In this quickstart, you are going to build an application with Remix and integrate it with FusionAuth. You'll be building it for [ChangeBank](https://www.youtube.com/watch?v=CXDxNCzUspM), a global leader in converting dollars into coins. It'll have areas reserved for users who have logged in as well as public facing sections.

The Docker Compose file and source code for a complete application are available at [https://github.com/FusionAuth/fusionauth-quickstart-javascript-remix-web](https://github.com/FusionAuth/fusionauth-quickstart-javascript-remix-web).

## Prerequisites[#](#prerequisites)

For this Quickstart, you'll need:

*   [Node 18](https://nodejs.org/en/download) or later.
*   [Docker](https://www.docker.com), which is the quickest way to start FusionAuth. (There are [other ways](https://fusionauth.io/docs/get-started/download-and-install.md)).

## General Architecture[#](#general-architecture)

Here's a typical application login flow before FusionAuth:

```mermaid
sequenceDiagram
    participant User
    participant App as Application

    User ->> App : View Homepage
    User ->> App : Click Login Link
    App ->> User : Show Login Form
    User ->> App : Fill Out and Submit Login Form
    App ->> App : Authenticates User
    App ->> User : Display User's Account or Other Info
```

Request flow during login before FusionAuth

And here's the same application login flow after introducing FusionAuth:

```mermaid
sequenceDiagram
    participant User
    participant App as Application
    participant FusionAuth

    User ->> App : View Homepage
    User ->> App : Click Login Link (to FusionAuth)
    User ->> FusionAuth : View Login Form
    FusionAuth ->> User : Show Login Form
    User ->> FusionAuth : Fill Out and Submit Login Form
    FusionAuth ->> FusionAuth : Authenticates User
    FusionAuth ->> User: Go to Redirect URI
    User ->> App: Request the Redirect URI
    App ->> FusionAuth : Is User Authenticated?
    FusionAuth ->> App : User is Authenticated
    App ->> User : Display User's Account or Other Info
```

Request flow during login after FusionAuth

In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.

## Getting Started[#](#getting-started)

Start with getting FusionAuth up and running and creating a new Remix application.

Remix has been merged into [React Router](https://reactrouter.com), which is now the recommended way to build Remix-style apps. This guide uses React Router's tooling and current APIs throughout.

### Clone The Code[#](#clone-the-code)

First, grab the code from the repository and change into that folder.

```plaintext
git clone https://github.com/FusionAuth/fusionauth-quickstart-javascript-remix-web.git
cd fusionauth-quickstart-javascript-remix-web
```

All shell commands in this guide can be entered in a terminal in this folder. On Windows, you need to replace forward slashes with backslashes in paths.

All the files you'll create in this guide already exist in the `complete-application` subfolder, if you prefer to copy them.

### Run FusionAuth Via Docker[#](#run-fusionauth-via-docker)

You'll find a Docker Compose file (`docker-compose.yml`) and an environment variables configuration file (`.env`) in the root directory of the repo.

Assuming you have Docker installed, you can stand up FusionAuth on your machine with the following.

```shell-session
docker compose up -d
```

Here you are using a bootstrapping feature of FusionAuth called [Kickstart](https://fusionauth.io/docs/get-started/download-and-install/development/kickstart.md). When FusionAuth comes up for the first time, it will look at the `kickstart/kickstart.json` file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth application, you need to delete the volumes created by Docker Compose by executing `docker compose down -v`, then re-run `docker compose up -d`.

FusionAuth will be initially configured with these settings:

*   Your client Id is `e9fdb985-9173-4e01-9d73-ac2d60d1dc8e`.
*   Your client secret is `super-secret-secret-that-should-be-regenerated-for-production`.
*   Your example username is `richard@example.com` and the password is `password`.
*   Your admin username is `admin@example.com` and the password is `password`.
*   The base URL of FusionAuth is `http://localhost:9011/`.

You can log in to the [FusionAuth admin UI](http://localhost:9011/admin) and look around if you want to, but with Docker and Kickstart, everything will already be configured correctly.

If you want to see where the FusionAuth values came from, they can be found in the [FusionAuth app](http://localhost:9011/admin). The tenant Id is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the `View` icon under the actions for the ChangeBank application. You'll find the Client Id and Client Secret values in the `OAuth configuration` section.

The `.env` file contains passwords. In a real application, always add this file to your `.gitignore` file and never commit secrets to version control.

### Create A Basic Remix Application[#](#create-a-basic-remix-application)

Next, you'll set up a basic Remix app using React Router's scaffolding tool. While this guide builds a new project, you can use the same method to integrate your existing project with FusionAuth.

Create the default starter project.

```shell
npx create-react-router@latest mysite
```

If `create-react-router` isn't on your machine, npx will prompt you to install it. Confirm the installation with a `y` input.

When prompted, choose **No** for the deployment target ("Do you want me to set up that too?"). This guide runs the app on Vite's built-in server, not a specific hosting target.

Change into the new project and pin the dev server to port 3000. Open `mysite/vite.config.ts` and add a `server` block:

```ts
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouter()],
  resolve: {
    tsconfigPaths: true,
  },
  server: {
    port: 3000,
  },
});
```

By default, the Vite dev server runs on port `5173`. This Quickstart, however, expects the Vite dev server to run on port `3000`. Without this change, your OAuth login flow will fail with a redirect URI mismatch.

Add additional npm packages the site will use using the commands below.

```shell
cd mysite
npm install dotenv remix-auth remix-auth-oauth2 jwt-decode
```

The two Remix authentication packages allow you to use FusionAuth without needing to know how OAuth works. The JWT package will provide your app with the user details returned from FusionAuth. The `dotenv` package allows the app to read configuration details from a `.env` environment file.

Now create the `.env` file in your `mysite` folder and add the following to it (note that this is a different environment file to the one in the root folder used by Docker for FusionAuth).

```shell
CLIENT_ID="e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
CLIENT_SECRET="super-secret-secret-that-should-be-regenerated-for-production"
AUTH_URL="http://localhost:9011/oauth2"
AUTH_CALLBACK_URL="http://localhost:3000/auth/callback"
```

### Preliminary Setup[#](#preliminary-setup)

Before you configure authentication, you need to change the default files created by the scaffolding tool.

First, **delete the scaffolded `mysite/app/routes/home.tsx` file and the `mysite/app/welcome/` folder.** You'll add your own routes as you go, so these defaults aren't needed.

Next, replace the contents of the file `mysite/app/root.tsx` with the following code.

```typescript
import {
  isRouteErrorResponse,
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "react-router";

import type { Route } from "./+types/root";
import "~/css/changebank.css";

export const links: Route.LinksFunction = () => [];

export function meta({}: Route.MetaArgs) {
  return [{ title: "FusionAuth OpenID and PKCE example" }];
}

export function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App() {
  return <Outlet />;
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
  let message = "Oops!";
  let details = "An unexpected error occurred.";
  let stack: string | undefined;

  if (isRouteErrorResponse(error)) {
    message = error.status === 404 ? "404" : "Error";
    details =
      error.status === 404
        ? "The requested page could not be found."
        : error.statusText || details;
  } else if (import.meta.env.DEV && error && error instanceof Error) {
    details = error.message;
    stack = error.stack;
  }

  return (
    <main>
      <h1>{message}</h1>
      <p>{details}</p>
      {stack && (
        <pre>
          <code>{stack}</code>
        </pre>
      )}
    </main>
  );
}
```

This is what these changes do:

*   Import the app's styling with `import "~/css/changebank.css";` at the top.
*   Specify the `<head>` content for the page (title and CSS link) in `meta` and the `Layout` component.
*   Define an `ErrorBoundary` so unhandled route errors render a plain error page instead of crashing.

Routes are no longer discovered automatically by filename. Instead, `mysite/app/routes.ts` lists every route explicitly. Create it with the following content. You'll add to this file as you create each route below.

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/_index.tsx"),
] satisfies RouteConfig;
```

If you'd rather keep the older filename-based routing convention (where a file's name determines its URL), you can install the `@react-router/fs-routes` package instead of listing routes by hand. This guide uses explicit registration since it's the more current, idiomatic approach.

## Authentication[#](#authentication)

Authentication in Remix requires two parts:

*   An Authenticator service to handle communication with FusionAuth.
*   Routes (login, logout, callback) that talk to the Authenticator.

### Services[#](#services)

The `app` folder in the `mysite` project holds all your code. The `routes` folder holds your application's pages. Let's make a `services` folder to keep your authentication code separate.

```shell
mkdir app/services
```

In this `services` folder, add a file called `session.server.ts` and copy in the following code (this guide follows the example from the [Remix Auth README](https://github.com/sergiodxa/remix-auth)).

```typescript
import { createCookieSessionStorage } from "react-router";

// export the whole sessionStorage object
export let sessionStorage = createCookieSessionStorage({
  cookie: {
    name: "_session", // use any name you want here
    sameSite: "lax", // this helps with CSRF
    path: "/", // remember to add this so the cookie will work in all routes
    httpOnly: true, // for security reasons, make this cookie http only
    secrets: ["s3cr3t"], // replace this with an actual secret
    secure: process.env.NODE_ENV === "production", // enable this in prod only
  },
});

// you can also export the methods individually for your own usage
export let { getSession, commitSession, destroySession } = sessionStorage;
```

This code provides a way for the app to store cookies that maintain the user's session. (If you'd prefer to use file-based sessions instead of cookie-based, you can read about it in an earlier [blog post about using Remix](https://fusionauth.io/blog/remix-demo.md).)

Next, create an authentication file in the `services` folder, `auth.server.ts`, that uses the session provider you just created.

```typescript
import { jwtDecode } from "jwt-decode";
import { Authenticator } from "remix-auth";
import type { OAuth2Strategy as OAuth2StrategyType } from "remix-auth-oauth2";
import { OAuth2Strategy } from "remix-auth-oauth2";

type User = string;
export let authenticator = new Authenticator<User>();

const authOptions: OAuth2StrategyType.ConstructorOptions = {
    authorizationEndpoint: `${process.env.AUTH_URL}/authorize`,
    tokenEndpoint: `${process.env.AUTH_URL}/token`,
    clientId: process.env.CLIENT_ID!,
    clientSecret: process.env.CLIENT_SECRET!,
    redirectURI: process.env.AUTH_CALLBACK_URL!,
    scopes: ["openid", "email", "profile", "offline_access"],
};

const authStrategy = new OAuth2Strategy(
    authOptions,
    async ({tokens}) => {
        const jwt = await jwtDecode<any>(tokens.idToken());
        return jwt?.email || "missing email check scopes";
    }
);

authenticator.use(
    authStrategy,
    "FusionAuth"
);
```

In the line `new Authenticator<User>()`, the type parameter specifies which user details you want to keep in the session. This example uses a single string containing the user's email, but you could define an object with more details or just a user Id.

In `authOptions`, you specify the FusionAuth URLs stored in the `.env` file, which match those created by Kickstart when you ran the Docker command to start FusionAuth.

Finally, `authStrategy` returns the user's email, decoded from the ID token returned by FusionAuth.

Unlike some older authentication setups, `authenticator.authenticate()` here only returns the logged-in user, and no longer commits anything to the session automatically. Each route below is responsible for reading, updating, and saving the session cookie itself.

### Routes[#](#routes)

Create the homepage of your app at `mysite/app/routes/_index.tsx` with the following code:

```typescript
import { Link, redirect } from "react-router";
import { sessionStorage } from "~/services/session.server";
import type { Route } from "./+types/_index";

export async function loader({ request }: Route.LoaderArgs) {
  const session = await sessionStorage.getSession(request.headers.get("cookie"));
  const user = session.get("user");
  if (user) throw redirect("/account");
  return null;
}

export default function Index() {
  return (
    <div id="page-container">
      <div id="page-header">
        <div id="logo-header">
          <img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
          <Link to="/login" className="button-lg">Login</Link>
        </div>

        <div id="menu-bar" className="menu-bar">
          <a className="menu-link">About</a>
          <a className="menu-link">Services</a>
          <a className="menu-link">Products</a>
          <a className="menu-link" style={{textDecorationLine: 'underline'}}>Home</a>
        </div>
      </div>

      <div style={{flex: '1'}}>
        <div className="column-container">
          <div className="content-container">
            <div style={{marginBottom: '100px'}}>
              <h1>Welcome to Changebank</h1>
              <p>To get started, <Link to="/login" >log in or create a new account</Link>.</p>
            </div>
          </div>
          <div style={{flex: '0'}}>
            <img src="/money.jpg" style={{maxWidth: '800px'}}/>
          </div>
        </div>
      </div>
    </div>
  );
}
```

Remix extends React with a few more custom elements that automatically hide the difference between client and server from the programmer. For example, the line `<Link to="/login" className="button-lg">Login</Link>` allows the app to load the linked page in the background without a separate page refresh.

If you have not worked with React before, there are a few differences from normal HTML to note:

*   The **class** attribute is replaced by `className`, as `class` is a reserved word in JavaScript.
*   To add JavaScript values into your HTML, you wrap the value in `{ }`.
*   The `style` values are not strings but object literals in braces, wrapped in more braces.

The `_index.tsx` file checks the session cookie for a logged-in user, and redirects to the account page if one is found. This check does not call FusionAuth; it only reads the user's existing cookie.

Note that this code runs in a `loader` function on the server, not the client. `loader` functions run for GET requests and always execute before any HTML is returned to the client. By contrast, the `default` function (that returns HTML) is exported from a route and runs only in the browser (unless the user browses directly to the route without already being on the site). Since routes mix server-side and client-side code in the same file, it is essential not to put any authentication handling or secrets in the client-side code.

The link you created looks for a login route, so let's code it. Create the file `mysite/app/routes/login.tsx` and add the following code to it.

```typescript
import { authenticator } from "~/services/auth.server";
import type { Route } from "./+types/login";

export async function loader({ request }: Route.LoaderArgs) {
  return await authenticator.authenticate("FusionAuth", request);
}
```

This route is purely server-side, as it exports no default function. Instead, the GET loader function redirects the user to FusionAuth. (The user will automatically be logged in without visiting FusionAuth if they already have a session cookie in their browser).

Now create the file `mysite/app/routes/logout.tsx` and add the following code to it.

```typescript
import { redirect } from "react-router";
import { sessionStorage } from "~/services/session.server";
import type { Route } from "./+types/logout";

export async function loader({ request }: Route.LoaderArgs) {
  const session = await sessionStorage.getSession(request.headers.get("cookie"));

  throw redirect("/", {
    headers: { "Set-Cookie": await sessionStorage.destroySession(session) },
  });
}
```

When a user browses to this route, their session cookie is destroyed and they're redirected to the home page.

The final route to create is `mysite/app/routes/auth.callback.tsx`.

```typescript
import { redirect } from "react-router";
import { authenticator } from "~/services/auth.server";
import { sessionStorage } from "~/services/session.server";
import type { Route } from "./+types/auth.callback";

export async function loader({ request }: Route.LoaderArgs) {
  const user = await authenticator.authenticate("FusionAuth", request);

  const session = await sessionStorage.getSession(request.headers.get("cookie"));
  session.set("user", user);

  throw redirect("/account", {
    headers: { "Set-Cookie": await sessionStorage.commitSession(session) },
  });
}
```

This is where FusionAuth redirects the user after authentication. `authenticator.authenticate` completes the OAuth2 token exchange and returns the user's email; the route then stores that email in the session cookie itself before redirecting to the account page.

If you are wondering why `auth.callback.tsx` has two dots, it's because this routing convention treats the first dot as a slash in the URL, `/auth/callback`.

Now add all the routes to `mysite/app/routes.ts`, replacing its contents with:

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/_index.tsx"),
  route("login", "routes/login.tsx"),
  route("logout", "routes/logout.tsx"),
  route("auth/callback", "routes/auth.callback.tsx"),
] satisfies RouteConfig;
```

A route file that isn't listed in `routes.ts` won't be reachable, even if it exists in the `routes` folder. This is different from older filename-convention routing, which automatically served any file under `routes/`.

## Customization[#](#customization)

Now that authentication is done, the last task is to create two example pages that a user can access only when logged in.

Copy over some CSS and an image from the example app.

```shell
mkdir app/css
cp ../complete-application/app/css/changebank.css ./app/css/changebank.css
cp ../complete-application/public/money.jpg ./public/money.jpg
```

Add the account page `mysite/app/routes/account.tsx` with the following code.

```typescript
import { Link, redirect } from "react-router";
import { sessionStorage } from "~/services/session.server";
import type { Route } from "./+types/account";

export async function loader({ request }: Route.LoaderArgs) {
  const session = await sessionStorage.getSession(request.headers.get("cookie"));
  const email = session.get("user");
  if (!email) throw redirect("/login");
  return email;
}

export default function Account({ loaderData }: Route.ComponentProps) {
  const email = loaderData as string;
  return (
    <div id="page-container">
      <div id="page-header">
        <div id="logo-header">
          <img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
          <div className="h-row">
            <p className="header-email">{email}</p>
            <Link to="/logout" className="button-lg">Logout</Link>
          </div>
        </div>

        <div id="menu-bar" className="menu-bar">
          <Link to="/change" className="menu-link inactive">Make Change</Link>
          <Link to="/account" className="menu-link">Account</Link>
        </div>
      </div>

      <div style={{flex: '1'}}>
        <div className="column-container">
          <div className="app-container">
            <h3>Your balance</h3>
            <div className="balance">$0.00</div>
          </div>
        </div>
      </div>
    </div>
  );
}
```

The `loader` function at the top of the page checks the session cookie and redirects to `/login` if there's no logged-in user. Any page you create that needs the user to be authenticated requires a check like this.

The `loaderData` prop passed into the `Account` component provides whatever data the loader function returns for use in the HTML. It's used in the line `<p className="header-email">{email}</p>`.

The final page you need is `mysite/app/routes/change.tsx`, with the following code.

```typescript
import { Link, redirect } from "react-router";
import { useState } from "react";
import type { ChangeEvent } from "react";
import { sessionStorage } from "~/services/session.server";
import type { Route } from "./+types/change";

export async function loader({ request }: Route.LoaderArgs) {
  const session = await sessionStorage.getSession(request.headers.get("cookie"));
  const email = session.get("user");
  if (!email) throw redirect("/login");
  return email;
}

export default function Change({ loaderData }: Route.ComponentProps) {
  const email = loaderData as string;
  const [state, setState] = useState({error: false, hasChange: false, total: '', nickels: '', pennies: ''});

  function onTotalChange(e: ChangeEvent<HTMLInputElement>): void {
    setState({ ...state, total: e.target.value, hasChange: false });
  }

  function makeChange() {
    const newState = { error: false, hasChange: true, total: '', nickels: '', pennies: ''};
    const total = Math.trunc(parseFloat(state.total)*100)/100;
    newState.total = isNaN(total) ? '' : total.toFixed(2);
    const nickels = Math.floor(total / 0.05);
    newState.nickels = nickels.toLocaleString();
    const pennies = ((total - (0.05 * nickels)) / 0.01);
    newState.pennies = Math.ceil((Math.trunc(pennies*100)/100)).toLocaleString();
    newState.error = ! /^(\d+(\.\d*)?|\.\d+)$/.test(state.total);
    setState(newState);
  }

  return (
    <div id="page-container">
      <div id="page-header">
        <div id="logo-header">
          <img src="https://fusionauth.io/cdn/samplethemes/changebank/changebank.svg" />
          <div className="h-row">
            <p className="header-email">{email}</p>
            <Link to="/logout" className="button-lg">Logout</Link>
          </div>
        </div>

        <div id="menu-bar" className="menu-bar">
          <Link to="/change" className="menu-link inactive">Make Change</Link>
          <Link to="/account" className="menu-link inactive">Account</Link>
        </div>
      </div>

      <div style={{flex: '1'}}>
        <div className="column-container">
          <div className="app-container change-container">
            <h3>We Make Change</h3>

            { state.error && state.hasChange &&
              <div className="error-message"> Please enter a dollar amount </div>
            }

            { !state.hasChange &&
              <div className="error-message"><br/> </div>
            }

            { !state.error && state.hasChange &&
              <div className="change-message">
                We can make change for ${ state.total } with { state.nickels } nickels and { state.pennies } pennies!
              </div>
            }

            <div className="h-row">
              <form onSubmit={(e) => { e.preventDefault(); makeChange(); }} >
                <div className="change-label">Amount in USD: $</div>
                <input className="change-input" name="amount" value={state.total} onChange={onTotalChange} />
                <input className="change-submit" type="submit" value="Make Change" />
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}
```

This page has identical authentication functionality to the Account page and some pure React code that updates the DOM whenever the state changes.

Note that `ChangeEvent` is imported with `import type` rather than as a regular import, since it's only a TypeScript type, not a value.

Finally, add both pages to `mysite/app/routes.ts`:

```ts
import { type RouteConfig, index, route } from "@react-router/dev/routes";

export default [
  index("routes/_index.tsx"),
  route("login", "routes/login.tsx"),
  route("logout", "routes/logout.tsx"),
  route("auth/callback", "routes/auth.callback.tsx"),
  route("account", "routes/account.tsx"),
  route("change", "routes/change.tsx"),
] satisfies RouteConfig;
```

## Run The Application[#](#run-the-application)

Start the server from the root `mysite` directory.

```shell
npm run dev
```

You can now browse to [the app](http://localhost:3000).

Log in using `richard@example.com` and `password`. The change page will allow you to enter a number. Log out and verify that you can't browse to the [account page](http://localhost:3000/account).

## Made it this far? Want a free t-shirt? We got ya.

Thank you for spending some time getting familiar with FusionAuth.

\*Offer only valid in the United States and Canada, while supplies last.

## Next Steps[#](#next-steps)

This quickstart is a great way to get a proof of concept up and running quickly, but to run your application in production, there are some things you're going to want to do.

### FusionAuth Customization

FusionAuth gives you the ability to customize just about everything to do with the user's experience and the integration of your application. This includes:

*   [Hosted pages](https://fusionauth.io/docs/customize/look-and-feel.md) such as login, registration, email verification, and many more.
*   [Email templates](https://fusionauth.io/docs/customize/email-and-messages/email-templates.md).
*   [User data and custom claims in access token JWTs](https://fusionauth.io/articles/tokens/jwt-components-explained.md).

### Security

*   You may want to customize the [token expiration times and policies](https://fusionauth.io/docs/lifecycle/authenticate-users/oauth.md) in FusionAuth.
*   Choose [password rules](https://fusionauth.io/docs/get-started/core-concepts/tenants.md#password) and a [hashing algorithm](https://fusionauth.io/docs/reference/password-hashes.md) that meet your security needs.

### Tenant and Application Management

*   Model your application topology using [Applications](https://fusionauth.io/docs/get-started/core-concepts/applications.md), [Roles](https://fusionauth.io/docs/get-started/core-concepts/roles.md), [Groups](https://fusionauth.io/docs/get-started/core-concepts/groups.md), [Entities](https://fusionauth.io/docs/get-started/core-concepts/groups.md), and more.
*   Set up [MFA](https://fusionauth.io/docs/lifecycle/authenticate-users/multi-factor-authentication.md), [Social login](https://fusionauth.io/docs/lifecycle/authenticate-users/identity-providers.md), or [SAML](https://fusionauth.io/docs/lifecycle/authenticate-users/identity-providers/overview-samlv2.md) integrations.
*   Integrate with external systems using [Webhooks](https://fusionauth.io/docs/extend/events-and-webhooks.md), [SCIM](https://fusionauth.io/docs/lifecycle/migrate-users/scim.md), and [Lambdas](https://fusionauth.io/docs/extend/code/lambdas.md).

### Remix Authentication[#](#remix-authentication)

*   [React Router Docs](https://reactrouter.com)
*   [Passport.js authentication concepts](https://www.passportjs.org/concepts/authentication/downloads/html/)
*   [Remix Auth](https://github.com/sergiodxa/remix-auth)
*   [Remix-Auth-Oauth2 Docs](https://github.com/sergiodxa/remix-auth-oauth2)

## Troubleshooting[#](#troubleshooting)

*   I get "This site can't be reached localhost refused to connect" when I click the login button.

Ensure FusionAuth is running in the Docker container. You should be able to log in as the admin user `admin@example.com` with a password of `password` at [http://localhost:9011/admin](http://localhost:9011/admin).

*   I get "invalid\_request" / "Invalid Authorization Code" after logging in to FusionAuth.

Double check that your dev server is actually running on port 3000 (see the `vite.config.ts` change earlier). The FusionAuth application's redirect URL is hardcoded to that port, so running on a different port will cause this error.

*   It still doesn't work.

You can always pull down a complete running application and compare what's different.

```plaintext
git clone https://github.com/FusionAuth/fusionauth-quickstart-javascript-remix-web.git
cd fusionauth-quickstart-javascript-remix-web
docker compose up -d
cd complete-application
npm install
npm run dev
```