โ 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-hereThe 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.