Deploying to Vercel
Deploying mailiam on Vercel
Section titled “Deploying mailiam on Vercel”This guide covers three deployment patterns for using mailiam with Vercel, from simple to advanced.
Quick Decision Matrix
Section titled “Quick Decision Matrix”Choose your approach based on your needs:
| Pattern | Complexity | Security | Use When |
|---|---|---|---|
| Static Forms | ⭐ Simple | ✅ High | Simple contact forms, no JS needed |
| API Route Proxy | ⭐⭐ Moderate | ✅✅ Highest | Need server-side validation, transformations |
| Public Token | ⭐⭐ Moderate | ✅ Good | Need client-side JS interactions |
Pattern 1: Static Forms (Recommended)
Section titled “Pattern 1: Static Forms (Recommended)”Perfect for: Simple contact forms, newsletter signups, feedback forms
No API key needed! Forms work automatically with origin validation.
Step 1: Configure mailiam
Section titled “Step 1: Configure mailiam”# Initialize mailiam in your projectmailiam init
# Configure your domaincat > mailiam.config.yaml <<EOFdomains: mysite.com: forms: contact: enabled: true recipient: team@mysite.com settings: spamProtection: true allowedOrigins: - https://mysite.vercel.app - https://mysite.com - https://www.mysite.comEOF
# Deploy configurationmailiam pushStep 2: Create Your Form (HTML)
Section titled “Step 2: Create Your Form (HTML)”<!-- 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 UXconst 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'; }});Step 4: Deploy to Vercel
Section titled “Step 4: Deploy to Vercel”vercel deploy --prodThat’s it! No API keys, no backend code needed.
How It’s Secure
Section titled “How It’s Secure”- Domain Verification - Your domain is verified via DNS
- Origin Validation - mailiam checks the Origin header
- Browser Detection - Blocks curl, Postman, etc.
- Rate Limiting - IP-based rate limiting prevents abuse
- Spam Protection - Built-in honeypot and spam detection
Pattern 2: API Route Proxy (Most Secure)
Section titled “Pattern 2: API Route Proxy (Most Secure)”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.
Step 1: Create Usage Key
Section titled “Step 1: Create Usage Key”# 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”# Via Vercel CLIvercel env add MAILIAM_API_KEY
# Or in Vercel Dashboard:# Settings → Environment Variables → Add# Name: MAILIAM_API_KEY# Value: mlm_sk_usage_x9y8z7w6v5u4t3s2...Step 3: Create API Route
Section titled “Step 3: Create API Route”Next.js (Pages Router)
Section titled “Next.js (Pages Router)”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' }); }}Next.js (App Router)
Section titled “Next.js (App Router)”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 } ); }}Step 4: Update Your Form
Section titled “Step 4: Update Your Form”'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> );}Benefits of API Route Proxy
Section titled “Benefits of API Route Proxy”✅ 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
Pattern 3: Public Token (Client-Side)
Section titled “Pattern 3: Public Token (Client-Side)”Perfect for: Static sites needing JS interactions, checking form status, analytics
Public tokens are domain-scoped and safe for client-side use.
Step 1: Generate Public Token
Section titled “Step 1: Generate Public Token”# Create a public token for your domainmailiam 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”# 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!
Step 3: Use in Your Application
Section titled “Step 3: Use in Your Application”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 statusexport 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();}Public Token Security
Section titled “Public Token Security”✅ 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
Advanced Features
Section titled “Advanced Features”Custom Success Page
Section titled “Custom Success Page”import { redirect } from 'next/navigation';
export default function ContactPage({ searchParams }: { searchParams: { success?: string } }) { if (searchParams.success) { return <SuccessMessage />; }
return <ContactForm />;}Integration with React Hook Form
Section titled “Integration with React Hook Form”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> );}Rate Limiting on Your Side
Section titled “Rate Limiting on Your Side”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 routeconst 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' });}Testing Locally
Section titled “Testing Locally”With Vercel CLI
Section titled “With Vercel CLI”# Install Vercel CLInpm i -g vercel
# Link projectvercel link
# Pull environment variablesvercel env pull .env.local
# Run locallyvercel devYour local server will run at http://localhost:3000 with production environment variables.
Environment Variable Management
Section titled “Environment Variable Management”# .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!.env*.local.env.production.vercelDeployment Checklist
Section titled “Deployment Checklist”- 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
Troubleshooting
Section titled “Troubleshooting”Forms not submitting
Section titled “Forms not submitting”-
Check allowed origins
domains:mysite.com:forms:settings:allowedOrigins:- https://mysite.vercel.app # Include all Vercel URLs- https://mysite.com -
Check domain verification
Terminal window mailiam domains verify mysite.com -
Check browser console for CORS errors
API key not working
Section titled “API key not working”-
Verify it’s a usage key
Terminal window mailiam auth list-keys -
Check environment variable
Terminal window vercel env ls -
Redeploy after adding environment variables
Terminal window vercel --prod
CORS errors
Section titled “CORS errors”Make sure your allowed origins include:
- Your production domain
- Your Vercel preview URLs (if needed)
localhostfor development
Performance Tips
Section titled “Performance Tips”-
Use SWR for form state
import useSWRMutation from 'swr/mutation';const { trigger, isMutating } = useSWRMutation('/api/contact', fetcher); -
Implement optimistic UI Show success before API confirmation
-
Add loading skeletons Improve perceived performance
-
Cache form configuration Reduce API calls with ISR/SSG
Security Best Practices
Section titled “Security Best Practices”✅ 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
Next Steps
Section titled “Next Steps”Deploy with confidence! Vercel + mailiam = Perfect match. 🚀