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 prompted2. 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.