โ† Back to Connections

Connect Supabase to PostPenguin

Easy SetupLanguage: PostgreSQLTime: 10 minutes

Use Supabase's PostgreSQL database and Edge Functions to receive PostPenguin webhooks. Perfect for modern web applications.

๐Ÿš€ Quick Setup

1. Create Supabase Project

Go to supabase.com and create a project. Note your project URL and anon key.

2. Create Database Table

In Supabase SQL Editor, run:

CREATE TABLE posts (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  postpenguin_id TEXT UNIQUE,
  title TEXT NOT NULL,
  slug TEXT NOT NULL UNIQUE,
  html TEXT NOT NULL,
  meta_title TEXT,
  meta_description TEXT,
  featured_image_url TEXT,
  status TEXT DEFAULT 'publish',
  published_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create indexes for better performance
CREATE INDEX idx_posts_slug ON posts(slug);
CREATE INDEX idx_posts_status ON posts(status);
CREATE INDEX idx_posts_published_at ON posts(published_at);

-- Enable Row Level Security (optional but recommended)
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Allow public read access (adjust as needed)
CREATE POLICY "Public read access" ON posts FOR SELECT USING (true);

3. Create Edge Function

Create supabase/functions/webhooks/postpenguin/index.ts:

import { serve } from 'https://deno.land/[email protected]/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, x-postpenguin-signature, apikey, content-type',
}

serve(async (req) => {
  // Handle CORS
  if (req.method === 'OPTIONS') {
    return new Response('ok', { headers: corsHeaders })
  }

  if (req.method !== 'POST') {
    return new Response(JSON.stringify({ error: 'Method not allowed' }), {
      status: 405,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' }
    })
  }

  try {
    // Verify webhook signature (recommended)
    const signature = req.headers.get('x-postpenguin-signature')
    const webhookSecret = Deno.env.get('POSTPENGUIN_WEBHOOK_SECRET')

    if (webhookSecret && signature) {
      const payload = await req.text()
      const encoder = new TextEncoder()
      const key = await crypto.subtle.importKey(
        'raw',
        encoder.encode(webhookSecret),
        { name: 'HMAC', hash: 'SHA-256' },
        false,
        ['sign']
      )

      const signatureBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(payload))
      const expectedSignature = Array.from(new Uint8Array(signatureBytes))
        .map(b => b.toString(16).padStart(2, '0'))
        .join('')

      const receivedSignature = signature.replace('sha256=', '')

      if (receivedSignature !== expectedSignature) {
        return new Response(JSON.stringify({ error: 'Invalid signature' }), {
          status: 401,
          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
        })
      }
    }

    // Parse request body
    const body = JSON.parse(await req.text())
    const { title, slug, html, meta_title, meta_description, featured_image } = body

    // Validate required fields
    if (!title || !slug || !html) {
      return new Response(JSON.stringify({ error: 'Missing required fields' }), {
        status: 400,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
      })
    }

    // Create Supabase client
    const supabase = createClient(
      Deno.env.get('SUPABASE_URL') ?? '',
      Deno.env.get('SUPABASE_ANON_KEY') ?? ''
    )

    // Save post to database
    const { data, error } = await supabase
      .from('posts')
      .upsert({
        postpenguin_id: body.postPenguinId || crypto.randomUUID(),
        title,
        slug,
        html,
        meta_title: meta_title || title,
        meta_description: meta_description || '',
        featured_image_url: featured_image || '',
        status: 'publish',
        published_at: new Date().toISOString(),
        updated_at: new Date().toISOString(),
      }, {
        onConflict: 'postpenguin_id'
      })

    if (error) {
      console.error('Database error:', error)
      return new Response(JSON.stringify({ error: 'Database error' }), {
        status: 500,
        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
      })
    }

    return new Response(JSON.stringify({
      success: true,
      post: data[0],
      message: 'Post received and saved'
    }), {
      headers: { ...corsHeaders, 'Content-Type': 'application/json' }
    })

  } catch (error) {
    console.error('Webhook error:', error)
    return new Response(JSON.stringify({ error: 'Internal server error' }), {
      status: 500,
      headers: { ...corsHeaders, 'Content-Type': 'application/json' }
    })
  }
})

4. Deploy Edge Function

# Install Supabase CLI
npm install supabase --save-dev

# Login to Supabase
npx supabase login

# Link to your project
npx supabase link --project-ref your-project-ref

# Deploy function
npx supabase functions deploy webhooks/postpenguin

5. Set Environment Variables

In Supabase Dashboard > Edge Functions > Environment Variables:

POSTPENGUIN_WEBHOOK_SECRET=your-webhook-secret-here

The SUPABASE_URL and SUPABASE_ANON_KEY are automatically available.

6. Configure PostPenguin

When adding your site to PostPenguin:

  • Webhook URL: https://your-project-ref.supabase.co/functions/v1/webhooks/postpenguin
  • Secret Key: Same as POSTPENGUIN_WEBHOOK_SECRET

๐ŸŽจ Frontend Integration

React with Supabase Client

// components/BlogPosts.js
import { useState, useEffect } from 'react'
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)

export default function BlogPosts() {
  const [posts, setPosts] = useState([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchPosts()
  }, [])

  const fetchPosts = async () => {
    try {
      const { data, error } = await supabase
        .from('posts')
        .select('*')
        .eq('status', 'publish')
        .order('published_at', { ascending: false })
        .limit(10)

      if (error) throw error

      setPosts(data)
    } catch (error) {
      console.error('Error fetching posts:', error)
    } finally {
      setLoading(false)
    }
  }

  if (loading) return <div>Loading posts...</div>

  return (
    <div className="blog-posts">
      {posts.map(post => (
        <article key={post.id}>
          {post.featured_image_url && (
            <img src={post.featured_image_url} alt={post.title} />
          )}
          <h2 className="text-gray-900">
            <a href={`/posts/${post.slug}`}>{post.title}</a>
          </h2>
          <div dangerouslySetInnerHTML={{ __html: post.html }} />
          <time>{new Date(post.published_at).toLocaleDateString()}</time>
        </article>
      ))}
    </div>
  )
}

๐Ÿงช Testing

Test Webhook Function

# Test the webhook function
curl -X POST https://your-project-ref.supabase.co/functions/v1/webhooks/postpenguin \
  -H "Authorization: Bearer YOUR_ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Test Post from Supabase",
    "slug": "test-post-supabase",
    "html": "<p className="text-gray-700">This is a test post from Supabase Edge Function.</p>",
    "meta_title": "Test Post Title",
    "meta_description": "Testing Supabase webhook integration",
    "featured_image": "https://via.placeholder.com/800x400",
    "status": "publish"
  }'

Need Help?

Read our webhook documentation for technical details, or contact support for custom integrations.