Skip to content

Deploying to Vercel

This guide covers three deployment patterns for using mailiam with Vercel, from simple to advanced.

Choose your approach based on your needs:

PatternComplexitySecurityUse When
Static Forms⭐ Simple✅ HighSimple contact forms, no JS needed
API Route Proxy⭐⭐ Moderate✅✅ HighestNeed server-side validation, transformations
Public Token⭐⭐ Moderate✅ GoodNeed client-side JS interactions

Perfect for: Simple contact forms, newsletter signups, feedback forms

No API key needed! Forms work automatically with origin validation.

Terminal window
# Initialize mailiam in your project
mailiam init
# Configure your domain
cat > mailiam.config.yaml <<EOF
domains:
mysite.com:
forms:
contact:
enabled: true
recipient: team@mysite.com
settings:
spamProtection: true
allowedOrigins:
- https://mysite.vercel.app
- https://mysite.com
- https://www.mysite.com
EOF
# Deploy configuration
mailiam push
<!-- pages/contact.html or app/contact/page.tsx -->
<form action="https://api.mailiam.dev/v1/mysite.com/send" method="POST">
<div>
<label for="name">Name</label>
<input
type="text"
id="name"
name="name"
required
>
</div>
<div>
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
required
>
</div>
<div>
<label for="message">Message</label>
<textarea
id="message"
name="message"
rows="5"
required
></textarea>
</div>
<!-- Honeypot for spam protection (hidden from users) -->
<input
type="text"
name="pooh-bear"
style="display:none"
tabindex="-1"
autocomplete="off"
>
<button type="submit">Send Message</button>
</form>

Step 3: Add Client-Side Enhancement (Optional)

Section titled “Step 3: Add Client-Side Enhancement (Optional)”
// Enhance with fetch for better UX
const form = document.querySelector('form');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(form);
const button = form.querySelector('button');
button.disabled = true;
button.textContent = 'Sending...';
try {
const response = await fetch(form.action, {
method: 'POST',
body: formData
});
if (response.ok) {
form.reset();
alert('Message sent successfully!');
} else {
alert('Failed to send message. Please try again.');
}
} catch (error) {
alert('Network error. Please check your connection.');
} finally {
button.disabled = false;
button.textContent = 'Send Message';
}
});
Terminal window
vercel deploy --prod

That’s it! No API keys, no backend code needed.

  1. Domain Verification - Your domain is verified via DNS
  2. Origin Validation - mailiam checks the Origin header
  3. Browser Detection - Blocks curl, Postman, etc.
  4. Rate Limiting - IP-based rate limiting prevents abuse
  5. Spam Protection - Built-in honeypot and spam detection

Perfect for: Custom validation, data transformation, integration with your backend

Use a Vercel serverless function to proxy requests to mailiam with a hidden API key.

Terminal window
# Create a usage key (limited permissions)
mailiam auth create-key --name "Vercel API Routes" --type usage
# Output: mlm_sk_usage_x9y8z7w6v5u4t3s2...

Step 2: Add to Vercel Environment Variables

Section titled “Step 2: Add to Vercel Environment Variables”
Terminal window
# Via Vercel CLI
vercel env add MAILIAM_API_KEY
# Or in Vercel Dashboard:
# Settings → Environment Variables → Add
# Name: MAILIAM_API_KEY
# Value: mlm_sk_usage_x9y8z7w6v5u4t3s2...
pages/api/contact.js
export default async function handler(req, res) {
// Only allow POST
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Optional: Add custom validation
const { name, email, message } = req.body;
if (!email || !message) {
return res.status(400).json({
error: 'Email and message are required'
});
}
// Optional: Custom spam check
if (message.length < 10) {
return res.status(400).json({
error: 'Message too short'
});
}
// Forward to mailiam
const response = await fetch(
'https://api.mailiam.dev/v1/mysite.com/send',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': process.env.MAILIAM_API_KEY
},
body: JSON.stringify(req.body)
}
);
if (!response.ok) {
const error = await response.json();
return res.status(response.status).json(error);
}
const result = await response.json();
return res.status(200).json(result);
} catch (error) {
console.error('Contact form error:', error);
return res.status(500).json({
error: 'Internal server error'
});
}
}
app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { name, email, message } = body;
// Validation
if (!email || !message) {
return NextResponse.json(
{ error: 'Email and message are required' },
{ status: 400 }
);
}
// Forward to mailiam
const response = await fetch(
'https://api.mailiam.dev/v1/mysite.com/send',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': process.env.MAILIAM_API_KEY!
},
body: JSON.stringify(body)
}
);
if (!response.ok) {
const error = await response.json();
return NextResponse.json(error, { status: response.status });
}
const result = await response.json();
return NextResponse.json(result);
} catch (error) {
console.error('Contact form error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
components/ContactForm.tsx
'use client';
import { useState } from 'react';
export default function ContactForm() {
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setStatus('loading');
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData.entries());
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
setStatus('success');
e.currentTarget.reset();
} else {
setStatus('error');
}
} catch (error) {
setStatus('error');
}
}
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={status === 'loading'}>
{status === 'loading' ? 'Sending...' : 'Send Message'}
</button>
{status === 'success' && <p>Message sent successfully!</p>}
{status === 'error' && <p>Failed to send. Please try again.</p>}
</form>
);
}

API key never exposed to client ✅ Custom validation before sending ✅ Data transformation (sanitize, format, etc.) ✅ Integration with your backend (save to DB, etc.) ✅ Rate limiting on your terms ✅ Error handling with custom messages


Perfect for: Static sites needing JS interactions, checking form status, analytics

Public tokens are domain-scoped and safe for client-side use.

Terminal window
# Create a public token for your domain
mailiam auth create-key \
--name "Public Token for mysite.com" \
--type public \
--domain mysite.com
# Output: mlm_pk_a7f8b3e1_j4k5l6m7n8o9p0q1...

Step 2: Add to Vercel Environment Variables

Section titled “Step 2: Add to Vercel Environment Variables”
Terminal window
# Add as public environment variable (NEXT_PUBLIC_ prefix)
vercel env add NEXT_PUBLIC_MAILIAM_TOKEN
# Value: mlm_pk_a7f8b3e1_j4k5l6m7n8o9p0q1...

Important: Variables with NEXT_PUBLIC_ prefix are embedded in the client bundle. This is safe for public tokens only!

lib/mailiam.ts
const PUBLIC_TOKEN = process.env.NEXT_PUBLIC_MAILIAM_TOKEN;
export async function submitForm(data: Record<string, any>) {
const response = await fetch(
'https://api.mailiam.dev/v1/mysite.com/send',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Public-Token': PUBLIC_TOKEN!
},
body: JSON.stringify(data)
}
);
if (!response.ok) {
throw new Error('Failed to submit form');
}
return response.json();
}
// Future: Check submission status
export async function getSubmissionStatus(submissionId: string) {
const response = await fetch(
`https://api.mailiam.dev/v1/mysite.com/status/${submissionId}`,
{
headers: {
'X-Public-Token': PUBLIC_TOKEN!
}
}
);
return response.json();
}

Domain-scoped - Only works for the specified domain ✅ Limited permissions - Can only submit forms, no admin access ✅ Rate limited - Lower limits (100/hour vs 1000/hour) ✅ Revocable - Regenerate anytime if compromised

Cannot manage domains ❌ Cannot access other domains ❌ Cannot modify settings


app/contact/page.tsx
import { redirect } from 'next/navigation';
export default function ContactPage({ searchParams }: { searchParams: { success?: string } }) {
if (searchParams.success) {
return <SuccessMessage />;
}
return <ContactForm />;
}
import { useForm } from 'react-hook-form';
type FormData = {
name: string;
email: string;
message: string;
};
export default function ContactForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<FormData>();
const onSubmit = async (data: FormData) => {
const response = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Failed to send');
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name', { required: true })} />
{errors.name && <span>Name is required</span>}
<input {...register('email', { required: true, pattern: /^\S+@\S+$/i })} />
{errors.email && <span>Valid email required</span>}
<textarea {...register('message', { required: true, minLength: 10 })} />
{errors.message && <span>Message must be at least 10 characters</span>}
<button disabled={isSubmitting}>Send</button>
</form>
);
}
lib/rateLimit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(5, '1 m'), // 5 requests per minute
});
export async function checkRateLimit(ip: string) {
const { success } = await ratelimit.limit(ip);
return success;
}
// In your API route
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const allowed = await checkRateLimit(ip);
if (!allowed) {
return res.status(429).json({ error: 'Too many requests' });
}
Terminal window
# Install Vercel CLI
npm i -g vercel
# Link project
vercel link
# Pull environment variables
vercel env pull .env.local
# Run locally
vercel dev

Your local server will run at http://localhost:3000 with production environment variables.

# .env.local (for local development)
MAILIAM_API_KEY=mlm_sk_usage_test_...
NEXT_PUBLIC_MAILIAM_TOKEN=mlm_pk_test_...
# Never commit this file to git!
.gitignore
.env*.local
.env.production
.vercel
  • mailiam domain configured (mailiam push)
  • DNS records verified
  • API key created (if using API routes)
  • Environment variables set in Vercel
  • Allowed origins configured in mailiam.config.yaml
  • Forms tested locally with vercel dev
  • Spam protection enabled
  • Success/error messages implemented
  • Rate limiting configured
  1. Check allowed origins

    domains:
    mysite.com:
    forms:
    settings:
    allowedOrigins:
    - https://mysite.vercel.app # Include all Vercel URLs
    - https://mysite.com
  2. Check domain verification

    Terminal window
    mailiam domains verify mysite.com
  3. Check browser console for CORS errors

  1. Verify it’s a usage key

    Terminal window
    mailiam auth list-keys
  2. Check environment variable

    Terminal window
    vercel env ls
  3. Redeploy after adding environment variables

    Terminal window
    vercel --prod

Make sure your allowed origins include:

  • Your production domain
  • Your Vercel preview URLs (if needed)
  • localhost for development
  1. Use SWR for form state

    import useSWRMutation from 'swr/mutation';
    const { trigger, isMutating } = useSWRMutation('/api/contact', fetcher);
  2. Implement optimistic UI Show success before API confirmation

  3. Add loading skeletons Improve perceived performance

  4. Cache form configuration Reduce API calls with ISR/SSG

DO:

  • Use usage keys in API routes
  • Use public tokens only when necessary
  • Enable spam protection
  • Implement rate limiting
  • Validate input server-side
  • Sanitize user input

DON’T:

  • Expose admin keys client-side
  • Trust client-side validation alone
  • Skip CORS configuration
  • Ignore rate limiting
  • Store API keys in git

Deploy with confidence! Vercel + mailiam = Perfect match. 🚀