Skip to main content

Overview

Add your Invent AI assistant to any Remix application. Perfect for full-stack React applications with nested routing and progressive enhancement.

Installation

1

Create Assistant Component

// app/components/InventAssistant.tsx
interface InventAssistantProps {
  assistantId: string;
  themeAppearance?: 'auto' | 'light' | 'dark';
  themeButtonBackgroundColor?: string;
  themeButtonColor?: string;
  userId?: string;
  userName?: string;
  userHash?: string;
  userAvatar?: string;
}

export default function InventAssistant({
  assistantId,
  themeAppearance = 'auto',
  themeButtonBackgroundColor,
  themeButtonColor,
  userId,
  userName,
  userHash,
  userAvatar,
}: InventAssistantProps) {
  return (
    <>
      <invent-assistant
        assistant-id={assistantId}
        theme-appearance={themeAppearance}
        {...(themeButtonBackgroundColor && {
          'theme-button-background-color': themeButtonBackgroundColor,
        })}
        {...(themeButtonColor && {
          'theme-button-color': themeButtonColor,
        })}
        {...(userId && { 'user-id': userId })}
        {...(userName && { 'user-name': userName })}
        {...(userHash && { 'user-hash': userHash })}
        {...(userAvatar && { 'user-avatar': userAvatar })}
      />
      <script
        type="text/javascript"
        src="https://www.useinvent.com/button.js"
        async
        defer
      />
    </>
  );
}
2

Add to Root

// app/root.tsx
import type { LinksFunction, LoaderFunctionArgs } from '@remix-run/node';
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
  useLoaderData,
} from '@remix-run/react';
import InventAssistant from './components/InventAssistant';

export async function loader({ request }: LoaderFunctionArgs) {
  // Get user data from session
  const userData = await getUserData(request);

  return {
    assistantId: process.env.INVENT_ASSISTANT_ID,
    userData,
  };
}

export default function App() {
  const { assistantId, userData } = useLoaderData<typeof loader>();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
        <InventAssistant
          assistantId={assistantId}
          {...userData}
        />
      </body>
    </html>
  );
}
3

Configure Environment

Add to .env:
INVENT_ASSISTANT_ID=ast_YOUR_ASSISTANT_ID
INVENT_SECRET_KEY=your_secret_key_here

User Authentication

Security Requirement: When using any user-* attributes (user-id, user-name, user-avatar), you must also provide user-hash. Both user-id and user-hash must be provided together, or neither should be provided. The user-hash must be generated on your backend using HMAC-SHA256 with your assistant’s secret key. Never expose the secret key to the client.

Server-Side Hash Generation

Use Remix loaders for secure hash generation:
// app/root.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import crypto from 'crypto';
import { getUser } from '~/services/auth.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await getUser(request);

  let userData = null;
  if (user) {
    const secretKey = process.env.INVENT_SECRET_KEY!;
    const userId = user.id;

    const userHash = crypto
      .createHmac('sha256', secretKey)
      .update(userId)
      .digest('hex');

    userData = {
      userId,
      userName: user.name,
      userAvatar: user.avatar,
      userHash,
    };
  }

  return json({
    assistantId: process.env.INVENT_ASSISTANT_ID!,
    userData,
  });
}

Using Resource Routes

Create a resource route for dynamic user data:
// app/routes/api.user-hash.ts
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import crypto from 'crypto';
import { requireUser } from '~/services/auth.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await requireUser(request);

  const secretKey = process.env.INVENT_SECRET_KEY!;
  const userId = user.id;

  const userHash = crypto
    .createHmac('sha256', secretKey)
    .update(userId)
    .digest('hex');

  return json({
    userId,
    userName: user.name,
    userAvatar: user.avatar,
    userHash,
  });
}
Then fetch in a component:
// app/components/InventAssistantWrapper.tsx
import { useFetcher } from '@remix-run/react';
import { useEffect } from 'react';
import InventAssistant from './InventAssistant';

export default function InventAssistantWrapper({
  assistantId,
}: {
  assistantId: string;
}) {
  const fetcher = useFetcher();

  useEffect(() => {
    if (fetcher.state === 'idle' && !fetcher.data) {
      fetcher.load('/api/user-hash');
    }
  }, [fetcher]);

  return (
    <InventAssistant
      assistantId={assistantId}
      {...(fetcher.data || {})}
    />
  );
}

Integration with Remix Auth

// app/root.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import crypto from 'crypto';
import { authenticator } from '~/services/auth.server';

export async function loader({ request }: LoaderFunctionArgs) {
  const user = await authenticator.isAuthenticated(request);

  let userData = null;
  if (user) {
    const secretKey = process.env.INVENT_SECRET_KEY!;
    const userHash = crypto
      .createHmac('sha256', secretKey)
      .update(user.id)
      .digest('hex');

    userData = {
      userId: user.id,
      userName: user.name,
      userAvatar: user.avatar,
      userHash,
    };
  }

  return json({
    assistantId: process.env.INVENT_ASSISTANT_ID!,
    userData,
    ENV: {
      // Only expose public variables
    },
  });
}

Nested Routes with Context

Pass route-specific context:
// app/routes/blog.$slug.tsx
import type { LoaderFunctionArgs } from '@remix-run/node';
import { json } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';
import { useEffect } from 'react';

export async function loader({ params }: LoaderFunctionArgs) {
  const post = await getPost(params.slug);
  return json({ post });
}

export default function BlogPost() {
  const { post } = useLoaderData<typeof loader>();

  useEffect(() => {
    if (typeof window !== 'undefined') {
      window.inventContext = {
        pageType: 'blog-post',
        title: post.title,
        author: post.author,
        category: post.category,
      };
    }
  }, [post]);

  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  );
}

TypeScript Support

Add type declarations:
// app/types/invent-assistant.d.ts
declare global {
  interface Window {
    inventContext?: Record<string, any>;
  }

  namespace JSX {
    interface IntrinsicElements {
      'invent-assistant': {
        'assistant-id': string;
        'theme-appearance'?: 'auto' | 'light' | 'dark';
        'theme-button-background-color'?: string;
        'theme-button-color'?: string;
        'user-id'?: string;
        'user-name'?: string;
        'user-hash'?: string;
        'user-avatar'?: string;
      };
    }
  }
}

export {};

Using with Sessions

// app/services/session.server.ts
import { createCookieSessionStorage } from '@remix-run/node';
import crypto from 'crypto';

const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: '__session',
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    secrets: [process.env.SESSION_SECRET!],
    sameSite: 'lax',
  },
});

export async function getUserData(request: Request) {
  const session = await sessionStorage.getSession(
    request.headers.get('Cookie')
  );

  const userId = session.get('userId');
  if (!userId) return null;

  const userHash = crypto
    .createHmac('sha256', process.env.INVENT_SECRET_KEY!)
    .update(userId)
    .digest('hex');

  return {
    userId,
    userName: session.get('userName'),
    userAvatar: session.get('userAvatar'),
    userHash,
  };
}

Tips for Remix

Server Loaders

Use loaders for secure hash generation

Progressive Enhancement

Works without JavaScript enabled

Nested Routes

Integrate with nested routing

Type Safety

Full TypeScript support

Troubleshooting

Solutions:
  • Add check if script already exists
  • Use proper cleanup in useEffect
  • Ensure component only renders once
  • Check for duplicate components in tree
Solutions:
  • Ensure data is available on both server and client
  • Check that loader data is serializable
  • Verify useEffect cleanup
  • Test with JavaScript disabled
Solutions:
  • Restart dev server after adding variables
  • Don’t prefix server-only variables
  • Never expose secret key to client
  • Check loader returns correct data

Best Practices

  • Always use loaders for server-side operations
  • Keep secret keys server-side only
  • Leverage Remix’s built-in session management
  • Use resource routes for dynamic data
  • Test with progressive enhancement in mind