Overview
The Vouch Remix SDK provides seamless integration with Remix’s action/loader pattern for both client and server-side email validation.Package:
@vouch-in/remix
Remix Version: 1.0+
TypeScript: Full type definitions includedInstallation
Copy
npm install @vouch-in/remix
Server-Side Validation
Action Handler
Copy
// 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: 'Email validation failed',
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
Copy
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:Copy
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:Copy
// 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:Copy
# .env
VOUCH_PROJECT_ID=your_project_id
VOUCH_SERVER_KEY=your_server_key
VOUCH_CLIENT_KEY=your_client_key
Copy
// 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
Copy
interface ValidationResponse {
checks: Record<string, CheckResult>;
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>;
}