A Next.js Email Assistant that uses Tambo to draft, preview, and send emails via Resend using natural language.
This project demonstrates how to use Tambo to generate UI and execute real-world actions by drafting and sending emails through Resend. It includes full authentication and database management with Superbase, contact management, and email tracking.
It is designed to give new Tambo users a clear, end-to-end example of:
- a generative UI component
- a tool with real side effects
- an AI-controlled interaction flow
- user authentication & sessions
- data persistence with Supabase
-
Generative Component
The AI renders anEmailPreviewcomponent to show a draft email. -
Tool with Side Effects
The AI can call asendEmailtool to send a real email using Resend. -
AI-Controlled Flow
Natural language → UI preview → user confirmation → email sent. -
Authentication & Session Management
Passwordless OTP-based authentication using Supabase Auth with secure session handling. -
Contact Management
Save, list, and manage contacts with automatic duplicate detection. -
Email Tracking
Track sent and draft emails with full audit trails. -
Sidebar Data Views
Sent emails, draft emails, and contacts are visible in the sidebar UI alongside the chat.
This is a minimal but complete starter showing how Tambo can both render UI and trigger real-world actions with persistent data.
- Next.js 15 (App Router)
- Tambo (AI-controlled UI + tools)
- Resend (email delivery)
- Supabase (authentication & database)
- Zod (tool & component schemas)
- TypeScript
cd community/templates/nextjs-tambo-resend
npm install
cp example.env.local .env.local
npm run dev
Open http://localhost:3000Add the following variables to your .env.local file:
# Tambo AI Configuration
NEXT_PUBLIC_TAMBO_API_KEY=your_tambo_api_key
# Resend Email Service
RESEND_API_KEY=your_resend_api_key
FROM_EMAIL=onboarding@resend.dev
# Supabase Configuration
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key- Tambo API Key: Get from Tambo Dashboard
- Resend API Key: Get from Resend Dashboard - you can use
onboarding@resend.devfor development without domain verification - Supabase URL & Key: Get from Supabase Dashboard - create a new project and find these in the project settings
The application uses Supabase Auth with passwordless OTP (One-Time Password) authentication:
-
Login Flow
- User enters email on the login page (
/login) - Supabase sends a secure sign-in link via email
- User clicks the link, which redirects to
/chatwith a session token - Session is stored in cookies via Supabase SSR middleware
- User enters email on the login page (
-
Session Persistence
- Sessions are managed using Supabase's cookie-based SSR pattern
- The
AuthGatecomponent (src/components/auth/AuthGate.tsx) protects routes - On app load,
AuthGatechecks if a valid session exists - If no session, user is redirected to
/login - Sessions persist across browser refreshes
-
Client-Side Integration
- Browser client created in
src/lib/superbase/client.ts - Automatically manages cookies for session tokens
- Cookies are sent with all API requests
- Browser client created in
-
Server-Side Integration
- Server client created in
src/lib/superbase/server.ts - Used in server actions for authenticated database operations
- Always retrieves the current authenticated user via
supabase.auth.getUser()
- Server client created in
src/app/login/page.tsx- Login page with OTP email flowsrc/components/auth/AuthGate.tsx- Route protection wrappersrc/lib/superbase/client.ts- Browser Supabase client with cookie handlingsrc/lib/superbase/server.ts- Server Supabase client
This application uses Supabase (PostgreSQL-based) to store user data securely:
-
users(Managed by Supabase Auth)id(UUID) - Primary keyemail(string) - User's email address- Auto-managed by Supabase authentication
-
contacts(User-created contacts)- id (UUID) - Primary key - user_id (UUID) - Foreign key to users - name (string) - Contact name - email (string) - Contact email - created_at (timestamp) - Creation timestamp - Indexes: (user_id, email) for duplicate detection -
emails(Email tracking - sent & draft)- id (UUID) - Primary key - user_id (UUID) - Foreign key to users - to_email (string) - Recipient email - subject (string) - Email subject - body (string) - Email body content - status (enum: 'draft' | 'sent') - Email status - created_at (timestamp) - Creation timestamp - Indexes: (user_id, status) for filtering
All tables have RLS policies enabled to ensure:
- Users can only see their own data
- Each query is automatically filtered by
user_id - Data is isolated per user
- Create a Supabase project at supabase.com
- In the SQL Editor, run the setup migration to create tables
- Enable RLS on all tables
- Copy your project URL and anon key to
.env.local
Contacts are stored in the Supabase contacts table with the following features:
// src/services/save-contact.ts
- Requires authentication
- Automatically associates contact with current user
- Prevents duplicate emails for the same user
- Returns status: 'saved' or 'exists'Usage in Chat:
"Save John Doe's email john@example.com"
"Add contact: jane@company.com (Jane Smith)"
// src/services/list-contacts.ts
- Retrieves all contacts for authenticated user
- Returns sorted list of contact names and emails
- Used to populate contact suggestions in the AI- ✅ Automatic duplicate detection (same user can't add same email twice)
- ✅ User isolation (each user's contacts are private)
- ✅ Timestamp tracking (know when contact was added)
- ✅ Full name storage (not just email)
Ask the AI to:
- "Show me my contacts"
- "Add contact john@example.com as John Doe"
- "Who are my current contacts?"
All emails (sent and draft) are stored in the Supabase emails table with full audit trails:
// src/services/save-email.ts
- Called before sending to create a draft
- Stores recipient, subject, body, and status='draft'
- User can review before confirming send
- Draft updates the status to 'sent' with timestampUsers can view their emails by status:
-
Draft Emails (
/emails?status=draft)- Unsent email proposals from the AI
- User can ask AI to modify before sending
- Automatically deleted after sending or if user declines
-
Sent Emails (
/emails?status=sent)- Successfully delivered emails via Resend
- Complete history with timestamps
- Recipient email and full content preserved
- ✅ Full email content stored (subject + body)
- ✅ Recipient tracking (to_email)
- ✅ Status filtering (sent vs. draft)
- ✅ Timestamp history (know when sent)
- ✅ User isolation (private email history)
// src/services/list-emails.ts
- Retrieves emails for authenticated user
- Optional filter by status: 'sent' | 'draft'
- Ordered by creation date (newest first)
- Returns: id, to, subject, body, status, createdAt- User requests draft email in chat: "Write an email to john@example.com about..."
- AI generates email and renders
EmailPreviewcomponent - Draft email is saved to database with
status='draft' - User confirms: "Send this email"
sendEmailtool is triggered via Resend API- Email status changes to
'sent'in database - Confirmation message shown to user
- Email history updated immediately
Ask the AI to:
- "Show me my sent emails"
- "What drafts do I have?"
- "List emails sent to jane@example.com"
- "Did I send that follow-up email?"
In addition to AI-driven chat workflows, this template includes a minimal sidebar UI that surfaces persisted data:
-
Sent Emails
View all emails that have been successfully sent via Resend. -
Draft Emails
View saved drafts that were generated by the AI but not yet sent. -
Contacts
View saved contacts associated with the authenticated user.
These views are accessible directly from the thread history sidebar, allowing users to inspect and reuse data without requiring additional prompts.
This demonstrates how AI-controlled actions and traditional UI patterns can coexist:
- The AI creates and acts on data
- The sidebar provides deterministic access to that same data
In the chat, try:
-
Add a Contact
Example: "Save contact: john@example.com as John Smith" -
Draft an Email
Example: "Write a polite internship follow-up email to john@example.com. Keep it professional and concise." -
Preview the Email
The AI renders anEmailPreviewshowing:- Recipient email
- Subject line
- Full email body
-
Send the Email
Say: "Send this email"- Email is sent via Resend
- Status updated to 'sent' in database
- Confirmation message displayed
-
View Email History
Say: "Show me my sent emails" or "What drafts do I have?" -
Manage Contacts
Say: "Who are my contacts?" or "Add contact: jane@company.com"
-
EmailPreviewComponent
Registered as a Tambo component and rendered dynamically by the AI to display the drafted email. -
sendEmailTool
Registered as a Tambo tool and implemented using Resend's API to send real emails. -
saveContact&listContactsTools
Manage contact storage and retrieval from Supabase. -
saveEmailDraft&listEmailsTools
Track email history with sent/draft status. -
TamboProvider
Connects registered components and tools, allowing the AI to decide what to render and when to act. -
AuthGateComponent
Protects all routes by requiring an active Supabase session.
-
src/components/tambo/EmailPreview.tsx
AI-rendered email preview component. -
src/components/auth/AuthGate.tsx
Session validation and route protection. -
src/lib/superbase/
Supabase client setup for browser and server. -
src/services/
All database operations:save-contact.ts- Add new contactslist-contacts.ts- Retrieve user contactssave-email.ts- Store email drafts/sendslist-emails.ts- Retrieve email historysend-email-and-persist.ts- Send via Resend + update databasefind-contact.ts- Search for specific contacts
-
src/lib/tambo.ts
Central registration of Tambo components and tools. -
src/app/chat/page.tsx
Chat interface powered by Tambo.
Run these SQL commands in the Supabase SQL Editor:
-- Create contacts table
CREATE TABLE contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
name TEXT NOT NULL,
email TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(user_id, email)
);
-- Create emails table
CREATE TABLE emails (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
to_email TEXT NOT NULL,
subject TEXT NOT NULL,
body TEXT NOT NULL,
status TEXT CHECK (status IN ('draft', 'sent')) DEFAULT 'draft',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Enable RLS
ALTER TABLE contacts ENABLE ROW LEVEL SECURITY;
ALTER TABLE emails ENABLE ROW LEVEL SECURITY;
-- RLS Policies for contacts
CREATE POLICY "Users see only their contacts"
ON contacts FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own contacts"
ON contacts FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own contacts"
ON contacts FOR UPDATE USING (auth.uid() = user_id);
-- RLS Policies for emails
CREATE POLICY "Users see only their emails"
ON emails FOR SELECT USING (auth.uid() = user_id);
CREATE POLICY "Users can insert own emails"
ON emails FOR INSERT WITH CHECK (auth.uid() = user_id);
CREATE POLICY "Users can update own emails"
ON emails FOR UPDATE USING (auth.uid() = user_id);
-- Create indexes for performance
CREATE INDEX idx_contacts_user_id ON contacts(user_id);
CREATE INDEX idx_emails_user_id ON emails(user_id);
CREATE INDEX idx_emails_status ON emails(user_id, status);- Screenshot:
-
Video demo:
-
Screen.Recording.2026-01-31.mp4
-
sendmail.mp4
- Do not commit
.env.local. .env.exampleis provided for reference.- This template intentionally keeps scope manageable to focus on clarity and learning.


