โ† Back to Connections

Connect Firebase to PostPenguin

Easy SetupPlatform: FirebaseTime: 10 minutes

Firebase provides Cloud Functions for serverless compute and Firestore for real-time database storage. This guide shows you how to receive PostPenguin webhooks and store posts in Firebase.

๐Ÿš€ Quick Setup

1. Initialize Firebase Functions

# Install Firebase CLI
npm install -g firebase-tools

# Login and init
firebase login
firebase init functions

# Choose TypeScript when prompted

2. Create Webhook Function

// functions/src/index.ts
import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'
import * as crypto from 'crypto'

admin.initializeApp()
const db = admin.firestore()

function verifySignature(payload: string, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex')
  const received = signature.replace('sha256=', '')
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
}

// PostPenguin webhook handler
export const postpenguinWebhook = functions.https.onRequest(async (req, res) => {
  // Only allow POST
  if (req.method !== 'POST') {
    res.status(405).json({ error: 'Method not allowed' })
    return
  }

  try {
    const body = JSON.stringify(req.body)
    const data = req.body
    
    // Verify signature
    const signature = req.headers['x-postpenguin-signature'] as string
    const secret = functions.config().postpenguin?.webhook_secret
    
    if (secret && signature) {
      if (!verifySignature(body, signature, secret)) {
        res.status(401).json({ error: 'Invalid signature' })
        return
      }
    }
    
    // Validate required fields
    if (!data.title || !data.slug || !data.html) {
      res.status(400).json({ error: 'Missing required fields' })
      return
    }
    
    const postId = data.postPenguinId || `pp_${Date.now()}`
    const now = admin.firestore.Timestamp.now()
    
    // Check if post exists
    const existingDoc = await db.collection('posts')
      .where('postpenguinId', '==', postId)
      .limit(1)
      .get()
    
    const postData = {
      postpenguinId: postId,
      title: data.title,
      slug: data.slug,
      html: data.html,
      metaTitle: data.meta_title || data.title,
      metaDescription: data.meta_description || '',
      featuredImage: data.featured_image || null,
      tags: data.tags || [],
      status: 'publish',
      publishedAt: now,
      updatedAt: now,
    }
    
    let docId: string
    let action: string
    
    if (!existingDoc.empty) {
      // Update existing
      docId = existingDoc.docs[0].id
      await db.collection('posts').doc(docId).update(postData)
      action = 'updated'
    } else {
      // Create new
      const docRef = await db.collection('posts').add({
        ...postData,
        createdAt: now,
      })
      docId = docRef.id
      action = 'created'
    }
    
    console.log(`Post ${action}: ${data.title}`)
    
    res.status(200).json({
      success: true,
      postId: docId,
      postpenguinId: postId,
      action
    })
    
  } catch (error) {
    console.error('Webhook error:', error)
    res.status(500).json({ error: 'Internal server error' })
  }
})

// Get all posts
export const getPosts = functions.https.onRequest(async (req, res) => {
  if (req.method !== 'GET') {
    res.status(405).json({ error: 'Method not allowed' })
    return
  }

  try {
    const limit = parseInt(req.query.limit as string) || 10
    
    const snapshot = await db.collection('posts')
      .where('status', '==', 'publish')
      .orderBy('publishedAt', 'desc')
      .limit(limit)
      .get()
    
    const posts = snapshot.docs.map(doc => ({
      id: doc.id,
      ...doc.data(),
      publishedAt: doc.data().publishedAt?.toDate?.().toISOString(),
      createdAt: doc.data().createdAt?.toDate?.().toISOString(),
      updatedAt: doc.data().updatedAt?.toDate?.().toISOString(),
    }))
    
    res.status(200).json({ posts })
    
  } catch (error) {
    console.error('Error:', error)
    res.status(500).json({ error: 'Internal server error' })
  }
})

// Get post by slug
export const getPostBySlug = functions.https.onRequest(async (req, res) => {
  if (req.method !== 'GET') {
    res.status(405).json({ error: 'Method not allowed' })
    return
  }

  try {
    const slug = req.query.slug as string
    
    if (!slug) {
      res.status(400).json({ error: 'Slug parameter required' })
      return
    }
    
    const snapshot = await db.collection('posts')
      .where('slug', '==', slug)
      .where('status', '==', 'publish')
      .limit(1)
      .get()
    
    if (snapshot.empty) {
      res.status(404).json({ error: 'Post not found' })
      return
    }
    
    const doc = snapshot.docs[0]
    const post = {
      id: doc.id,
      ...doc.data(),
      publishedAt: doc.data().publishedAt?.toDate?.().toISOString(),
      createdAt: doc.data().createdAt?.toDate?.().toISOString(),
      updatedAt: doc.data().updatedAt?.toDate?.().toISOString(),
    }
    
    res.status(200).json({ post })
    
  } catch (error) {
    console.error('Error:', error)
    res.status(500).json({ error: 'Internal server error' })
  }
})

3. Firestore Rules

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Posts collection - read publicly, write only from functions
    match /posts/{postId} {
      allow read: if resource.data.status == 'publish';
      allow write: if false; // Only Cloud Functions can write
    }
  }
}

4. Firestore Indexes

// firestore.indexes.json
{
  "indexes": [
    {
      "collectionGroup": "posts",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "status", "order": "ASCENDING" },
        { "fieldPath": "publishedAt", "order": "DESCENDING" }
      ]
    },
    {
      "collectionGroup": "posts",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "slug", "order": "ASCENDING" },
        { "fieldPath": "status", "order": "ASCENDING" }
      ]
    }
  ]
}

โš™๏ธ Set Config

firebase functions:config:set postpenguin.webhook_secret="your-secret-key-here"

๐Ÿš€ Deploy

# Deploy functions
firebase deploy --only functions

# Deploy Firestore rules and indexes
firebase deploy --only firestore

๐Ÿงช Testing

# Get your function URL from deployment output
FUNCTION_URL="https://us-central1-your-project.cloudfunctions.net"

# Test webhook
curl -X POST $FUNCTION_URL/postpenguinWebhook \
  -H "Content-Type: application/json" \
  -d '{
    "postPenguinId": "test_firebase_123",
    "title": "Test Firebase Post",
    "slug": "test-firebase-post",
    "html": "<p className="text-gray-700">This is a test post in Firebase.</p>",
    "tags": ["firebase", "firestore"]
  }'

# Fetch posts
curl "$FUNCTION_URL/getPosts"

# Get single post
curl "$FUNCTION_URL/getPostBySlug?slug=test-firebase-post"

๐ŸŽจ Frontend Integration

// React example with Firebase SDK
import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore'
import { db } from './firebase'

async function getPosts() {
  const q = query(
    collection(db, 'posts'),
    where('status', '==', 'publish'),
    orderBy('publishedAt', 'desc'),
    limit(10)
  )
  
  const snapshot = await getDocs(q)
  return snapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data()
  }))
}

async function getPostBySlug(slug: string) {
  const q = query(
    collection(db, 'posts'),
    where('slug', '==', slug),
    where('status', '==', 'publish'),
    limit(1)
  )
  
  const snapshot = await getDocs(q)
  if (snapshot.empty) return null
  
  const doc = snapshot.docs[0]
  return { id: doc.id, ...doc.data() }
}

๐Ÿ“ฆ Firebase Hosting with Functions

// firebase.json
{
  "hosting": {
    "public": "public",
    "rewrites": [
      {
        "source": "/api/webhooks/postpenguin",
        "function": "postpenguinWebhook"
      },
      {
        "source": "/api/posts",
        "function": "getPosts"
      },
      {
        "source": "/api/posts/**",
        "function": "getPostBySlug"
      }
    ]
  }
}

Need Help?

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