Issue: chrishannah/miniship#25
Type: Pro Feature
Estimated Time: 8-12 hours
Implement email notification system for Miniship subscribers to receive updates when new releases are published. This is a Pro-tier feature requiring Stripe subscription validation.
-- Subscribers table
CREATE TABLE subscribers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
project_id UUID REFERENCES projects(id) ON DELETE CASCADE,
email TEXT NOT NULL,
subscribed_at TIMESTAMPTZ DEFAULT NOW(),
unsubscribed_at TIMESTAMPTZ,
unsubscribe_token UUID DEFAULT gen_random_uuid(),
notification_settings JSONB DEFAULT '{"release": true, "changelog": false}',
UNIQUE(user_id, project_id)
);
-- Notification queue/log
CREATE TABLE notification_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
subscriber_id UUID REFERENCES subscribers(id) ON DELETE CASCADE,
release_id UUID REFERENCES releases(id) ON DELETE CASCADE,
sent_at TIMESTAMPTZ DEFAULT NOW(),
email_id TEXT, -- Resend email ID
status TEXT DEFAULT 'sent', -- sent, failed, bounced
error_message TEXT
);
-- Indexes
CREATE INDEX idx_subscribers_project ON subscribers(project_id) WHERE unsubscribed_at IS NULL;
CREATE INDEX idx_subscribers_user ON subscribers(user_id);
CREATE INDEX idx_notification_logs_release ON notification_logs(release_id);Use Resend (already in stack per previous issues):
- API key stored in environment variables
- Rate limiting: 100 emails/hour on free tier
- Need to verify sending domain
-
Create migration (
supabase/migrations/YYYYMMDDHHMMSS_email_notifications.sql)- Subscribers table
- Notification logs table
- RLS policies (subscribers can only see their own)
-
Create TypeScript types (
lib/types/notifications.ts)export interface Subscriber { id: string; userId: string; projectId: string; email: string; subscribedAt: string; unsubscribedAt?: string; unsubscribeToken: string; notificationSettings: { release: boolean; changelog: boolean; }; }
-
Database helpers (
lib/db/subscribers.ts)getSubscribers(projectId)addSubscriber(userId, projectId)removeSubscriber(userId, projectId)unsubscribeByToken(token)
-
Create email components (
emails/)- Use React Email (react.email) for templates
NewReleaseEmail.tsx- main notification templateWelcomeEmail.tsx- subscription confirmation
-
Email content structure
Subject: [Project Name] New Release: v1.0.0 - Release title & version - Changelog highlights (first 3 items) - Download link - View full release link - Unsubscribe link (footer) -
Resend integration (
lib/email/resend.ts)import { Resend } from 'resend'; const resend = new Resend(process.env.RESEND_API_KEY); export async function sendReleaseNotification( subscriber: Subscriber, release: Release ) { return await resend.emails.send({ from: 'Miniship <releases@miniship.app>', to: subscriber.email, subject: `[${release.projectName}] New Release: ${release.version}`, react: NewReleaseEmail({ release, unsubscribeToken: subscriber.unsubscribeToken }), }); }
-
Public subscription form (
app/[username]/[project]/subscribe/page.tsx)- Email input (pre-filled if authenticated)
- Notification preferences checkboxes
- Pro feature badge/gate
- reCAPTCHA for anonymous subscribers
-
Settings page (
app/dashboard/projects/[id]/subscribers/page.tsx)- List active subscribers
- Export subscriber list (CSV)
- Subscriber count stats
- Manual notification test button
-
User subscription management (
app/dashboard/subscriptions/page.tsx)- List projects user is subscribed to
- Unsubscribe buttons
- Notification preferences toggles
-
Trigger on release publish (
app/api/releases/[id]/publish/route.ts)// After publishing release if (project.owner.isProUser) { await queueReleaseNotifications(release.id); }
-
Background job (
lib/jobs/send-release-notifications.ts)- Get all active subscribers for project
- Batch send (max 50/minute to respect rate limits)
- Log each send to notification_logs
- Handle bounces/failures
-
Optional: Use Inngest/Trigger.dev for job queue
- More reliable than API route
- Automatic retries
- Better observability
-
Unsubscribe page (
app/unsubscribe/page.tsx)- Token validation
- Confirmation message
- Optional: feedback form (why unsubscribing?)
-
API route (
app/api/unsubscribe/route.ts)- Validate token
- Set
unsubscribed_at - Return success/error
-
Check subscription status
// Only Pro users can enable notifications if (!user.hasProSubscription) { return <UpgradePrompt feature="Email Notifications" />; }
-
Feature flag in project settings
- Toggle "Email notifications enabled"
- Disable during maintenance/rate limit issues
Add to .env.example and production:
RESEND_API_KEY=re_xxxxx
RESEND_SENDING_DOMAIN=miniship.app
NOTIFICATION_RATE_LIMIT_PER_MINUTE=50- Subscribe to project notifications (authenticated)
- Subscribe to project notifications (anonymous)
- Publish new release → notification sent
- Unsubscribe via email link
- Notification preferences update
- Pro user gates enforced
- Email renders correctly (mobile + desktop)
- Rate limiting works (mock 100+ subscribers)
- Bounce handling
- Re-subscribe after unsubscribe
- Deploy database migration
- Deploy email templates + API routes
- Test with beta users (Chris's projects)
- Monitor Resend dashboard for bounces/spam
- Enable for all Pro users
- Add to marketing site as Pro feature
- Digest mode (weekly summary instead of per-release)
- Customize email frequency
- Email preview before publish
- Analytics (open rates, click rates)
- Multi-language email templates
- Resend account + API key
- Domain verification for sending (miniship.app)
- React Email library
- Pro subscription validation logic
Risk: Email deliverability issues
Mitigation: Use Resend (good reputation), verify domain, include unsubscribe
Risk: Spam reports
Mitigation: Double opt-in, clear unsubscribe, only notify on actual releases
Risk: Rate limits on free Resend tier
Mitigation: Batch sends, upgrade plan if needed, queue system
Risk: Users forget they subscribed → spam complaints
Mitigation: Include context in email ("You subscribed to updates for Project X")
- Day 1: Database schema + models (Phase 1)
- Day 2: Email templates + Resend integration (Phase 2)
- Day 3: UI for subscription management (Phase 3)
- Day 4: Release hook + background jobs (Phase 4)
- Day 5: Unsubscribe flow + Pro gates (Phase 5-6)
- Day 6: Testing + refinement
Total: 5-6 days (or 8-12 focused hours spread across time)
Ready to implement? Start with Phase 1 (database schema) to establish foundation.