Subscription emails
The subscription emails feature allows users to receive automated email reports based on their preferences. This document explains the technical implementation of this feature.
Overview
The feature consists of several components:
- A settings UI where users can manage their email preferences
- A database schema to store subscription preferences and status
- A cron-triggered API route that sends scheduled emails
- Email templates and content generation logic
- A type-safe email sending system with status tracking
Database structure
Tables
email_type
Stores the different types of emails that can be sent:
CREATE TABLE public.email_type (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
status email_type_status NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ
);email_subscription
Stores user preferences and status for each email type:
CREATE TABLE public.email_subscription (
id UUID PRIMARY KEY,
profile_id UUID NOT NULL,
email_type_id UUID NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
frequency email_subscription_frequency NOT NULL,
status email_subscription_status NOT NULL DEFAULT 'pending',
error_message TEXT,
last_sent_at TIMESTAMPTZ,
next_send_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ,
FOREIGN KEY (profile_id) REFERENCES public.profile (id),
FOREIGN KEY (email_type_id) REFERENCES public.email_type (id),
UNIQUE (profile_id, email_type_id, is_active)
);Enums
CREATE TYPE public.email_subscription_frequency AS ENUM(
'1D', -- Daily
'1W', -- Weekly
'1M', -- Monthly
'transactional'
);
CREATE TYPE public.email_subscription_status AS ENUM(
'pending',
'sent',
'failed'
);
CREATE TYPE public.email_type_status AS ENUM(
'active',
'inactive',
'deprecated'
);Automatic subscription creation
The system automatically creates default email subscriptions in two scenarios:
- When a new user joins a group (via
handle_group_user_creationtrigger) - During backfill for existing users (via migration scripts)
Default subscriptions are only created for regular users, not for advisors.
User interface
The email preferences UI is implemented in email-notifications-card.tsx. It provides:
- A toggle to enable/disable email subscriptions
- A frequency selector (daily/weekly/monthly)
- Display of the next scheduled email date
- Status indicators for subscription state
- Automatic saving of changes
Email sending system
Architecture
- Trigger: A cron job calls
/api/send-subscription-emails - Processing:
- Fetches due subscriptions
- Processes them in chunks of 5 to avoid overload
- Updates
last_sent_at,next_send_at, andstatusafter sending - Records any error messages if sending fails
Period ranges
The system uses a unified range system with the following types:
export enum Range {
OneDay = "1D",
OneWeek = "1W",
OneMonth = "1M",
ThreeMonths = "3M",
SixMonths = "6M",
OneYear = "1Y",
All = "ALL",
}
export type EmailRange = Range.OneDay | Range.OneWeek | Range.OneMonth;Each range includes:
- Sampling intervals for data aggregation
- Display text for UI and emails
- Period calculation logic
- Validation through type guards
Email providers
The system supports two email providers:
- Production: Resend
- Development: Local Inbucket SMTP server
Configuration is determined by environment variables:
VERCEL_ENV: Determines if in productionMAIL_PROVIDER: Can be set to "inbucket" for local development
Type safety
The email system uses TypeScript to ensure type safety:
type EmailTemplate =
| "waitlistInvite"
| "welcomeEmail"
| "userInvite"
| "acceptedInvite"
| "removedMember"
| "cancelledInvite"
| "leftSpace"
| "netWorthReport";
interface EmailSubscription {
id: string;
profileId: string;
emailTypeId: string;
isActive: boolean;
frequency: EmailSubscriptionFrequency;
status: EmailSubscriptionStatus;
errorMessage?: string;
lastSentAt?: Date;
nextSendAt: Date;
}Net worth report implementation
The net worth report includes:
- Period-based net worth variations
- Top movers analysis showing biggest gainers and losers
- Localized content using the user's preferred language
- Formatted currency and percentage values
Top movers calculation
The system:
- Retrieves holdings with enriched account and security information
- Calculates variations for the specified period
- Ranks holdings by absolute variation
- Includes both positive and negative movers in the report
Admin client support
For system-generated emails, queries can use an admin client to ensure access to necessary data:
function getHoldingsData(spaceId: string, useAdminClient = false) {
const client = useAdminClient ? createAdminClient() : createClient();
// ... query logic
}Maintenance and troubleshooting
Status tracking
Each email subscription maintains a status:
pending: Awaiting next sendsent: Successfully deliveredfailed: Delivery failed (includes error message)
Audit logging
All email sending attempts are logged with:
- Operation type:
email_send - Status:
SUCCESSorERROR - Detailed metadata including template, recipients, and environment
- Full error message if failed
- Subscription status updates
Development setup
- Ensure Inbucket is running locally (port 54325)
- Set
MAIL_PROVIDER=inbucketin environment - Emails will be captured by Inbucket instead of being sent
Adding new email types
- Add new type to
EmailTypeenum - Create migration to insert new type into
email_typetable with appropriate status - Implement email template component
- Add template props interface to
TemplatePropstype - Create content generation logic if needed
- Update documentation
Security considerations
- API route is protected by
CRON_SECRETin production - Email subscriptions are scoped to profile ID
- Admin client usage is restricted to system operations
- Sensitive data is not stored in email templates
- API keys are environment-specific
- Emails use notification-specific domains per environment