<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Serverless TypeScript]]></title><description><![CDATA[Serverless TypeScript]]></description><link>https://serverlesstypescript.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 12 Apr 2026 11:24:45 GMT</lastBuildDate><atom:link href="https://serverlesstypescript.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Pinecone x Hashnode: add semantic search to your Hashnode blog posts]]></title><description><![CDATA[Team members

Andrea Amorosi (Hashnode | Twitter | GitHub)

Pascal Vogel (Hashnode | Twitter)


Overview
Made popular together with large language models (LLMs), semantic search has proven to be a powerful new approach for providing relevant search r...]]></description><link>https://serverlesstypescript.com/pinecone-x-hashnode-add-semantic-search-to-your-hashnode-blog-posts</link><guid isPermaLink="true">https://serverlesstypescript.com/pinecone-x-hashnode-add-semantic-search-to-your-hashnode-blog-posts</guid><category><![CDATA[APIHackathon]]></category><category><![CDATA[Pinecone]]></category><category><![CDATA[openai]]></category><category><![CDATA[powertools]]></category><category><![CDATA[serverless]]></category><category><![CDATA[AWS]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[GraphQL]]></category><dc:creator><![CDATA[Andrea Amorosi]]></dc:creator><pubDate>Fri, 02 Feb 2024 18:00:13 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706875559326/45aafb3c-d73c-4c7a-8281-ec12992d9b7b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-team-members">Team members</h2>
<ul>
<li><p><a target="_blank" href="https://hashnode.com/@andreamorosi">Andrea Amorosi</a> (<a target="_blank" href="https://hashnode.com/@andreamorosi">Hashnode</a> | <a target="_blank" href="https://twitter.com/dreamorosi">Twitter</a> | <a target="_blank" href="https://github.com/dreamorosi">GitHub</a>)</p>
</li>
<li><p><a target="_blank" href="https://hashnode.com/@pbv0">Pascal Vogel</a> (<a target="_blank" href="https://hashnode.com/@pbv0">Hashnode</a> | <a target="_blank" href="https://twitter.com/pvogel_">Twitter</a>)</p>
</li>
</ul>
<h2 id="heading-overview">Overview</h2>
<p>Made popular together with large language models (LLMs), semantic search has proven to be a powerful new approach for providing relevant search results based on natural language queries.</p>
<p>Semantic search takes into account the meaning and intention behind search queries and does not rely on exact or near-exact matching of search terms to keywords.</p>
<p>For example, if I search for "warm-weather hat" on amazon.com, I get a lot of results for winter gear although what I'm actually looking for are hats to wear in the summer. In this case, a semantic search would be more successful in understanding my intention and can offer more relevant results.</p>
<p>With this <a target="_blank" href="https://github.com/dreamorosi/serverless-semantic-search-hashnode">Pinecone x Hashnode integration</a>, a submission to the <a target="_blank" href="https://hashnode.com/hackathons/apihackathon">Hashnode APIs Hackathon</a> in Category #1, you can easily set up semantic search for your Hashnode blog posts and query them using a serverless REST API endpoint.</p>
<p>To implement this, we rely on <a target="_blank" href="https://www.pinecone.io/">Pinecone</a>, a managed vector database, the <a target="_blank" href="https://openai.com/">OpenAI</a> API for embedding models, and <a target="_blank" href="https://aws.amazon.com/serverless/">AWS serverless services</a> to host the solution in your own AWS account.</p>
<p>This project builds on <a target="_blank" href="https://serverlesstypescript.com/hashbridge-extend-your-hashnode-blog-with-event-driven-serverless-functions">HashBridge</a> (<a target="_blank" href="https://github.com/dreamorosi/serverless-webhook-hashnode">GitHub</a>) which makes it easy to set up integrations between Hashnode and AWS serverless functions with <a target="_blank" href="https://aws.amazon.com/lambda/">AWS Lambda</a>.</p>
<p>In the rest of this blog post, we go into more detail on how you can set up and use the Pinecone x Hashnode integration, how it works, and the implementation decisions we made.</p>
<h2 id="heading-how-to-use-pinecone-x-hashnode">How to use Pinecone x Hashnode</h2>
<p>If you want to get started quickly, watch this 8 minute demo to learn how to set up and use HashBridge with your Hashnode blog:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=4OwumYqpT5I">https://www.youtube.com/watch?v=4OwumYqpT5I</a></div>
<p> </p>
<p>We provide detailed deployment instructions and the full source code in our <a target="_blank" href="https://github.com/dreamorosi/serverless-webhook-hashnode">GitHub repository</a>. Your issues and contributions are very welcome!</p>
<h2 id="heading-how-pinecone-x-hashnode-works">How Pinecone x Hashnode works</h2>
<p>The architecture of this Pinecone x Hashnode integration can roughly be split in two parts that work independently:</p>
<ol>
<li><p><strong>Content ingestion</strong>: whenever a blog post is published, updated, or deleted on our Hashnode blog, we need to update our vector database to reflect this change. In addition, we need to be able to do an initial ingestion of existing blog posts into the vector database.</p>
</li>
<li><p><strong>Search</strong>: when a user submits a search query to our search API, we need to run a search in our vector database and return the most relevant results.</p>
</li>
</ol>
<p>Sounds easy enough! In the following sections, we explain the architecture and underlying technical concepts of these two elements in detail. But first, let's do a quick primer on semantic search and vector embeddings.</p>
<h3 id="heading-primer-semantic-search-and-vector-embeddings">Primer: semantic search and vector embeddings</h3>
<p>Vector embeddings are the basis for semantic search. A vector embedding is a series of numbers (a vector) in a vector space that represents an object such as a word, sentence, or entire document. Each object is a point with a certain position in the vector space. By measuring the distance between points, we can assess and compare their similarity.</p>
<p><img src="https://cdn.sanity.io/images/vr8gru94/production/ec6f20344b056465a17d98cc98b834eb364f93ce-1432x504.png" alt="Semantic similarity in sentence embeddings." /></p>
<p><em>Similar sentences are positioned close-by in the vector space (Source:</em><a target="_blank" href="https://deepai.org/publication/in-search-for-linear-relations-in-sentence-embedding-spaces"><em>DeepAI</em></a><em>).</em></p>
<p>In case of semantic search, we can use this to our advantage. We generate vector embeddings for our Hashnode blog posts which positions them in the vector space. When a user inputs a search query, we also generate a vector embedding that represents the query. Finally, we can compare the position of the search query with the positions of our blog content and find the content that is closest and therefore most similar to the query.</p>
<p>To generate vector embeddings, we can use an embedding model. This type of LLM takes a text input and returns a vector with a certain number of dimensions. Besides countless open-source models that you can host yourself, there are also managed models available from <a target="_blank" href="https://openai.com/">OpenAI</a> or via <a target="_blank" href="https://aws.amazon.com/bedrock/">Amazon Bedrock</a>.</p>
<h3 id="heading-ingesting-hashnode-events-and-storing-vector-embeddings-in-pinecone">Ingesting Hashnode events and storing vector embeddings in Pinecone</h3>
<p><img src="https://lh7-us.googleusercontent.com/_ktu_hbdLWUAznOKyrgYgbGM1Bt56zfH_crOexfEDxO_2zB0Y-gGuKUiHczXNHLws_84Fuo31IBJOlkE1OLYAswHTZ6l3PpHqId3tWURMnS1nZ3OyR87MkQk90IEWQCgDkYKe6EWtcdwCic52U3k9Q" alt /></p>
<p>Because this integration builds on <a target="_blank" href="https://serverlesstypescript.com/hashbridge-extend-your-hashnode-blog-with-event-driven-serverless-functions">HashBridge</a> (<a target="_blank" href="https://github.com/dreamorosi/serverless-webhook-hashnode">GitHub</a>), we already have a way to receive webhook events from Hashnode on an EventBridge event bus in our AWS account which triggers a serverless AWS Lambda function. HashBridge uses the Hashnode GraphQL API to enrich webhook events with additional data and metadata such as blog post content in markdown, author, URL, and more.</p>
<p><a target="_blank" href="https://github.com/dreamorosi/serverless-semantic-search-hashnode/blob/main/src/functions/consumer/index.ts">A Lambda function</a>, written in TypeScript, contains our logic for generating embeddings and communicating with the Pinecone vector database.</p>
<p>Pinecone is a fully-managed vector database that enables AI use cases like semantic search, retrieval-augmented generation (RAG), and more. The database is integrated with popular frameworks like <a target="_blank" href="https://www.langchain.com/">LangChain</a> and <a target="_blank" href="https://www.llamaindex.ai/">LlamaIndex</a> and offers clients for <a target="_blank" href="https://github.com/pinecone-io/pinecone-python-client">Python</a>, <a target="_blank" href="https://github.com/pinecone-io/pinecone-ts-client">Node.js</a>, and <a target="_blank" href="https://github.com/pinecone-io/pinecone-java-client">Java</a> as well as a <a target="_blank" href="https://docs.pinecone.io/reference">REST API</a>.</p>
<p>Pinecone offers both <a target="_blank" href="https://www.pinecone.io/pricing/pods/">pod-based</a> and <a target="_blank" href="https://www.pinecone.io/pricing/">serverless</a> pricing options, the latter in preview. We tested this integration with the pod-based free tier but it should work similar in case of the serverless option.</p>
<p>One challenge when generating vector embeddings is the context size supported by embedding models. <a target="_blank" href="https://platform.openai.com/docs/guides/embeddings/what-are-embeddings">OpenAI’s <code>ext-embedding-3-small</code> embedding model</a>, which we are using here, supports up to 8191 input tokens, which translates to roughly 32,000 characters in case of English-language text.</p>
<p>This means that for blog posts with more than 32,000 characters, we need to split the content into multiple chunks. We also need to consider that we are embedding the markdown content instead of text content to not lose URLs or code formatting metadata. Depending on the use case, different chunking strategies can be applied to improve retrieval accuracy. <a target="_blank" href="https://www.pinecone.io/learn/chunking-strategies/">This Pinecone blog post</a> goes into more detail.</p>
<p>In our case, we decide to break the markdown blog content into chunks of 500 characters with an overlap of 50 characters before generating vector embeddings for each chunk. <a target="_blank" href="https://github.com/langchain-ai/langchainjs">LangChain</a>’s Markdown splitter helps with keeping text with common context together during chunking based on the Markdown document structure.</p>
<p>To store these chunks which all make up one blog post, we follow <a target="_blank" href="https://docs.pinecone.io/docs/manage-rag-documents">Pinecone’s recommendation</a> of <a target="_blank" href="https://docs.pinecone.io/docs/upsert-data">upserting</a> each chunk separately as a distinct record. We use ID prefixes to connect each chunk to its parent document. The resulting structure looks something like this:</p>
<pre><code class="lang-json">{<span class="hljs-attr">"id"</span>: <span class="hljs-string">"blogid1#chunk1"</span>, <span class="hljs-attr">"values"</span>: [<span class="hljs-number">0.1</span>, <span class="hljs-number">0.1</span>, <span class="hljs-number">0.1</span>, <span class="hljs-number">0.1</span>, <span class="hljs-number">0.1</span>, <span class="hljs-number">0.1</span>, <span class="hljs-number">0.1</span>, <span class="hljs-number">0.1</span>, ...]},
{<span class="hljs-attr">"id"</span>: <span class="hljs-string">"blogid1#chunk2"</span>, <span class="hljs-attr">"values"</span>: [<span class="hljs-number">0.2</span>, <span class="hljs-number">0.2</span>, <span class="hljs-number">0.2</span>, <span class="hljs-number">0.2</span>, <span class="hljs-number">0.2</span>, <span class="hljs-number">0.2</span>, <span class="hljs-number">0.2</span>, <span class="hljs-number">0.2</span>, ...]},
{<span class="hljs-attr">"id"</span>: <span class="hljs-string">"blogid1#chunk3"</span>, <span class="hljs-attr">"values"</span>: [<span class="hljs-number">0.3</span>, <span class="hljs-number">0.3</span>, <span class="hljs-number">0.3</span>, <span class="hljs-number">0.3</span>, <span class="hljs-number">0.3</span>, <span class="hljs-number">0.3</span>, <span class="hljs-number">0.3</span>, <span class="hljs-number">0.3</span>, ...]},
{<span class="hljs-attr">"id"</span>: <span class="hljs-string">"blogid1#chunk4"</span>, <span class="hljs-attr">"values"</span>: [<span class="hljs-number">0.4</span>, <span class="hljs-number">0.4</span>, <span class="hljs-number">0.4</span>, <span class="hljs-number">0.4</span>, <span class="hljs-number">0.4</span>, <span class="hljs-number">0.4</span>, <span class="hljs-number">0.4</span>, <span class="hljs-number">0.4</span>, ...]}
</code></pre>
<p>In addition to embedding new posts, you might already have a whole bunch of posts in your Hashnode blog that you want to make searchable. In this case, you can use our <a target="_blank" href="https://github.com/dreamorosi/serverless-semantic-search-hashnode/blob/main/src/scripts/index-existing-posts.mts">initial embedding script</a> which will perform the query, chunk, embed, and store steps necessary to get all of your blog posts into Pinecone.</p>
<p>To update or delete a blog post from Pinecone based on a Hashnode event, we need to be a bit more careful.</p>
<h3 id="heading-using-the-search-api">Using the search API</h3>
<p><img src="https://lh7-us.googleusercontent.com/8OWss-1Wm3OJzvBUY80-Z32io_sZeFijUZ1sDclSNUw4NIo4NNDTJSrWLAPBGIx4lHLUeyLlotkbXEQ_37TTSfkoZZJQ37jEnsA9_d9dwHlH_a96QxN4M4gxFaFD5Nbd0lDcbzt9Fsq8o0FnWd0bgQ" alt /></p>
<p>Our search API consists of a Lambda function with its <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html">function URL and IAM authentication enabled</a>. The function URL is protected by a CloudFront distribution where a <a target="_blank" href="https://aws.amazon.com/lambda/edge/">Lambda@Edge</a> function signs each request using <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html">Sigv4</a>. This setup is very similar to what we used to build HashBridge, so head over to the <a target="_blank" href="https://serverlesstypescript.com/hashbridge-extend-your-hashnode-blog-with-event-driven-serverless-functions">HashBridge announcement blog post</a> and take a look at the <em>Verifying webhook events with Lambda@Edge</em> section for more details.</p>
<p>We process search queries in the <a target="_blank" href="https://github.com/dreamorosi/serverless-semantic-search-hashnode/blob/main/src/functions/search/index.ts">Search API handler function</a>. This function performs the following tasks:</p>
<ol>
<li><p>Get vector embeddings for the search query text from OpenAI.</p>
</li>
<li><p>Run a <a target="_blank" href="https://docs.pinecone.io/docs/query-data">query</a> operation on Pinecone to identify the chunks most similar to query embeddings.</p>
</li>
<li><p>Extract the post ID from the three similar Pinecone query result chunks and use the Hashnode GraphQL API to get the post brief.</p>
</li>
</ol>
<p>Let's look at an example where we ask a natural language question to the <a target="_blank" href="https://engineering.hashnode.com/">Hashnode Engineering Blog</a>.</p>
<p>After deploying the integration, we can make a simple HTTP <code>GET</code> request to the search API endpoint and include our query as a URL parameter. In this case, we are using <a target="_blank" href="https://httpie.io/"><code>httpie</code></a> to make the request from the command line for the query "peer review infra changes":</p>
<pre><code class="lang-bash">http https://dkzum3j6x4pzr.cloudfront.net/search text==<span class="hljs-string">"peer review infra changes"</span>
</code></pre>
<p>The search API would return a response that contains the three most similar results, including their author, brief, similarity score, and more:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"matches"</span>: [
        {
            <span class="hljs-attr">"post"</span>: {
                <span class="hljs-attr">"author"</span>: {
                    <span class="hljs-attr">"name"</span>: <span class="hljs-string">"Shad Mirza"</span>,
                    <span class="hljs-attr">"username"</span>: <span class="hljs-string">"iamshadmirza"</span>
                },
                <span class="hljs-attr">"brief"</span>: <span class="hljs-string">"Hey, everyone! If you've been using AWS, chances are you've come across CDK and building cloud apps with it. As seamless as it is to deploy apps using CDK, it is equally important to monitor changes in infrastructure code and prevent issues.\nThis gui..."</span>,
                <span class="hljs-attr">"id"</span>: <span class="hljs-string">"647ed1c80fce655a6be9ac07"</span>,
                <span class="hljs-attr">"title"</span>: <span class="hljs-string">"Simple Steps to Include CDK Diffs in GitHub PRs"</span>
            },
            <span class="hljs-attr">"similarity_score"</span>: <span class="hljs-number">0.4194794</span>
        },
        <span class="hljs-comment">// 2 more matches</span>
    ]
}
</code></pre>
<p>And, sure enough, when we check the result with the highest score, we find a relevant discussion covering the topic addressed in our query in the post content despite the exact terms not being present in the title or brief of the post (<a target="_blank" href="https://engineering.hashnode.com/simple-steps-to-include-cdk-diffs-in-github-prs">Simple Steps to Include CDK Diffs in GitHub PRs</a>).</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Our Pinecone x Hashnode integration makes it easy to add semantic search to your Hashnode blog. In particular if you are using <a target="_blank" href="https://hashnode.com/headless">Hashnode's headless mode</a>, you may want to offer a unified semantic search experience across your existing website content and your Hashnode blog content. You can ingest all of these content types into Pinecone and our solution provides a simple but accurate search enpoint.</p>
<p>Let us know what you think in the comments or open a <a target="_blank" href="https://github.com/dreamorosi/serverless-semantic-search-hashnode">GitHub</a> issue with any questions and feedback.</p>
]]></content:encoded></item><item><title><![CDATA[HashBridge: extend your Hashnode blog with event-driven serverless functions]]></title><description><![CDATA[Team members

Andrea Amorosi (Hashnode | Twitter | GitHub)

Pascal Vogel (Hashnode | Twitter)


Overview
When we saw the Hashnode APIs Hackathon announcement, we thought long and hard about what integration could make an impactful addition to the Has...]]></description><link>https://serverlesstypescript.com/hashbridge-extend-your-hashnode-blog-with-event-driven-serverless-functions</link><guid isPermaLink="true">https://serverlesstypescript.com/hashbridge-extend-your-hashnode-blog-with-event-driven-serverless-functions</guid><category><![CDATA[powertools]]></category><category><![CDATA[APIHackathon]]></category><category><![CDATA[serverless]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[AWS]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[event-driven-architecture]]></category><dc:creator><![CDATA[Pascal Vogel]]></dc:creator><pubDate>Thu, 01 Feb 2024 18:36:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706727655321/c46a88ed-9af7-4a4c-a391-397da4ce7d3e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-team-members">Team members</h2>
<ul>
<li><p><a target="_blank" href="https://hashnode.com/@andreamorosi">Andrea Amorosi</a> (<a target="_blank" href="https://hashnode.com/@andreamorosi">Hashnode</a> | <a target="_blank" href="https://twitter.com/dreamorosi">Twitter</a> | <a target="_blank" href="https://github.com/dreamorosi">GitHub</a>)</p>
</li>
<li><p><a target="_blank" href="https://hashnode.com/@pbv0">Pascal Vogel</a> (<a target="_blank" href="https://hashnode.com/@pbv0">Hashnode</a> | <a target="_blank" href="https://twitter.com/pvogel_">Twitter</a>)</p>
</li>
</ul>
<h2 id="heading-overview">Overview</h2>
<p>When we saw the <a target="_blank" href="https://hashnode.com/hackathons/apihackathon">Hashnode APIs Hackathon</a> announcement, we thought long and hard about what integration could make an impactful addition to the Hashnode ecosystem for the hackathon Category #1. With this submission, we decided to build a re-usable integration that anyone can use to extend their Hashnode blog with <a target="_blank" href="https://aws.amazon.com/">Amazon Web Services (AWS</a>).</p>
<p><strong>HashBridge</strong> is an integration that allows you to connect your Hashnode blog to serverless <a target="_blank" href="https://aws.amazon.com/lambda/">AWS Lambda</a> functions and other resources based on events in your Hashnode blog.</p>
<p>Each time you publish, update or delete a blog post, you trigger a serverless function that receives blog content and metadata such as URL, author, and more as an input. Inside your function, you can then write code in <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html">the programming language of your choice</a> that communicates with AWS services or any external API.</p>
<p>For example, you could use a text-to-speech service to generate an audio version of your blog post, use AI to generate a cover image, or cross-post to social media. All of this without setting up and managing your own infrastructure and at close to zero cost for running HashBridge in your own AWS account.</p>
<p>In the rest of this blog post, we go into more detail on how you can set up and use HashBridge, how we built it, and the implementation decisions we made.</p>
<h2 id="heading-how-to-use-hashbridge">How to use HashBridge</h2>
<p>Use the following quick instructions to deploy HashBridge to your AWS account. For more detailed deployment instructions and the full source code, head over to the <a target="_blank" href="https://github.com/dreamorosi/serverless-webhook-hashnode">GitHub repository</a>. Your issues and contributions are very welcome.</p>
<p>As a prerequisite, you need <a target="_blank" href="https://repost.aws/knowledge-center/create-and-activate-aws-account">an AWS account</a> and Node.js installed. Next, follow these four steps to set up HashBridge:</p>
<ol>
<li><p>Go to your Hashnode blog dashboard, choose <strong>Webhooks</strong> and then <strong>Add New Webhook</strong>. Do not input an URL just yet but select all of the <code>post_*</code> events and <strong>copy the secret value</strong>. Keep this window open and we'll come back later with the URL.</p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/create_secret.html">Create a new secret</a> in <a target="_blank" href="https://aws.amazon.com/secrets-manager/">AWS Secrets Manager</a> in the <code>us-east-1</code> region. Choose <strong>Other type of secret</strong> and input the Hashnode webhook secret value as <strong>Plaintext</strong> and save it. Call your secret <code>hashnode/webhook-secret</code>. Note that while this secret needs to be in <code>us-east-1</code>, you can deploy the CDK stack in the next step to any AWS region.</p>
</li>
<li><p>Clone the HashBridge <a target="_blank" href="https://github.com/dreamorosi/serverless-webhook-hashnode">GitHub repository</a>, run <code>npm ci</code> and deploy with <code>npm run cdk deploy -- --all</code>.</p>
</li>
<li><p>Copy the CloudFront URL from the CDK outputs and paste it into the <strong>URL field</strong> from step 1. Choose <strong>Create</strong> and you’re done!</p>
</li>
</ol>
<p>That's it, you will now receive events from your Hashnode blog in your AWS account. Edit the sample <a target="_blank" href="https://github.com/dreamorosi/serverless-webhook-hashnode/blob/main/src/functions/consumer/index.ts"><code>consumer</code> Lambda function</a> to react to these events.</p>
<h2 id="heading-how-hashbridge-works">How HashBridge works</h2>
<p>HashBridge uses AWS serverless services to set up a lightweight and easy-to-use integration between Hashnode, <a target="_blank" href="https://aws.amazon.com/eventbridge/">Amazon EventBridge</a> and AWS Lambda:</p>
<p><img src="https://lh7-us.googleusercontent.com/UKf6uf3dUshYiafA0P2_VH3MYU9LF-UTpjzqIM-XXBkRSPDoDX-ud4iE0Z3I2keC1Q8elZc1J2Mbb93LNWIs_cQ7tGQrJmEv5Rb_E7PzydDdmXfwroaIw446APeUdpr_c6Ba5tCT_kP-Rj-hm8Lx7w" alt /></p>
<p>In the following sections, we explain each element of this architecture in more detail.</p>
<h3 id="heading-hashnode-webhooks-and-graphql-api">Hashnode webhooks and GraphQL API</h3>
<p>On the Hashnode side, two features make HashBridge possible: <a target="_blank" href="https://support.hashnode.com/en/articles/8488809-blog-webhooks">webhooks</a> and the <a target="_blank" href="https://apidocs.hashnode.com/">GraphQL API</a>.</p>
<p>Hashnode webhooks send an HTTP request to an API endpoint of your choice whenever you publish, update or delete a blog post or static page on your Hashnode blog. They are a great way to build event-driven integrations between your blog and third-party tools. You can then use these events to trigger static site rebuilds, post to social media, or anything else you can think of.</p>
<p>Hashnode’s GraphQL API lets you CRUD all aspects of your Hashnode blog, such as posts and their metadata, comments, or static pages. With this API, you can use Hashnode as a headless CMS to build your own blog frontend (e.g. using the <a target="_blank" href="https://github.com/Hashnode/starter-kit">starter kit</a>), publish a monthly newsletter, <a target="_blank" href="https://engineering.hashnode.com/what-can-i-do-with-hashnodes-public-api#heading-3-embed-a-hashnode-feed">and more</a>.</p>
<p>HashBridge makes use of webhooks to receive events from Hashnode and then the GraphQL API to enrich these events with data and metadata such as a blog post’s content in markdown. You can customize the query and enrich the event to suit your needs.</p>
<h3 id="heading-receiving-webhook-events-with-lambda-function-urls">Receiving webhook events with Lambda function URLs</h3>
<p>When you activate webhooks, Hashnode will start sending events as requests to an HTTP endpoint. As we don’t want to operate a full-blown virtual machine for this, we can use serverless functions with AWS Lambda to receive and process the webhook requests, ensuring we only pay for what we need.</p>
<p>The easiest way to receive requests in Lambda are <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html">Lambda function URLs</a>. A function URL is a dedicated HTTP(S) endpoint for a Lambda function and basically turns a function into a simple API endpoint.</p>
<p>Hashnode webhook requests contain the <a target="_blank" href="https://apidocs.hashnode.com/#definition-Post">post ID</a> of the blog post related to the event but they do not contain the full blog content. For example:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"metadata"</span>: {
    <span class="hljs-attr">"uuid"</span>: <span class="hljs-string">"66c9430c-1152-4243-9fa1-17478f56f2bf"</span>
  },
  <span class="hljs-attr">"data"</span>: {
    <span class="hljs-attr">"publication"</span>: {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"65a965a9f60adbf4aeebde2f"</span>
    },
    <span class="hljs-attr">"post"</span>: {
      <span class="hljs-attr">"id"</span>: <span class="hljs-string">"65b7f792400f6e60e52d0bf2"</span>
    },
    <span class="hljs-attr">"eventType"</span>: <span class="hljs-string">"post_published"</span>
  }
}
</code></pre>
<p>To get the blog content, our Lambda function uses the Hashnode GraphQL API to <a target="_blank" href="https://apidocs.hashnode.com/#query-post">query the corresponding post by id</a> to get its content:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">query</span> {
  post(<span class="hljs-symbol">id:</span> <span class="hljs-string">"65b7f792400f6e60e52d0bf2"</span>) {
    content {
      markdown
    }
  }
}
</code></pre>
<p>When we started to work on the hackathon, this query was not yet possible. But sure enough, Hashnode created this new endpoint <em>within just a couple of hours</em> after hearing our feedback. That’s the new standard for customer obsession!</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://twitter.com/dreamorosi/status/1749855626354643292">https://twitter.com/dreamorosi/status/1749855626354643292</a></div>
<p> </p>
<p>Finally, we publish the webhook event enriched with the post content to an <a target="_blank" href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus.html">EventBridge event bus</a> from where we can then flexibly consume events inside and outside of AWS.</p>
<p>This allows us to receive the event once and then use it many times in multiple places. By consuming the event from the event bus, we are also decoupling the webhook verification and enrichment logic from the consuming services.</p>
<h3 id="heading-verifying-webhook-events-with-lambdaedge">Verifying webhook events with Lambda@Edge</h3>
<p>Many Lambda-based webhook solutions stop at this point and just leave the Lambda function URL exposed to the internet. However, as you can see in the architecture diagram, we decided to go a step further to improve the flexibility and security of our implementation.</p>
<p>The main reason we consider this necessary is that Lambda function URLs have <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html">limited options when it comes to authentication</a> and security today: they only support IAM authentication, don’t allow us to restrict request headers and HTTP methods, or to set up a WAF, caching, or DDoS protection. We also can’t set up a custom domain, so if we want to replace the function later we have to update the Hashnode webhook URL as well.</p>
<p>To improve on these points, we set up an <a target="_blank" href="https://aws.amazon.com/cloudfront/">Amazon CloudFront</a> distribution and define the function URL as its origin. CloudFront is a CDN which means that we directly get some benefits out of the box: we make use of the AWS global network of edge locations, get <a target="_blank" href="https://aws.amazon.com/shield/">DDoS protection</a> and a stable URL endpoint (and could even set up a custom domain).</p>
<p>Even better, we can now use <a target="_blank" href="https://aws.amazon.com/lambda/edge/">Lambda@Edge</a>, a serverless edge computing service, to inspect and manipulate each request that arrives at our distribution before it reaches our Lambda function URL. This way, we can verify webhook requests from Hashnode close to where they originate and before they reach our Lambda function URL.</p>
<h3 id="heading-verifying-webhook-requests-with-lambdaedge">Verifying webhook requests with Lambda@Edge</h3>
<p>In the Lambda@Edge function, called <em>Auth handler</em> in our case, we want to verify that the incoming events are actually coming from Hashnode. To make this possible, we can use the <code>x-hashnode-signature</code> header which Hashnode sends with each webhook request. It consists of a timestamp and the actual signature:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"user-agent"</span>: <span class="hljs-string">"HashnodeWebhooks/1.0 (https://hashnode.com/)"</span>,
  <span class="hljs-attr">"content-type"</span>: <span class="hljs-string">"application/json"</span>,
  <span class="hljs-attr">"content-length"</span>: <span class="hljs-string">"187"</span>,
  <span class="hljs-attr">"accept-encoding"</span>: <span class="hljs-string">"gzip, deflate, br"</span>,
  <span class="hljs-attr">"x-hashnode-signature"</span>: <span class="hljs-string">"t=1706555289174,v1=e2971dae21c3bd9bb1ca16c842c84d3032cb0b8fd4e1e4d6dda7ff959e1243e8"</span>
}
</code></pre>
<p>If we are in possession of the webhook secret, which is displayed in the Hashnode dashboard when creating the webhook, we can cryptographically verify the validity of an event’s signature and ensure it’s coming from Hashnode. Thanks to the timestamp, we are also protected from replay attacks where an attacker reuses a signature. Take a look at <a target="_blank" href="https://support.hashnode.com/en/articles/8488809-blog-webhooks#h_ca60049898">Hashnode’s webhook documentation</a> to see details of how this works.</p>
<p>We securely store the Hashnode webhook secret in AWS Secrets Manager and access it in our Lambda@Edge function.</p>
<p>Besides validating that requests are actually coming from Hashnode, the Lambda@Edge function also enables us to turn on IAM authentication on the Lambda function URL so that it only accepts requests from callers with appropriate IAM permissions.</p>
<p>We achieve this by configuring our Lambda@Edge with the appropriate permissions and then having it sign the verified requests that we want to forward to the function URL with <a target="_blank" href="https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html">SigV4</a>.</p>
<h3 id="heading-ensuring-idempotency">Ensuring idempotency</h3>
<p>To make the integration more resilient against data inconsistencies, we also set up <a target="_blank" href="https://en.wikipedia.org/wiki/Idempotence">idempotency</a> controls. Let’s imagine that you receive two identical events for the same blog post being published from the Hashnode webhook due to an error. Depending on what you built on top of Hashnode, this could mean two emails going out to customers instead of one, or two processes being launched in downstream systems.</p>
<p>This is where the concept of idempotency comes into play. An operation is idempotent if it can be applied multiple times without changing the result beyond the initial execution. In our case, we want to make sure that even if the same Hashnode event arrives multiple times, for example for a new blog post being published, only one event is published on the EventBridge event bus.</p>
<p>As our API handler Lambda function is written in TypeScript, we can simply use the idempotency utility included in the <a target="_blank" href="https://docs.powertools.aws.dev/lambda/typescript/latest/">Powertools for AWS Lambda (TypeScript)</a> to prevent the function from executing more than once on the same event payload. We use a serverless <a target="_blank" href="https://aws.amazon.com/dynamodb/">Amazon DynamoDB</a> table as a lightweight and performant idempotency persistence layer.</p>
<h3 id="heading-building-your-first-serverless-function-based-on-hashbridge">Building your first serverless function based on HashBridge</h3>
<p>To write your first serverless function that makes use of HashBridge, you can use the <a target="_blank" href="https://github.com/dreamorosi/serverless-webhook-hashnode/blob/main/src/functions/consumer/index.ts">example function</a> included in the HashBridge GitHub repository.</p>
<p>We set up this minimal example function with the Node.js runtime and TypeScript but you can adapt the CDK template to use any other <a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html">Lambda runtime</a> as well:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { EventBridgeEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">"aws-lambda"</span>;
<span class="hljs-keyword">import</span> { Logger } <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-lambda-powertools/logger"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { EventType, PostEvent } <span class="hljs-keyword">from</span> <span class="hljs-string">"./types.js"</span>;

<span class="hljs-keyword">const</span> logger = <span class="hljs-keyword">new</span> Logger({ logLevel: <span class="hljs-string">"DEBUG"</span> });

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-function">(<span class="hljs-params">event: EventBridgeEvent&lt;EventType, PostEvent&gt;</span>) =&gt;</span> {
    <span class="hljs-keyword">const</span> eventType = event[<span class="hljs-string">"detail-type"</span>];
    logger.debug(<span class="hljs-string">"Received event"</span>, { eventType, event });
};
</code></pre>
<p>This function is subscribed to the EventBridge event bus and is triggered each time a webhook event is received from Hashnode.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>HashBridge makes it easy to extend your Hashnode blog with event-driven serverless functions. We had a blast building HashBridge over the last two weeks and we're excited to see what you are going to build on top of it.</p>
<p>Reach out in the comments or <a target="_blank" href="https://github.com/dreamorosi/serverless-webhook-hashnode">via GitHub</a> issue with any questions and feedback.</p>
<p>A big thank you to the Hashnode team for this Hackathon challenge and for answering our questions throughout the process.</p>
]]></content:encoded></item></channel></rss>