> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vouch.expert/llms.txt
> Use this file to discover all available pages before exploring further.

# Remix SDK

> Email validation for Remix applications

## Overview

The Vouch Remix SDK provides seamless integration with Remix's action/loader pattern for both client and server-side email validation.

<Note>
  **Package:** `@vouch-in/remix`
  **Remix Version:** 1.0+
  **TypeScript:** Full type definitions included
</Note>

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @vouch-in/remix
  ```

  ```bash yarn theme={null}
  yarn add @vouch-in/remix
  ```

  ```bash pnpm theme={null}
  pnpm add @vouch-in/remix
  ```
</CodeGroup>

## Server-Side Validation

### Action Handler

```typescript theme={null}
// app/routes/signup.tsx
import { json, type ActionFunctionArgs } from '@remix-run/node';
import { Vouch } from '@vouch-in/remix';

const vouch = new Vouch(
  process.env.VOUCH_PROJECT_ID!,
  process.env.VOUCH_SERVER_KEY!
);

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const email = formData.get('email') as string;

  const result = await vouch.validate(email, {
    ip: request.headers.get('x-forwarded-for') || undefined,
    userAgent: request.headers.get('user-agent') || undefined,
  });

  // Check the recommendation
  if (result.recommendation !== 'allow') {
    return json({
      error: result.message,
      recommendation: result.recommendation
    }, { status: 400 });
  }

  // Check specific validations
  if (!result.checks.syntax?.pass) {
    return json({ error: 'Invalid email format' }, { status: 400 });
  }

  if (!result.checks.disposable?.pass) {
    return json({ error: 'Disposable emails not allowed' }, { status: 400 });
  }

  // Check device history
  if (result.metadata.previousSignups > 10) {
    return json({ error: 'Too many signups from this device' }, { status: 400 });
  }

  // Create user
  const user = await createUser(email);

  return json({ user });
}
```

### Client Component

```tsx theme={null}
import { Form, useActionData, useNavigation } from '@remix-run/react';

export default function SignupRoute() {
  const actionData = useActionData<typeof action>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === 'submitting';

  return (
    <Form method="post">
      <input type="email" name="email" required />
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Validating...' : 'Sign Up'}
      </button>

      {actionData?.error && (
        <p className="error">{actionData.error}</p>
      )}
    </Form>
  );
}
```

## Client-Side Validation

For client-side validation with device fingerprinting:

```tsx theme={null}
import { useState } from 'react';
import { Form } from '@remix-run/react';
import { VouchProvider, useValidateEmail } from '@vouch-in/remix/client';

export default function SignupPage() {
  return (
    <VouchProvider
      projectId={window.ENV.VOUCH_PROJECT_ID}
      apiKey={window.ENV.VOUCH_CLIENT_KEY}
    >
      <SignupForm />
    </VouchProvider>
  );
}

function SignupForm() {
  const { validate, loading, data } = useValidateEmail();
  const [email, setEmail] = useState('');
  const [error, setError] = useState<string | null>(null);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setError(null);

    const result = await validate(email);

    if (result.recommendation !== 'allow') {
      if (!result.checks.syntax?.pass) {
        setError('Invalid email format');
      } else if (!result.checks.disposable?.pass) {
        setError('Disposable emails not allowed');
      } else {
        setError('Email validation failed');
      }
      return;
    }

    // Submit the form to the server
    (e.target as HTMLFormElement).submit();
  };

  return (
    <Form method="post" onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <button type="submit" disabled={loading}>
        {loading ? 'Validating...' : 'Sign Up'}
      </button>
      {error && <p className="error">{error}</p>}
    </Form>
  );
}
```

## Hybrid Validation

Combine client-side fingerprinting with server-side validation:

```tsx theme={null}
// app/routes/signup.tsx
import { json, type ActionFunctionArgs } from '@remix-run/node';
import { Form, useActionData, useNavigation } from '@remix-run/react';
import { useState } from 'react';
import { Vouch } from '@vouch-in/remix';
import { VouchProvider, useValidateEmail } from '@vouch-in/remix/client';

const vouch = new Vouch(
  process.env.VOUCH_PROJECT_ID!,
  process.env.VOUCH_SERVER_KEY!
);

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const email = formData.get('email') as string;
  const fingerprintHash = formData.get('fingerprintHash') as string;

  const result = await vouch.validate(email, {
    ip: request.headers.get('x-forwarded-for') || undefined,
    userAgent: request.headers.get('user-agent') || undefined,
    fingerprintHash, // Include fingerprint from client
  });

  if (result.recommendation !== 'allow') {
    return json({ error: 'Email validation failed' }, { status: 400 });
  }

  const user = await createUser(email);
  return json({ user });
}

export default function SignupPage() {
  return (
    <VouchProvider
      projectId={window.ENV.VOUCH_PROJECT_ID}
      apiKey={window.ENV.VOUCH_CLIENT_KEY}
    >
      <SignupForm />
    </VouchProvider>
  );
}

function SignupForm() {
  const { validate, loading } = useValidateEmail();
  const [email, setEmail] = useState('');
  const [fingerprintHash, setFingerprintHash] = useState('');
  const actionData = useActionData<typeof action>();
  const navigation = useNavigation();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    // Validate client-side and get fingerprint hash
    const result = await validate(email);

    if (result.recommendation !== 'allow') {
      return; // Don't submit if client validation fails
    }

    // Store fingerprint hash for server submission
    setFingerprintHash(result.metadata.fingerprintHash || '');

    // Submit form to server
    (e.target as HTMLFormElement).submit();
  };

  return (
    <Form method="post" onSubmit={handleSubmit}>
      <input
        type="email"
        name="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        required
      />
      <input type="hidden" name="fingerprintHash" value={fingerprintHash} />
      <button type="submit" disabled={loading || navigation.state === 'submitting'}>
        {loading ? 'Validating...' : 'Sign Up'}
      </button>
      {actionData?.error && <p className="error">{actionData.error}</p>}
    </Form>
  );
}
```

## Environment Variables

Set up environment variables in your Remix app:

```bash theme={null}
# .env
VOUCH_PROJECT_ID=your_project_id
VOUCH_SERVER_KEY=your_server_key
VOUCH_CLIENT_KEY=your_client_key
```

Expose client-side variables via loader:

```typescript theme={null}
// app/root.tsx
import { json, type LoaderFunctionArgs } from '@remix-run/node';

export async function loader({ request }: LoaderFunctionArgs) {
  return json({
    ENV: {
      VOUCH_PROJECT_ID: process.env.VOUCH_PROJECT_ID,
      VOUCH_CLIENT_KEY: process.env.VOUCH_CLIENT_KEY,
    },
  });
}
```

## API Types

```typescript theme={null}
interface ValidationResponse {
  checks: Record<string, CheckResult>;
  message: string | null;
  metadata: {
    fingerprintHash: string | null;
    previousSignups: number;
    totalLatency: number;
  };
  recommendation: 'allow' | 'block' | 'flag';
  signals: string[];
}

interface CheckResult {
  pass: boolean;
  error?: string;
  latency: number;
  metadata?: Record<string, unknown>;
}
```

<Note>
  The `message` field contains a human-friendly error message when the recommendation is `flag` or `block` (e.g. "Please use a permanent email address, not a temporary one."). It is `null` when the recommendation is `allow`.
</Note>

## Next Steps

<CardGroup cols={2}>
  <Card title="Node.js SDK" icon="node" href="/sdks/node">
    Server-side SDK
  </Card>

  <Card title="React SDK" icon="react" href="/sdks/react">
    React hooks
  </Card>
</CardGroup>
