h MDX for version-controlled content
- Data Layer: Supabase (PostgreSQL + Row Level Security + Edge Functions)
- Distribution Layer: Resend (email), GitHub Actions (scheduled social/content sync), OpenAI API (assisted drafting)
- Analytics Layer: Plausible or PostHog (privacy-first, event-driven)
- Hosting: Vercel (edge deployment, preview environments, automatic rollbacks)
Step 2: Content Pipeline Implementation
Store content as Markdown/MDX in Git. Sync to Supabase via a build-time or on-demand resolver. This ensures diffing, rollbacks, and collaborative PR workflows.
// lib/content-sync.ts
import { createClient } from '@supabase/supabase-js'
import { readFileSync, readdirSync } from 'fs'
import { join } from 'path'
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
export async function syncContentToDB(contentDir: string) {
const files = readdirSync(contentDir).filter(f => f.endsWith('.md'))
const results = []
for (const file of files) {
const raw = readFileSync(join(contentDir, file), 'utf-8')
const { data, error } = await supabase
.from('marketing_content')
.upsert({
slug: file.replace('.md', ''),
body: raw,
updated_at: new Date().toISOString()
}, { onConflict: 'slug' })
if (error) console.error(`Failed to sync ${file}:`, error)
else results.push(data)
}
return results
}
Step 3: Automated Distribution Pipeline
Use GitHub Actions to trigger distribution jobs. This removes manual scheduling and ensures idempotent execution.
# .github/workflows/marketing-distribution.yml
name: Marketing Distribution
on:
schedule:
- cron: '0 9 * * 1,3,5' # Mon, Wed, Fri at 9 AM UTC
workflow_dispatch:
jobs:
distribute:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx ts-node scripts/distribute.ts
env:
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_SERVICE_KEY: ${{ secrets.SUPABASE_SERVICE_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
Step 4: Webhook-Driven Lead Capture
Replace third-party form builders with a lightweight API route that validates, stores, and triggers sequences.
// app/api/leads/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@supabase/supabase-js'
import { Resend } from 'resend'
const supabase = createClient(process.env.SUPABASE_URL!, process.env.SUPABASE_SERVICE_KEY!)
const resend = new Resend(process.env.RESEND_API_KEY)
export async function POST(request: Request) {
try {
const { email, source, utm_params } = await request.json()
if (!email?.includes('@')) {
return NextResponse.json({ error: 'Invalid email' }, { status: 400 })
}
// Upsert lead with idempotency key
const { error } = await supabase
.from('leads')
.upsert({ email, source, utm_params, created_at: new Date().toISOString() }, { onConflict: 'email' })
if (error) throw error
// Trigger welcome sequence
await resend.emails.send({
from: 'updates@yourdomain.com',
to: [email],
subject: 'Welcome to the product',
html: `<p>Thanks for joining. We'll share updates weekly.</p>`
})
return NextResponse.json({ status: 'queued' })
} catch (err) {
return NextResponse.json({ error: 'Processing failed' }, { status: 500 })
}
}
Architecture Decisions & Rationale
- Supabase over Firebase: PostgreSQL provides relational integrity for lead segmentation, content tagging, and campaign attribution. Row-level security enables safe client-side queries without exposing service keys.
- MDX/MD in Git: Enables
git diff for content changes, PR reviews for messaging, and automatic rollback on deployment. No CMS login required.
- Vercel Edge Runtime: Reduces cold starts for webhook handlers and keeps latency under 100ms for lead capture.
- GitHub Actions over Cron Services: Native to the repository, uses existing secrets management, and provides execution logs without third-party dashboards.
Pitfall Guide
-
Storing dynamic content in the database instead of Git
Database-stored content breaks version control, makes diffing impossible, and complicates rollbacks. Keep source-of-truth in Markdown/MDX. Sync to DB only for querying and distribution.
-
Ignoring API rate limits on email and social platforms
Resend, Twitter/X, and LinkedIn enforce strict rate limits. Batch requests, implement exponential backoff, and queue jobs via a lightweight task runner (e.g., bullmq or Supabase Edge Functions) instead of synchronous loops.
-
Hardcoding secrets in client-side bundles
Marketing scripts often leak API keys when bundled incorrectly. Use process.env with NEXT_PUBLIC_ prefixes only for non-sensitive endpoints. Route all secret-dependent calls through server-side API routes or Edge Functions.
-
Skipping analytics event standardization
Without a consistent event schema, attribution breaks. Define a strict interface for track() calls: event_name, user_id/session_id, properties, timestamp. Validate against a JSON schema before sending to Plausible/PostHog.
-
Treating AI-assisted content as production-ready
LLM outputs require deterministic post-processing. Implement a validation layer that checks for broken links, tone consistency, and compliance markers before publishing. Always version AI drafts alongside human edits.
-
No fallback mechanism for distribution failures
Webhooks timeout, APIs return 5xx, and queues stall. Implement dead-letter queues, retry policies with jitter, and alerting via Slack/Discord webhooks when failure rates exceed 2% over a 15-minute window.
-
Skipping idempotency in lead capture
Duplicate submissions corrupt attribution and spam users. Always use upsert with a unique constraint on email or user_id. Return 200 OK even on duplicate to prevent client-side retries from cascading.
Best Practices from Production:
- Keep the marketing stack under 400 lines of custom code. Complexity scales non-linearly with team size.
- Use preview deployments for every content PR. Verify rendering before merge.
- Monitor deliverability, not just opens. Track bounce rate, spam complaints, and inbox placement.
- Version your marketing schemas. Treat
leads and campaigns tables like API contracts.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Bootstrapped SaaS (<1k MRR) | MDX + Supabase + Resend + GitHub Actions | Lowest operational overhead, full version control, predictable $10-30/mo infra | $0-$30/mo |
| Open Source Project | Static site + Plausible + Discord/Slack webhooks | Community-driven, no email compliance overhead, focus on engagement metrics | $0/mo |
| Agency Freelancer | Next.js + Supabase + Vercel + PostHog | Client reporting requires attribution, multi-project isolation, audit trails | $20-$50/mo |
| Content Creator/Newsletter | Ghost/ConvertKit alternative via Supabase + Resend | Higher deliverability, custom segmentation, avoid platform fees | $15-$40/mo |
Configuration Template
# .env.local
NEXT_PUBLIC_SITE_URL=https://yourdomain.com
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
RESEND_API_KEY=re_1234567890abcdef
OPENAI_API_KEY=sk-proj-xxxxxxxxxxxx
POSTHOG_PROJECT_API_KEY=phc_xxxxxxxxxxxxx
SLACK_ALERT_WEBHOOK=https://hooks.slack.com/services/T00/B00/xxxx
# vercel.json
{
"framework": "nextjs",
"functions": {
"app/api/**/*.ts": {
"runtime": "edge"
}
},
"env": {
"SUPABASE_SERVICE_KEY": "@supabase_service_key",
"RESEND_API_KEY": "@resend_api_key"
}
}
Quick Start Guide
- Clone the template repository and run
npm install
- Copy
.env.example to .env.local, populate Supabase, Resend, and analytics keys
- Run
npx supabase init && npx supabase db push to provision tables and RLS policies
- Start development server:
npm run dev, verify lead capture at /api/leads
- Push to GitHub, enable Vercel auto-deployment, trigger first workflow via
workflow_dispatch
The one-person marketing OS eliminates context switching, enforces engineering discipline on growth workflows, and scales without hiring. Treat marketing as code, version everything, and automate distribution. The rest is iteration.