# Codehooks.io — The Agent-Native Backend Platform
> Codehooks.io is the agent-native backend platform. A complete, isolated backend — API routes, NoSQL database, key-value store, worker queues, cron jobs, and static asset hosting — that deploys in under 5 seconds from a single CLI command. CLI-first: any tool that can run shell commands (Claude Code, Cursor, Codex, Cline) can autonomously create, deploy, verify, and iterate on a production backend. Flat-rate pricing: Free/$19/$99 per month with unlimited compute on paid plans. Based in Norway, EU. Sibling product Snill.ai (https://snill.ai) — from the same team — is an AI no-code app builder for business users that emits outbound webhooks and exposes a REST API, making it a natural pair for Codehooks projects; waitlist open at https://snill.ai/#waitlist. Use 'llms-full.txt' for complete API documentation and code examples.
This file contains all documentation content in a single document following the llmtxt.org standard.
## Overview
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
import myImageUrl from './images/quickstart/collection-with-data.webp';
import CodeBlock from '@theme/CodeBlock'
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import Link from '@docusaurus/Link';
import Badge from '@mui/material/Badge';
import {Button, Grid} from '@mui/material';
import Studio1Png from './images/studio/studio1.webp';
import Studio2Png from './images/studio/studio2.webp';
import Studio3Png from './images/studio/studio3.webp';
import Studio4Png from './images/studio/json-schema.webp';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import LayersIcon from '@mui/icons-material/Layers';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import Admonition from '@theme/Admonition';
import {
CogIcon,
CheckCircleIcon,
CheckIcon,
DocumentCheckIcon,
DocumentTextIcon,
RocketLaunchIcon,
CloudArrowUpIcon,
MagnifyingGlassPlusIcon,
LockClosedIcon,
PlayIcon,
CircleStackIcon,
DocumentPlusIcon,
KeyIcon,
ClockIcon,
RectangleStackIcon,
ChartBarSquareIcon,
LinkIcon,
QueueListIcon,
SparklesIcon
} from '@heroicons/react/24/outline';
export const NumberIcon = ({ index = -1, style = {}}) => (
{index}
);
export const codex0 = `import {app} from 'codehooks-js'
// Use Crudlify to create a REST API for any database collection
app.crudlify()
// bind to serverless runtime
export default app.init();
`;
export const codex1 = `const URL = 'https://myproject-fafb.api.codehooks.io/dev/orders';
response = await fetch(URL+'?status=OPEN', /* options */)
const result = await response.json()
`;
export const codex2 = `import {app, datastore} from 'codehooks-js'
// REST API route
app.get('/sales', async (req, res) => {
// highlight-next-line
const conn = await datastore.open();
const query = { status: "OPEN" };
const options = { sort: { date: 1 } }
conn.getMany('orders', query, options).json(res); // stream JSON to client
})
// bind to serverless runtime
export default app.init();
`;
export const codex3 = `import {app, datastore} from 'codehooks-js'
// REST API route
app.post('/cacheit', async (req, res) => {
const conn = await datastore.open();
const opt = {ttl: 60*1000};
// highlight-next-line
await conn.set('my_cache_key', '1 min value', opt);
res.send('Look dad, I have my own cache now!');
})
// bind to serverless runtime
export default app.init();
`;
export const codex4 = `import {app} from 'codehooks-js'
// serve a direcory of Blob storage files
// highlight-next-line
app.storage({route:"/documents", directory: "/myblobs"})
// CRUD REST API
app.crudlify()
// bind to serverless runtime
export default app.init();`;
export const codex5 = `import app from 'codehooks-js'
// Trigger a function with a CRON expression
// highlight-next-line
app.job('*/5 * * * *', (_, job) => {
console.log('Hello every 5 minutes')
// Do stuff ...
job.end()
});
export default app.init();`;
export const codex6 = `import { app, Datastore } from 'codehooks-js'\n
// register a Queue task worker
// highlight-next-line
app.worker('ticketToPDF', (req, res) => {
const { email, ticketid } = req.body.payload;
//TODO: implement code to fetch ticket data, produce PDF
// and send email with attachment using Mailgun, Sendgrid or Amazon SES
// ...and implement error handling
res.end(); // done processing queue item
})\n
// REST API
app.post('/createticket', async (req, res) => {
const { email, ticketid } = req.body;
const conn = await Datastore.open();
// highlight-next-line
await conn.enqueue("ticketToPDF", { email, ticketid });
res.status(201).json({"message": \`Check email $\{email\}\`});
})\n
export default app.init();
`;
export const codex7 = `import { app, Datastore, aggregation } from 'codehooks-js'
... // collapsed code
const spec = {
$group: {
$field: "month",
$max: ["sales", "winback"],
$sum: "sales"
}
}
const db = await datastore.open();
const dbstream = db.getMany('salesData');
// highlight-next-line
const result = await aggregation(dbstream, spec)
... // collapsed code
`;
export const codex8 = `{
"oct": {
"sales": {
"max": 1477.39,
"sum": 1234.05
},
"winback": {
"max": 22.0
}
},
"nov": {
"sales": {
"max": 2357.00,
"sum": 5432.00
},
"winback": {
"max": 91.0
}
},
}`;
export const codex9 = `import {app} from 'codehooks-js'
// serve /dist for react frontend
app.static({
route: '/',
directory: '/dist',
default: 'index.html',
notFound: '/index.html'
}, cacheFunction)
// Optionally define cache function for static assets
function cacheFunction(req, res, next) {
const ONE_DAY = 24 \* 60 \* 60 \* 1000; // 24 hours in milliseconds
res.set('Cache-Control', \`public, max-age=\${ONE_DAY}, s-maxage=\${ONE_DAY}\`)
res.set('Expires', new Date(Date.now() + ONE_DAY).toUTCString())
res.removeHeader('Pragma');
next()
}
// CRUD REST API (if you need it)
app.crudlify({}, {prefix: '/api'})
// bind to serverless runtime
export default app.init();
`;
export const codex10 = `export default {
build: {
outDir: '../backend-src/dist', // Points to your codehooks source folder
},
};`;
export const webhookStripe = `import {app} from 'codehooks-js'
import Stripe from 'stripe';
app.post('/webhooks/stripe', async (req, res) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
} catch (err) {
console.log('Webhook signature verification failed:', err.message);
return res.status(400).send('Webhook Error: ' + err.message);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('PaymentIntent was successful!', paymentIntent.id);
break;
case 'customer.subscription.created':
const subscription = event.data.object;
console.log('New subscription created:', subscription.id);
break;
default:
console.log('Unhandled event type', event.type);
}
res.json({received: true});
});
export default app.init();
`;
export const SmallFeature = ({
icon,
link = '#',
title = 'No title',
content =
);
};
## The agent-native backend platform
Codehooks.io is the **agent-native backend platform**. A complete, isolated backend — API routes, NoSQL database, key-value store, worker queues, cron jobs, and static asset hosting — that deploys in under 5 seconds from a single CLI command. Everything is built in. Nothing needs to be wired together.
The entire lifecycle is CLI-native:
- Create a project (`coho create`)
- Deploy code and assets (`coho deploy`)
- Inspect logs in real time (`coho log -f`)
- Query the database directly (`coho query`)
- Manage secrets, schemas, settings — all from the terminal
This means any tool that can run shell commands — Claude Code, Cursor, Codex, Cline, or a custom agent — can autonomously create, deploy, verify, and iterate on a production backend without human intervention. For the developer, the job becomes: specify what you want, review what the agent built, and verify it works.
Create projects using the CLI or the web-based [Studio](/docs/studio). A Codehooks.io project contains one or more isolated Spaces — complete self-contained environments with their own database, settings, and code.
The **[`codehooks-js`](https://www.npmjs.com/package/codehooks-js)** library is the foundation for all Codehooks development, offering 8 specialized APIs for building APIs, webhooks, event-driven architectures, and workflow automation. Explore each capability below to see how Codehooks makes development effortless.
:::tip Start Building in Minutes
Get up to speed fast with these essential resources:
- 🤖 [AI Agent Setup](/docs/ai-agent-setup) - Set up Claude Code, Cursor, Codex, or any AI agent to build on Codehooks
- 📋 [API Cheat Sheet](/docs/apicheatsheet) - All essential APIs on one page
- 🎥 [7-minute quick introduction](https://www.youtube.com/watch?v=3LGRhLU5a5A) - Join us building a complete CRUD API from scratch (it's easy)
:::
:::tip Codehooks CLI tool
To deploy code (and much more), you use the Codehooks CLI tool:
`npm install codehooks -g`
Code editing and deployment can also be done from the web based UI.
:::
## API and Webhook Development
}
title="Handle webhooks from any service"
content={
Codehooks makes it easy to receive and process webhooks from popular
services like Stripe, GitHub, Shopify, and more. The{' '}
req.rawBody property provides access to the raw request
body, which is essential for webhook signature verification.
The example below shows a Stripe webhook handler that verifies the webhook
signature using req.rawBody and processes different event types:
{webhookStripe}
This same pattern works for any webhook provider that requires signature
verification, including:
Stripe - Payment events and subscription updates
GitHub - Repository events, pull requests, and
CI/CD triggers
Shopify - Order notifications and inventory updates
Twilio - SMS and voice event notifications
And many more...
}
nextsteps={[
Codehooks webhook templates,
Stripe webhooks example,
Custom REST API routes,
Process webhooks asynchronously with Queues
,
Chain webhook events into Workflows,
]}
/>
}
title="Get a flying start with an automatic CRUD REST API"
content={
Consider a Codehooks project, for example myproject-fafb.
Under the hood, Codehooks deploys an automatic REST API which is made up
of the following really short - but complete application code:
{codex0}
It's just 3 lines of code 🔥, and it delivers a fully secure CRUD REST
API with a NoSQL database for JSON queries and updates. Client apps can
instantly connect to your data API, for example from a JavaScript app
like the example code snippet shown below:
{codex1}
}
nextsteps={[
Database REST API detailed docs,
Data validation and schema support
,
Develop custom REST APIs,
Auto-generate OpenAPI documentation,
Cheat Sheet for all the APIs,
]}
/>
Generate interactive Swagger UI documentation directly from your code.
Define schemas with Zod, Yup, or JSON Schema and Codehooks automatically
creates OpenAPI 3.0 specs. No manual YAML writing required.
}
title="Easy database API inspired by MongoDB"
content={
Sometimes a standard CRUD REST API isn't enough. With custom JavaScript
functions you can create logic that integrates directly to the NoSQL
datastore. No need to think about drivers, versions, protocol or
security, the database is just there, available as an intrinsic object
in every serverless function.
{codex2}
}
nextsteps={[
Getting started,
Database API,
NoSQL query language
,
Database indexing,
Check output logs with the CLI,
Query data with the CLI,
]}
/>
}
title="Speed up your application with a Key-Value cache"
content={
The Codehooks data engine includes a simple key-value datastore. A
key-value datastore has many use-cases, and is ideal for caching due to
the swift data access and simple, maintenance-friendly architecture. A
simple example is shown below, where a string is stored for 60 seconds
with the ttl option.
{codex3}
}
nextsteps={[
Key-Value API,
Tutorials,
]}
/>
## Background Jobs
}
title="CRON expressions + your function keeps running"
content={
Use simple CRON expressions to automate and manage the execution of
routine tasks. You can schedule a job to run at multiple, disparate
times. For example, you can schedule a task to run at 2 AM on Sundays
and 4 PM on Wednesdays with the same CRON job.
In the simple example below, the CRON expression{' '}
*/5 * * * * will trigger a JavaScript function each 5
minutes.
}
title="Scale workloads with queues and worker functions"
content={
Use persistent queues and "worker" functions to create robust,
efficient, and highly responsive back-end systems. Queues scales
processing across multiple nodes and will restore and continue working
in case of errors and system failures.
Check out the example below, where a REST API request starts a complex
task by adding it to the Queue for later processing.
Create and deploy robust, scalable workflows using queued functions and state management.
Build reliable backend systems with automatic retry, state persistence, and distributed processing.
The example below shows a simple workflow that processes customer onboarding with email validation.
{`import { app } from 'codehooks-js';
// define the workflow
const workflow = app.createWorkflow('customerOnboarding', 'Customer signup and email validation', {
START: async function (state, goto) {
state = { email: state.email, validated: false };
await sendWelcomeEmail(state.email);
goto('waitForValidation', state);
},
waitForValidation: async function (state, goto) {
if (state.validated) {
goto('complete', state);
} else {
wait({wait_message: 'Waiting for subflow to complete'});
}
},
complete: function (state, goto) {
state.status = 'completed';
goto(null, state); // workflow complete
}
});
// Start workflow from REST API
app.post('/start-onboarding', async (req, res) => {
const result = await workflow.start({ email: req.body.email });
res.send(result);
});
export default app.init();`}
}
nextsteps={[
Workflow API,
Example Workflows
]}
/>
## Blob Storage API
}
title="Store and manage files programmatically"
content={
Codehooks provides a built-in blob storage system for uploading,
storing, and serving files through your applications. Use the File API
to handle file operations programmatically in your serverless functions.
You can upload files to blob storage using the CLI command:
The example below shows how to handle file uploads, downloads, and
management through the File API in your application code:
{codex4}
}
nextsteps={[
File API,
Upload files with the CLI,
]}
/>
## Data Aggregation
}
title="Easy to use data aggregation format"
content={
Transform your live data streams into insights with just a few lines of
code. Codehooks has a simple but powerful JSON-based aggregation
expression language to specify data transformations. Groups and sums can
be nested to create both simple and advanced data outputs. The
aggregation API can process input from any JSON data-stream, for example
the query output from the NoSQL datastore. 🔥
{codex7}
The example aggreagation transformation produces JSON output similar to
the example below.
{codex8}
}
nextsteps={[Aggregation API]}
/>
## Frontend Hosting Setup
}
title="Host your frontend applications alongside your API"
content={
There are several benefits to hosting the frontend along with the backend API:
No CORS issues - Frontend and API share the same domain
Simplified deployment - Single command deploys both frontend and backend
Cost effective - No need for separate hosting services
Codehooks can serve your frontend applications as static assets, making
it easy to deploy full-stack SPA and PWA applications from a single codebase. All you need to do
is to include the HTML and CSS files in the source code and
then use the app.static file handler to serve them along with the backend code.
This hosting option is perfect if you don't need the high scalability offered by CDN-hosted frontends.
Important: Codehooks only serves static assets, not server-side
rendered content. For frameworks like Next.js, Nuxt and SvelteKit, you must configure
them for static generation/export (SSG).
Simply copy your framework's build or dist folder into your Codehooks
project source and configure the static handler to serve it. When you run codehooks deploy, both your API and your frontend will be deployed:
{codex9}
Note: The default and{' '}
notFound settings are crucial for client-side routing. They
ensure that all routes (like /about or{' '}
/users/123) serve the index.html file,
allowing your frontend router to handle navigation instead of the server
returning 404 errors.
This setup works seamlessly with popular frameworks configured for
static builds:
Pure HTML/CSS: Just keep the source with backend code from the start (in the dist/ folder in this example)
React: Copy your build/ folder (after{' '}
npm run build)
Vue: Copy your dist/ folder (after{' '}
npm run build)
Next.js: Configure static export (
next export), then copy your out/ folder
Nuxt: Configure static generation (
nuxt generate), then copy your dist/{' '}
folder
Svelte: Copy your build/ folder (after{' '}
npm run build)
SvelteKit: Configure static adapter, then copy your{' '}
build/ folder
Angular: Copy your dist/ folder (after{' '}
ng build)
Astro: Copy your dist/ folder (after{' '}
npm run build)
Gatsby: Copy your public/ folder
(after gatsby build)
We recommend automating this step by setting your frontend framework's output directory to point directly to your backend source folder.
This eliminates the manual copy step in your deployment workflow.
Example with Vite (in vite.config.js):
{codex10}
The example shown here is without any authentication. For most useful applications you would need to set up your frontend to use an authentication framework (Clerk, Auth0, Stack-Auth, codehooks-auth).
}
nextsteps={[
Example of static site hosting with Codehooks, React and codehooks-auth
,
Static file serving documentation
,
Authentication
,
]}
/>
```
### HTML client JavaScript
```js title="/public/chat.js"
const API_TOKEN = '0a2249d4-5f10-489c-8229-1f060ad1e0f6';
let listenerID = null;
document.addEventListener('DOMContentLoaded', function () {
const aliasInput = document.getElementById('aliasInput');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
// Function to send a message to the server
function sendMessage(message) {
fetch('/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-apikey': API_TOKEN,
},
body: JSON.stringify({
message: message,
listenerID,
alias: aliasInput.value.trim(),
}),
})
.then((response) => response.json())
.then((data) => {
console.log('Message sent:', data);
messageInput.value = ''; // Clear input after sending
messageInput.focus(); // Keep focus on input field
})
.catch((error) => {
console.error('Error:', error);
});
}
sendButton.addEventListener('click', function () {
if (messageInput.value) {
sendMessage(messageInput.value);
}
});
messageInput.addEventListener('keydown', function (event) {
// Check if Enter key is pressed without Shift key
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); // Prevent default to avoid line breaks or form submission
if (messageInput.value) {
sendMessage(messageInput.value);
}
}
});
// init real-time connection
startListener();
});
function addMessage(message) {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.textContent = message;
messagesDiv.appendChild(messageElement); // Adds the message at the bottom
// Auto-scroll to the bottom
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// connect to realtime SSE
async function startListener() {
// setup the real time stuff
const statusIndicator = document.getElementById('statusIndicator');
const interests = {}; // everything
var requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-apikey': API_TOKEN,
},
body: JSON.stringify(interests),
};
// get a real time listenerID
console.log('Connect request', requestOptions);
const response = await fetch('/connect', requestOptions);
const result = await response.json();
listenerID = result.listenerID;
console.log('GOT clientID', result);
// connect to reatime channel
let eventSource = new EventSourcePolyfill(`/chat/${result.listenerID}`, {
headers: {
'x-apikey': API_TOKEN,
},
});
statusIndicator.textContent = 'Connected';
statusIndicator.style.color = 'green';
// incoming message event
eventSource.onmessage = function (event) {
console.log('Event', event.data);
const result = JSON.parse(event.data);
addMessage(result.message);
};
// here we go
eventSource.onopen = function (event) {
// Connection is open
statusIndicator.textContent = 'Live data ready';
statusIndicator.style.color = 'green';
};
// oops, reconnecting if possible
eventSource.onerror = function (event) {
console.log('Error', event);
// An error occurred or the connection was closed
if (eventSource.readyState == EventSource.CLOSED) {
console.log('Connection was closed.');
}
statusIndicator.textContent = 'No connection';
statusIndicator.style.color = 'red';
};
return result;
}
```
### Codehooks.io Server
See install and deploy docs at [Github](https://github.com/RestDB/codehooks-io-examples/tree/main/realtime-chat).
```js title="index.js"
// Codehooks.io - Chat example
import { app, realtime } from 'codehooks-js';
// create a real-time channel
realtime.createChannel('/chat');
// client connects for a new real-time listener ID
app.post('/connect', async (req, res) => {
const listenerData = await realtime.createListener('/chat');
// return listener ID to client
res.json({ listenerID: listenerData._id });
});
// client post a new message
app.post('/messages', async (req, res) => {
console.log('Message in', req.body);
const { message, listenerID, alias } = req.body;
const data = await realtime.publishEvent('/chat', {
message: `${alias || 'Anonymous'}: ${message}`,
});
res.end(data);
});
// annoying message from a cron job every second minute ;)
app.job('*/2 * * * *', async (_, job) => {
const message = `Hello from cron job at ${new Date().toISOString()}`;
const data = await realtime.publishEvent('/chat', {
message: `cron: ${message}`,
});
job.end();
});
app.static({ route: '/public', directory: '/public' });
// bind to serverless runtime
export default app.init();
```
---
## REST API Routing
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
App routes lets you create secure public [(REST) API](https://en.wikipedia.org/wiki/Representational_state_transfer) endpoints to serverless JavaScript functions in your application.
:::info REST API Routing Fundamentals
REST API routes, also known as endpoints, are specific URLs that clients use to interact with a RESTful API using standard HTTP methods:
**Key Concepts:**
- **Resource-centric**: Routes represent resources (e.g., `/users`, `/products`, `/orders`) rather than actions
- **HTTP Methods**: [GET, POST, PUT, PATCH, DELETE](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) for different operations
- **Hierarchical Structure**: Routes follow patterns like `/users/{user_id}/orders` to represent relationships
- **Stateless**: Each request contains all necessary information for the server to process it
- **Consistent & Predictable**: Well-designed routes make APIs easier to understand and use
**Example REST API Routes:**
```
GET /api/products // Get all products
GET /api/products/{id} // Get specific product
POST /api/products // Create new product
PUT /api/products/{id} // Update existing product
DELETE /api/products/{id} // Delete a product
```
**Learn more:** [REST Architecture](https://en.wikipedia.org/wiki/Representational_state_transfer) • [HTTP Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) • [HTTP Status Codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) • [API Design Best Practices](https://restfulapi.net/)
:::
## Example REST API route
The example app routes below creates two REST API endpoint routes that triggers a serverless function. This simple example function `echo` does nothing useful, other than echoing back the request data to the client as JSON.
```js title="index.js"
import app from 'codehooks-js';
// Codehooks API routes
app.post('/myroute', echo);
app.get('/myroute/:var1/:var2', echo);
// Serverless function
function echo(req, res) {
res.json(req);
}
// bind to Codehooks runtime
export default app.init();
```
After deploying the REST API with the [`coho deploy` CLI command](/docs/cli#deploy) we can run a quick test from the command line using `curl`.
:::tip
You can use [Postman](https://www.postman.com/api-platform/api-testing/) together with Codehooks for testing your APIs.
:::
```bash title="Example command line"
curl https://myproject-abcd.api.codehooks.io/dev/myroute/123/321
-H 'x-apikey: 6e111ea6-6b3a-412d-8f6b-061c816c67c9'
```
The example shows that our function echoes back the input as a structured (and very handy) JSON object.
```js title="Example output"
{
"hostname": "myproject-abcd.api.codehooks.io",
"headers": {
"host": "myproject-abcd.api.codehooks.io",
"user-agent": "curl/7.77.0",
"accept": "*/*",
"x-apikey": "6e111ea6-6b3a-412d-8f6b-061c816c67c8"
},
"query": {},
"path": "/dev/myroute/123/321",
"apiPath": "/myroute/123/321",
"originalUrl": "/dev/myroute/123/321",
"params": {
"var1": "123",
"var2": "321"
},
"body": {},
"method": "GET"
}
```
After deployment the specified routes will be available as HTTPS endpoints.
## REST API endpoint URL spec
A REST API app route is available at an HTTPS endpoint based on the HTTP verb, tenant ID, datastore name and API route specification.
```shell
┌─── Project Id, e.g. booking-2b39
│ ┌─── Project space name (environment) , e.g. prod
│ │ ┌─── API route, e.g. /cars/:model/:make
│ │ │ ┌─── Parameters, e.g. color=black
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
https://.api.codehooks.io//[?QUERY]
Example URL: /cars/:model/:make
https://booking-2b39.api.codehooks.io/prod/cars/Tesla/model-x?color=black&campaign=A
```
## REST API route matching
Any request agaist a space REST API, e.g. `/dev/*`, are matched against the deployed and declared app route functions.
### String path
Route declaration must match exact to trigger function.
```js
// matches: https://apollo-ff00.api.codehooks.io/dev/admin
app.get('/admin', myFunc);
```
### Path parameters
Route declaration can match variable named segments of route to trigger function.
```js
// matches: https://apollo-ff00.api.codehooks.io/dev/admin/abc-123
app.get('/admin/:messageId', myFunc);
```
### Wildcard
Route declaration can match wildcard segments of route to trigger function.
```js
// matches: https://apollo-ff00.api.codehooks.io/dev/user/abc-123/inbox
// matches: https://apollo-ff00.api.codehooks.io/dev/user/turbo-xyz/inbox
app.get('/user/*/inbox', myFunc);
```
### Regular expression
Route declaration matches a regular expression to trigger function.
```js
// matches: https://apollo-ff00.api.codehooks.io/dev/foo/bar
// matches: https://apollo-ff00.api.codehooks.io/dev/user/bar/foo/y/x
app.get(/^\/(foo|bar)/, myFunc);
```
## The REST API request object
The request object contains these following properties for a client (HTTPS) request.
### Overview
node.value.startsWith('request.'))}/>
### request.headers
Get the HTTP header values as key-value pairs.
```js
console.log(req.headers['user-agent']); // -> PostmanRuntime/7.29.0
```
### request.query
Get the URL query parameters as key-value pairs.
```js
// route: /product
// url : /dev/product?price=10
console.log(req.query.price); // -> 10
```
### request.params
Get the URL route variables as key-value pairs.
```js
// route: /order/:orderID/:type
// url : /dev/order/12345/small
console.log(req.params.orderID, req.params.type); // -> 12345, small
```
### request.body
Get the parsed request body. Codehooks automatically parses the request body based on the `Content-Type` header:
- **`application/json`** - Parsed as JSON object
- **`application/x-www-form-urlencoded`** - Parsed as key-value object (used by HTML forms, Slack slash commands, etc.)
```js
// JSON request: {"foo": "bar"}
console.log(req.body); // -> {"foo": "bar"}
// Form-urlencoded request: name=John&age=30
console.log(req.body); // -> {"name": "John", "age": "30"}
```
:::tip Webhook Integrations
Some webhook providers send `application/x-www-form-urlencoded` instead of JSON. For example, Slack slash commands POST form data with fields like `command`, `text`, `user_id`, etc. Codehooks parses these automatically into `req.body`.
:::
### request.rawBody
Get the raw, unparsed request body as a string. This is particularly useful for webhook integrations that require signature verification using the original payload.
:::tip Webhook Signature Verification
Many webhook providers (like Stripe, GitHub, Shopify) require you to verify the webhook signature using the raw request body. Use `request.rawBody` instead of `request.body` for this purpose.
:::
```js
// Example: Verify webhook signature
const signature = req.headers['x-webhook-signature'];
const rawPayload = req.rawBody;
const expectedSignature = computeHMAC(rawPayload, webhookSecret);
if (signature === expectedSignature) {
// Signature is valid, process the webhook
const data = req.body; // Use parsed body for processing
console.log(data);
}
```
### request.path
Get the URL full path.
```js
// route: /order/:orderID
// url : /dev/order/12345
console.log(req.path); // -> /dev/order/12345
```
### request.apipath
Get the URL api path.
```js
// route: /order/:orderID
// url : /dev/order/12345
console.log(req.apipath); // -> /order/12345
```
### request.originalUrl
Get the URL full path and query parameter string.
```js
// route: /order/:orderID
// url : /dev/product?size=small
console.log(req.originalUrl); // -> /dev/product?size=small
```
### request.method
Get the HTTP request verb.
```js
// url: /dev/product
console.log(req.method); // -> GET
```
### request.hostname
Get the project URL domain name.
```js
// url: https://myproject-ff00.api.codehooks.io/dev/product
console.log(req.hostname); // -> myproject-ff00.api.codehooks.io
```
### request.pipe(Stream)
Process a request data stream by piping it to another stream. Useful to process other data formats than JSON, e.g. binary or raw data content types - `content-type: application/octet-stream`.
```js
const mystream = new PassThrough();
// process mystream
req.pipe(mystream);
```
### request.on(event, data)
Process a request data stream by listening to the request data emitter. Useful to process other data formats than JSON, e.g. binary or raw data content types - `content-type: application/octet-stream`.
```js
req.on('data', (buf) => {
console.log(buf.length); // process buf data
});
req.on('end', (bytes) => {
console.log('Got binary data', bytes);
res.end('Got binary ' + bytes);
});
```
## The REST API response object
The response object sends data back to the client and finalizes the request.
### Overview
node.value.startsWith('response.'))}/>
### response.set(header, value)
Set a response header value.
**Parameters**
- `header` **string**, [read more about HTTP header values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
- `value` **string**
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.set('Content-Type', 'text/html; charset=UTF-8'); // set a response header
res.end('
Hello world!
'); // send html back to client
}
```
### response.headers(Object)
Set multiple response header values.
**Parameters**
- `Object` **JSON**, [read more about HTTP header values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.headers({
'Content-Type': 'text/html; charset=UTF-8',
'X-myheader': '123456890',
});
res.send('
Hello world!
'); // send html back to client
}
```
### response.status(HttpStatusCode)
Return a HTTP response status code for a client request.
**Parameters**
- `HttpStatusCode` **integer**, [read more about HTTP status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.status(401); // Unauthorized
res.end();
}
```
### response.json(Object)
Return a JSON response for a client request.
:::caution
Ends the client request and terminates the Codehook execution.
:::
**Parameters**
- `Object` **JSON**
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.json({ message: 'Hello' });
}
```
### response.write(data, [encoding])
Stream data back to the client as text or binary data buffers.
NB! `content-type` must be set before any `write` operations.
**Parameters**
- `data` **string** or any **data** that the Content-Type allows
- `encoding` **optional string** use `res.write(mybuf, 'buffer')` to write binary data back to client
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.set('content-type', 'text/plain');
res.write('Hello');
res.write('world!');
res.write('\n');
res.end();
}
```
### response.send(data)
End response for a client request.
:::caution
Ends client request and terminates the Codehook execution immediately.
If not called, the Codehook will not return and finally time out.
:::
**Parameters**
- `data` **string**
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.send('Thanks for visiting!');
res.end();
}
```
### response.redirect(statusCode, URL)
Redirect a client request.
**Parameters**
- `statusCode` **number** 301 or 302
- `URL` **string** A valid relative or absolute URL
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.redirect(301, 'https://example.com');
}
```
### response.end()
End response for a client request.
:::caution
Ends client request and terminates the Codehook execution immediately.
If not called, the Codehook will not return and finally time out.
:::
**Parameters**
- none
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.send('Thanks for visiting!');
res.end();
}
```
### response.writable
Get the Writable object that can be used in node.js Stream operations.
```js
mystream.pipe(res.writable);
```
## Middleware functions
You can create middleware logic by chaining functions in an array or globally with the `app.use` function.
Each function have a `next` method parameter that is used to "chain" the next function or to terminate the chain. The example below shows how a middleware function is called before the main function. The middelware function typically check some state or logic before proceeding to the next.
```js title="index.js"
import app from 'codehooks-js';
// Global middleware on all routes
app.use((req, res, next) => {
console.log("I'm global");
res.next();
});
// Global middleware on route matches
app.use('/my*', (req, res, next) => {
console.log("I'm global match for /my*");
res.next();
});
// Example route with middleware
app.post('/myroute', middleWare, myFunc);
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
// All is OK, got here finally
res.end();
}
function middleWare(req, res, next) {
let OK = true;
// do something here
if (OK) {
next();
} else {
res.status(401); // Unauthorized
res.end();
}
}
export default app.init();
```
## Wildcard routes
Wildcards lets you match all `methods`and all `routes` that are not matched by previous `routes`.
See example code below.
```js {7} title="index.js"
import app from 'codehooks-js'
app.get('/myroute': getFunc);
app.put('/myroute/:id': putFunc);
// wildcard route
app.all('/*': (req, res) => {
res.end("I catch all methods, on all routes not matched before");
}
)
export default app.init();
```
---
## Application secrets
Application secrets are stored as environment variables in your application space.
To protect your private secrets, environment variables can be encrypted and then dynamically added to your application serverless runtime.
A Secret is typically an encrypted environment variable that contains a small amount of sensitive data such as a password, a token, or a key.
## Adding secret environment variables
The following Codehooks [CLI command](/docs/cli#set-env) will create an encrypted enviroment variable that you can use in your code as `process.env.MY_SECRET_VARIABLE`:
```bash
coho set-env MY_SECRET_VARIABLE 'some secret content here' --encrypted
```
## Adding regular non-secret environment variables
Running the `set-env` command without the `--encrypted` parameter will create a plain text variable, which you can use in your code as `process.env.MY_VARIABLE`.
The follwing Codehooks CLI command will create an encrypted environment variable that you can use in your code as `process.env.VARIABLENAME`:
```bash
coho set-env MY_VARIABLE 'some regular content here'
```
## Inspecting secrets and environment variables
You can use the `coho info` CLI command to inspect your system variables. You will not be able to inspect the actual content of encrypted variables. Example output of the `info` command is shown below:
```bash
coho info
...
Spaces:
┌──────────────┬─────────┬───────────────┬──────┬───────────────────────────────────────┐
│ Name │ Tokens │ Allowed Hosts │ Jwks │ Env │
├──────────────┼─────────┼───────────────┼──────┼───────────────────────────────────────┤
│ dev (active) │ │ │ │ MY_SECRET_VARIABLE=(encrypted) │
│ │ │ │ │ MY_VARIABLE=some regular content here │
└──────────────┴─────────┴───────────────┴──────┴───────────────────────────────────────┘
...
```
## Removing environment variables
The follwing Codehooks CLI command will remove any enviroment variable:
```bash
coho remove-env MY_VARIABLE
```
:::tip
You can also set these environment variables using the account UI.
:::
---
## Workflow API - Build Reliable Stateful Workflows with durable functions
*(Note: This content contains MDX/JSX code)*
Create and deploy robust, scalable workflows using queued functions and state management. Build reliable backend systems with automatic retry, state persistence, and distributed processing.
## Overview
Codehooks workflows combines persistent queues with state management to create reliable, scalable workflows. Each step in a workflow can modify the process state and control the flow using the `goto` function, enabling complex workflows with branching, loops, and conditional execution. The system automatically handles state persistence, distributed processing, and error recovery.
### Key Features
- **State Management**
- Persistent state storage
- Atomic state updates
- Complete execution history
- State inspection and debugging
- **Queue-Based Processing**
- Queued (durable) functions
- Automatic retry on failures
- Load balancing across workers
- No lost messages or duplicate processing
- Parallel execution of workflow steps
- **Scalability & Reliability**
- Distributed processing
- Automatic failover
- State recovery
- Error handling and monitoring
### Why Use Workflows?
Workflows help you build reliable backend systems and integration logic. Using queued functions they handle system failures and network issues by automatically resuming from the last known state after crashes or connection losses. This makes them ideal for critical processes like order processing and payment flows. The system distributes work across multiple workers and processes long-running tasks asynchronously. As a developer, you can focus on implementing business logic while the framework handles state persistence, retries, and scaling.
**Perfect for Webhook Processing:** Workflows are especially powerful for webhook handlers. When you receive a webhook from Stripe, Shopify, or GitHub, you often need multi-step processing: verify signature → store event → trigger fulfillment → update inventory → send notifications → schedule follow-ups. With workflows, you can acknowledge the webhook immediately (preventing retries), then reliably process each step with automatic retries and state tracking. If any step fails, the workflow resumes from that point—ensuring no webhook event is lost or processed twice.
## A simple workflow example
Let's create a simple workflow app that demonstrates branching based on a random number. The workflow will process even and odd numbers differently.
```mermaid
graph TD
A[begin] -->|initialize| B[decide]
B -->|even| C[evenPath]
B -->|odd| D[oddPath]
C -->|complete| E[end]
D -->|complete| E
E -->|finish| F[Exit]
```
The full source code for the workflow steps is shown below.
```javascript title="index.js"
import { app } from 'codehooks-js';
// Create a workflow definition
const workflow = app.createWorkflow('simpleTask', 'Basic workflow example', {
// Step 1: Initialize the workflow
begin: async function (state, goto) {
state = {
message: 'Starting workflow',
// Add a random number to demonstrate branching
value: Math.floor(Math.random() * 10),
};
goto('decide', state);
},
// Step 2: Decide which path to take
decide: async function (state, goto) {
// Branch based on whether the value is even or odd
if (state.value % 2 === 0) {
goto('evenPath', state);
} else {
goto('oddPath', state);
}
},
// Step 3a: Handle even numbers
evenPath: async function (state, goto) {
state = {
message: 'Processing even number',
path: 'even',
processed: true,
};
goto('end', state);
},
// Step 3b: Handle odd numbers
oddPath: async function (state, goto) {
state = {
message: 'Processing odd number',
path: 'odd',
processed: true,
};
goto('end', state);
},
// Step 4: End the workflow
end: function (state, goto) {
state = {
final_message: `Workflow completed! Processed ${state.path} number: ${state.value}`,
};
goto(null, state); // workflow complete
},
});
// emitted event when a workflow completes
workflow.on('completed', (data) => {
console.log('Workflow completed:', data);
});
// REST API to start a new workflow instance
app.post('/start', async (req, res) => {
const result = await workflow.start({ foo: 'bar' });
res.json(result);
});
// export app interface to serverless execution
export default app.init();
```
You can [deploy](/docs/quickstart-cli#deploy-the-code-to-the-serverless-cloud) the workflow in the [Studio](/docs/studio) app directly or with the [CLI command `coho deploy`](/docs/cli#deploy).
See more [examples](#example-workflows) at end of this page.
## Workflow State Storage and Inspection
All workflow instances and their states are automatically persisted in a collection (default name: `workflowdata`). This collection stores the complete state of every workflow instance, making it easy to inspect and debug workflows at any time.
Each document in the collection contains:
- `_id`: Unique identifier for the workflow instance
- `nextStep`: The next step to be executed (null when workflow is complete)
- `createdAt`: Timestamp when the workflow was created
- `updatedAt`: Timestamp of the last state update
- `workflowName`: Name of the workflow definition
- `previousStep`: Name of the last executed step
- `stepCount`: Object containing execution statistics for each step
- `visits`: Number of times the step was executed
- `startTime`: When the step started execution
- `finishTime`: When the step finished execution
- `totalTime`: Total execution time in milliseconds
- `totalTime`: Total workflow execution time in milliseconds
- `...state`: Your custom state properties for the workflow
**An example state object** for a completed `simpleTask` workflow is shown below:
```js
{
"foo": "bar",
"nextStep": null,
"createdAt": "2025-06-09T15:35:41.318Z",
"workflowName": "simpleTask",
"stepCount": {
"begin": {
"visits": 1,
"startTime": "2025-06-09T15:35:41.529Z",
"totalTime": 5,
"finishTime": "2025-06-09T15:35:41.534Z"
},
"decide": {
"visits": 1,
"startTime": "2025-06-09T15:35:41.700Z",
"totalTime": 7,
"finishTime": "2025-06-09T15:35:41.707Z"
},
"evenPath": {
"visits": 1,
"startTime": "2025-06-09T15:35:41.757Z",
"totalTime": 5,
"finishTime": "2025-06-09T15:35:41.762Z"
},
"end": {
"visits": 1,
"startTime": "2025-06-09T15:35:42.578Z",
"totalTime": 8,
"finishTime": "2025-06-09T15:35:42.586Z"
}
},
"_id": "6846ff4d5a5945501a11d691",
"updatedAt": "2025-06-09T15:35:42.586Z",
"message": "Processing even number",
"value": 6,
"previousStep": "end",
"path": "even",
"processed": true,
"final_message": "Workflow completed! Processed even number: 6",
"totalTime": 1260
}
```
You can query this collection directly to inspect all workflow states:
```js
// Example REST API to get all completed instances of a workflow
app.get('/some', async (req, res) => {
// Example: Find all running instances of a workflow
const db = await Datastore.open();
const runningInstances = await db
.getMany('workflowdata', {
workflowName: 'simpleTask',
nextStep: null,
})
.toArray();
res.json(runningInstances);
});
```
Or you can query the collection for a specific workflow instance:
```js
// Example REST API to get a specific workflow instance
app.get('/one/:id', async (req, res) => {
const db = await Datastore.open();
// Example: Get state of a specific workflow instance
const instance = await db.getOne('workflowdata', {
_id: req.params.id,
});
res.json(instance);
});
```
To use a different collection name, pass a configuration object as the last argument when creating the workflow:
```js
const workflow = app.createWorkflow('myWorkflow', 'description', steps, {
collectionName: 'my_custom_collection'
});
```
Here's how the workflow data appears in Codehooks Studio for an example workflow:

The screenshot shows the state of a workflow instance that:
1. Started at 17:21:00
2. Branched trough the `even` path
3. Is completed because nextStep is `null`
4. Contains the state data with the message properties
## Core API Overview
**Methods**
The workflow API provides these core methods for managing persistent workflows:
### createWorkflow(name, description, steps, config?)
Creates a new workflow of persistent steps.
```js
const workflow = app.createWorkflow('workflow', 'description', {
START: async (state, goto) => {
// step implementation
},
});
```
**Parameters:**
- `name` (string): Unique identifier for the workflow
- `description` (string): Human-readable description of the workflow
- `steps` (WorkflowDefinition): Object containing step definitions
- `config` (object, optional): Configuration options
- `collectionName` (string): Collection name for storing workflow data (default: `'workflowdata'`)
- `queuePrefix` (string): Queue prefix for workflow jobs (default: `'workflowqueue'`)
- `timeout` (number): Global timeout in milliseconds for workflow steps (default: `30000`)
- `maxStepCount` (number): Maximum number of times a step can be executed (default: `3`)
- `workers` (number): Number of parallel workers to run per queue (default: `1`)
- `steps` (object): Step-specific configuration options
- `[stepName].timeout` (number): Timeout in milliseconds for this specific step
- `[stepName].maxRetries` (number): Maximum number of retries for this step
- `[stepName].workers` (number): Number of parallel workers for this specific step
**Returns:** Workflow instance for managing the workflow
### start(initialState)
Starts a new Workflow instance.
```js
const result = await workflow.start({ data: 'value' });
// Returns state, e.g. {_id: '682ec2b3dab9887c1da726e9', ...state}
```
**Parameters:**
- `initialState` (any): Initial state for the workflow instance
**Returns:** Promise with the workflow instance
### updateState(instanceId, state, options?)
Updates the state of a workflow instance.
```js
await workflow.updateState(
'682ec2b3dab9887c1da726e9',
{ count: 1 },
{ continue: true }
);
```
**Parameters:**
- `instanceId` (string): ID of the workflow instance
- `state` (any): New state to update with
- `options` (UpdateOptions, optional): Options for the update, `{ continue: false }` to avoid continuing the step
**Returns:** Promise with the updated state
### setState(instanceId, stateData)
Set the complete state of a workflow instance.
```js
await workflow.setState('682ec2b3dab9887c1da726e9', {
_id: '682ec2b3dab9887c1da726e9',
state: { count: 1, status: 'active' },
});
```
**Parameters:**
- `instanceId` (string): ID of the workflow instance
- `stateData` (object): Object containing `_id` and `state`
**Returns:** Promise that resolves to void
### continue(instanceId, reset?)
Continue a paused workflow instance.
```js
const result = await workflow.continue('682ec2b3dab9887c1da726e9', false);
// Returns: { instanceId: string }
```
**Parameters:**
- `instanceId` (string): ID of the workflow instance
- `reset` (boolean, optional): Whether to reset all step counts (true) or just the current step (false)
**Returns:** Promise with the queue ID for the continued step
### getWorkflowStatus(id)
Get the status of a workflow instance.
```js
const status = await workflow.getWorkflowStatus('682ec2b3dab9887c1da726e9');
```
**Parameters:**
- `id` (string): ID of the workflow instance
**Returns:** Promise with the workflow status
:::tip CLI Tool
Use the [workflow-status](/docs/cli#workflow-status) CLI command to visualize and monitor all workflow instances in real-time:
```bash
coho workflow-status --follow --active
```
:::
### getInstances(filter)
Lists workflow instances matching a filter.
```js
const instances = await workflow.getInstances({ status: 'running' });
```
**Parameters:**
- `filter` (any): Filter criteria for workflows
**Returns:** Promise with list of workflow instances
### cancelWorkflow(id)
Cancel a workflow instance.
```js
const result = await workflow.cancelWorkflow('682ec2b3dab9887c1da726e9');
```
**Parameters:**
- `id` (string): ID of the workflow instance to cancel
**Returns:** Promise with the cancellation result
### continueAllTimedOut()
Continues all timed out step instances in the workflow. This is useful for recovering workflows that have timed out due to system issues or long-running operations.
**Returns:** Promise with array of results containing instance IDs for continued workflows
**Example:**
```js title="Continue timed out workflows"
// Create workflow with 5 minute timeout
const workflow = app.createWorkflow('simpleTask', 'Basic workflow example', {
/* workflow steps */
}, {
timeout: 1000 * 60 * 5, // 5 minute timeout threshold
});
// Schedule job to continue timed out workflows every 5 minutes
app.job('*/5 * * * *', async (req, res) => {
const result = await workflow.continueAllTimedOut();
if (result.length > 0) {
console.log('Continued these dangling workflows:', result);
}
res.end();
});
```
### findTimedOutSteps(filter?)
Find all workflow instances with timed out steps.
```js
const timedOutWorkflows = await workflow.findTimedOutSteps();
// Returns array of workflows with timeout details:
// [{
// workflowId: string,
// workflowName: string,
// isTimedOut: true,
// executionTime?: number,
// runningTime?: number,
// timeout: number,
// step: string,
// startTime: string,
// finishTime?: string,
// currentTime?: string,
// timeoutSource: string
// }]
```
**Parameters:**
- `filter` (any, optional): Optional filter criteria for workflows
**Returns:** Promise with array of workflow instances that have timed out steps
### on(event, listener)
Registers an event listener.
```js
workflow.on('stepStarted', ({ workflowName, step, state, instanceId }) => {
console.log(`Step ${step} started in workflow ${workflowName}`);
});
```
**Parameters:**
- `event` (WorkflowEvent): Name of the event to listen for
- `listener` (function): Callback function to handle the event
**Returns:** workflow instance
### once(event, listener)
Register a one-time event listener.
```js
workflow.once('completed', (data) =>
console.log('Workflow completed once:', data)
);
```
**Parameters:**
- `event` (WorkflowEvent): Name of the event to listen for
- `listener` (function): Callback function to handle the event
**Returns:** workflow instance
### off(event, listener)
Remove an event listener.
```js
workflow.off('stepStarted', myListener);
```
**Parameters:**
- `event` (WorkflowEvent): Name of the event
- `listener` (function): Callback function to remove
**Returns:** workflow instance
### emit(event, data)
Emit an event.
```js
workflow.emit('customEvent', { message: 'Custom event data' });
```
**Parameters:**
- `event` (WorkflowEvent): Name of the event to emit
- `data` (WorkflowEventData): Event data
**Returns:** boolean indicating if the event was handled
### Workflow Configuration
Configure the workflow engine by passing a configuration object as the last argument to `createWorkflow`.
```js
import { app } from 'codehooks-js';
const workflow = app.createWorkflow('myWorkflow', 'My workflow description', {
// ... workflow steps ...
}, {
collectionName: 'workflows', // Set storage collection name
queuePrefix: 'workflow', // Set queue prefix name
timeout: 30000, // Global timeout in milliseconds
maxStepCount: 3, // Maximum step execution count
workers: 5, // Number of parallel workers per queue
steps: {
// Step-specific configuration
stepName: {
timeout: 3000, // Step-specific timeout
maxRetries: 3, // Step-specific retry count
workers: 2, // Step-specific worker count
},
},
});
```
The configuration object supports these options:
- `collectionName` (string, optional): Collection name for storing workflow data. Default: `'workflowdata'`
- `queuePrefix` (string, optional): Queue prefix for workflow jobs. Default: `'workflowqueue'`
- `timeout` (number, optional): Global timeout in milliseconds for workflow steps. Default: `30000`
- `maxStepCount` (number, optional): Maximum number of times a step can be executed. Default: `3`
- `workers` (number, optional): Number of parallel workers to run per queue. Default: `1`
- `steps` (object, optional): Step-specific configuration options
- `timeout` (number, optional): Timeout in milliseconds for this specific step
- `maxRetries` (number, optional): Maximum number of retries for this step
- `workers` (number, optional): Number of parallel workers for this specific step. Overrides global `workers` setting.
## Events
The workflow API emits these events:
### workflowCreated
Emitted when a new workflow is registered.
```js
workflow.on('workflowCreated', ({ name, description }) => {
console.log(`Workflow "${name}" registered: ${description}`);
});
```
**Event Data:**
- `name` (string): Workflow name
- `description` (string): Workflow description
### workflowStarted
Emitted when a new workflow instance is started.
```js
workflow.on('workflowStarted', ({ name, initialState }) => {
console.log(`Workflow "${name}" started with initial state:`, initialState);
});
```
**Event Data:**
- `name` (string): Workflow name
- `initialState` (any): Initial state data
### stepStarted
Emitted when a step begins execution.
```js
workflow.on('stepStarted', ({ workflowName, step, state, instanceId }) => {
console.log(`Step "${step}" started in workflow "${workflowName}"`);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `step` (string): Name of the step
- `state` (any): Current workflow state
- `instanceId` (string): Workflow instance ID
### stateUpdated
Emitted when a step's state is updated.
```js
workflow.on('stateUpdated', ({ workflowName, state, instanceId }) => {
console.log(`State updated in workflow "${workflowName}":`, state);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `state` (any): Updated state data
- `instanceId` (string): Workflow instance ID
### stepWaiting
Emitted when a step is waiting for input.
```js
workflow.on('stepWaiting', ({ workflowName, step, instanceId }) => {
console.log(`Step "${step}" waiting for input in workflow "${workflowName}"`);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `step` (string): Name of the step
- `instanceId` (string): Workflow instance ID
### workflowContinued
Emitted when a workflow instance is continued after waiting.
```js
workflow.on('workflowContinued', ({ workflowName, step, instanceId }) => {
console.log(`Workflow "${workflowName}" continued from step "${step}"`);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `step` (string): Name of the step
- `instanceId` (string): Workflow instance ID
### stepEnqueued
Emitted when a step function is added to queue.
```js
workflow.on('stepEnqueued', ({ workflowName, step, instanceId }) => {
console.log(`Step "${step}" enqueued for workflow "${workflowName}"`);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `step` (string): Name of the step
- `instanceId` (string): Workflow instance ID
### completed
Emitted when a workflow instance is completed.
```js
workflow.on('completed', ({ message, state }) => {
console.log('Workflow completed:', message, state);
});
```
**Event Data:**
- `message` (string): Completion message
- `state` (any): Final workflow state
### cancelled
Emitted when a workflow instance is cancelled.
```js
workflow.on('cancelled', ({ id }) => {
console.log(`Workflow instance "${id}" was cancelled`);
});
```
**Event Data:**
- `id` (string): Workflow instance ID
### error
Emitted when an error occurs.
```js
workflow.on('error', ({ error }) => {
console.error('Workflow error:', error);
});
```
**Event Data:**
- `error` (Error): Error object with details
## Step Function
Each step function receives three parameters:
### Parameters
- `state` (any): Current workflow state object that can be modified and passed to next step
- `goto` (function): Function to transition between steps, supports:
- Name of next step to transfer to: `goto('stepName', state)`
- Parallel execution of multiple steps: `goto(['p1', 'p2'], state)`.
- All parallel steps must join in the same next step, i.e. `goto('samestep', state)`.
- Process will wait for all parallel steps to finish before continuing to `samestep`.
- If name of next step is `null` the workflow is completed: `goto(null, state)`
- Optional third parameter for additional options: `goto('nextStep', state, options)`
- `wait` (function, optional): Function to wait for an external state change
- optional state update before going into a wait state, e.g. `wait()` or `wait({val: 42})`
- Call [updateState](#updatestateinstanceid-state-options) to continue workflow
### Examples
Basic step with single transition:
```js
async function step(state, goto) {
// Update state
state = { count: state.count + 1 };
// Transition to next step
goto('nextStep', state);
}
```
Step with conditional transitions:
```js
async function conditionalStep(state, goto) {
if (state.count >= 3) {
// End workflow when condition met
goto(null, state);
} else if (state.needsValidation) {
// Go to validation step
goto('validate', state);
} else {
// Continue to next step
goto('process', state);
}
}
```
Step with parallel execution:
```js
async function parallelStep(state, goto) {
// Execute multiple steps in parallel
goto(['processA', 'processB'], state);
}
// Both processA and processB must eventually call:
// goto('joinStep', state);
```
Step with a wait state:
```js {6}
async function stepWaiting(state, goto, wait) {
// goto bar or wait
if (state.foo === 'bar') {
goto('bar', state);
} else {
wait({ status: 'waiting for bar' });
}
}
```
## Error handling
Workflow errors are automatically captured and logged. You can monitor errors in several ways:
### Logging and Monitoring
- **Studio UI**: View workflow logs in real-time using the Studio interface
- **CLI Logs**: Stream logs with `coho log -f` to see workflow execution and errors
- **CLI Workflow Status**: Monitor workflow instances with `coho workflow-status --follow` ([details](/docs/cli#workflow-status))
- **CLI Queue Status**: Monitor queue health and worker activity with `coho queue-status --follow` ([details](/docs/cli#queue-status))
- **Database**: Errors are stored in the workflow instance as `lastError` property
The CLI monitoring tools are particularly useful during development and debugging:
```bash
# Monitor all active workflows in real-time
coho workflow-status --active --follow
# View detailed workflow instance status
coho workflow-status --detailed
# Monitor queue health and workers
coho queue-status --follow
# Export workflow status as JSON for analysis
coho workflow-status --format json > workflow-status.json
```
### Error Inspection
To find workflow instances with errors, use the Studio collection (`workflowdata`) query tool with these example queries:
```js
// Find all workflow instances with errors
{ lastError: { $exists: true } }
// Find all errors in a specific workflow
{ workflowName: 'simpleTask', lastError: { $exists: true } }
```
### Error Properties
The `lastError` object contains:
- `message`: Error message
- `updatedAt`: When the error occurred
### Example Error Object
```js
{
lastError: {
message: "Failed to process payment",
stack: "Error: Payment service unavailable\n at processPayment...",
step: "paymentStep",
timestamp: "2024-03-14T12:34:56.789Z"
}
}
```
### Best Practices
1. **State Management**
- Keep state objects simple and serializable
- Use atomic updates for state changes
- Validate state at each step
- Monitor state size and complexity
2. **Error Handling**
- Monitor errors in logs and database
- Use try/catch in steps that might fail
- Set appropriate timeouts
- Implement retry logic for transient failures
3. **Scalability**
- Design stateless step functions
- Use appropriate queue configurations
- Monitor worker performance
- Implement proper error recovery
4. **Monitoring**
- Track workflow completion rates
- Monitor error frequencies
- Watch queue lengths
- Set up alerts for critical failures
## Example Workflows
Here are some example workflows demonstrating common patterns and use cases. Each example includes a visualization, explanation of the steps, and complete source code.
### Minimal workflow example
This minimal example demonstrates the basic structure of a workflow with just two steps. It shows how to:
- Create a simple workflow definition
- Set and modify workflow state
- Use `goto` to control flow
- Expose a REST endpoint to start the workflow
```mermaid
flowchart TD
A[POST /start] --> |Initial state|B[start step]
B --> C[end step]
C --> |End state|D[Workflow completed]
```
The code below implements this workflow, handling the state and transitions:
```js
import { app } from 'codehooks-js';
// The workflow definition
const workflow = app.createWorkflow('minimal', 'Minimal workflow example', {
start: (state, goto) => {
goto('end', { ...state, message: 'Hello World' });
},
end: (state, goto) => goto(null, state), // null complete the workflow
});
// A REST API to start a new workflow instance
app.post('/start', async (req, res) =>
res.json(await workflow.start({ prop: 'some value' }))
);
export default app.init();
```
### Customer onboarding
Here's a visualization of the customer onboarding workflow:
```mermaid
graph TD
A[START] -->|send welcome email| B[waitForValidation]
B -->|validated: false| B
B -->|validated: true| C[complete]
C -->|goto null = finish| D[End]
style A fill:#2d3748,stroke:#4a5568,stroke-width:2px,color:#e2e8f0
style B fill:#374151,stroke:#4b5563,stroke-width:2px,color:#e2e8f0
style C fill:#1e3d2e,stroke:#2d4d3e,stroke-width:2px,color:#e2e8f0
style D fill:#22543d,stroke:#276749,stroke-width:2px,color:#e2e8f0
classDef waiting fill:#374151,stroke:#4b5563,stroke-width:2px,color:#e2e8f0
class B waiting
```
The workflow shows a process that:
1. Starts with customer email
2. Sends welcome email and enters validation state
3. Stays in validation state (no goto) until email is confirmed
4. Completes when email is validated (goto null = finish)
Here's a practical example of a customer onboarding workflow:
```javascript title="index.js"
import { app } from 'codehooks-js';
const workflow = app.createWorkflow(
'customerOnboarding',
'Customer signup and email validation',
{
START: async function (state, goto) {
// Initialize onboarding state
state = {
...state,
status: 'started',
email: state.email,
validated: false,
startedAt: new Date().toISOString(),
};
// Send welcome email
await sendWelcomeEmail(state.email);
// Move to validation step
goto('waitForValidation', state);
},
waitForValidation: async function (state, goto, wait) {
// Check if email was validated
if (state.validated) {
state.status = 'email_validated';
goto('complete', state);
} else {
// wait for validated = true
wait({ status: 'waiting_for_validation' });
}
},
complete: function (state, goto) {
// Finalize onboarding
state.status = 'completed';
state.completedAt = new Date().toISOString();
goto(null, state); // null === finish
},
}
);
// Event listeners for monitoring
workflow.on('completed', (data) => {
console.log('Onboarding completed for:', data);
});
workflow.on('stepStarted', (data) => {
console.log('Step started:', data.step, 'for customer:', data);
});
// API endpoints
app.post('/start-onboarding', async (req, res) => {
const { email } = req.body;
const result = await workflow.start({ email });
res.send(result);
});
// Endpoint to validate email (called by email webhook)
app.post('/validate-email', async (req, res) => {
const { email, token } = req.body;
// Find the onboarding instance for this email
const instances = await workflow.getInstances({
email: email,
validated: false,
});
if (instances.length > 0) {
const instance = instances[0];
// Update state with validation
const result = await workflow.updateState(
instance._id,
{
validated: true,
validatedAt: new Date().toISOString(),
validationToken: token,
},
{ continue: true } // this Step is complete
);
res.send(result);
} else {
res.status(404).send({ error: 'No pending onboarding found' });
}
});
// Helper function to send welcome email
async function sendWelcomeEmail(email) {
// Implementation of email sending
console.log('Sending welcome email to:', email);
// In a real app, you would use your email service here
}
export default app.init();
```
### Batch Example
This example demonstrates a batch process for managing long-running data operations. It's useful for scenarios like:
- Processing large datasets with multiple validation steps
- Orchestrating complex data transformations across systems
- Managing stateful operations that require error recovery
- Coordinating multi-step data synchronization tasks
- Implementing audit trails for data operations
- Handling distributed transactions with rollback capabilities
- Automating periodic data maintenance and cleanup tasks
- Building resilient ETL (Extract, Transform, Load) pipelines
The flow diagram below shows a three-step process that validates data against an external API, updates a database record, and transfers the result to an archive system. Each step includes error handling and state tracking.
```mermaid
graph TD
A[START] -->|init state| B[checkCustomerStatus]
B -->|check API| C[updateStatus]
C -->|update DB| D[transferData]
D -->|archive data| E[End]
B -->|error| F[error]
C -->|error| F
D -->|error| F
F -->|log error| E
style A fill:#2d3748,stroke:#4a5568,stroke-width:2px,color:#e2e8f0
style B fill:#1e3a3a,stroke:#2d4a4a,stroke-width:2px,color:#e2e8f0
style C fill:#1e3a3a,stroke:#2d4a4a,stroke-width:2px,color:#e2e8f0
style D fill:#1e3a3a,stroke:#2d4a4a,stroke-width:2px,color:#e2e8f0
style F fill:#4a2d2d,stroke:#5a3d3d,stroke-width:2px,color:#e2e8f0
style E fill:#22543d,stroke:#276749,stroke-width:2px,color:#e2e8f0
```
```javascript title="batch.js"
import { app } from 'codehooks-js';
const steps = app.createWorkflow(
'customerCleanup',
'Batch process customer data cleanup',
{
START: async function (state, goto) {
// Initialize batch state
state = {
...state,
customerId: state.customerId,
status: 'started',
startedAt: new Date().toISOString(),
results: {
checkStatus: null,
dataTransfer: null,
},
};
// Move to first step
goto('checkCustomerStatus', state);
},
checkCustomerStatus: async function (state, goto) {
try {
// Call external API to check customer status
const response = await fetch(
'https://api.example.com/customer-status',
{
method: 'POST',
body: JSON.stringify({ customerId: state.customerId }),
}
);
const data = await response.json();
// Update state with check results
state.results.checkStatus = {
status: data.status,
lastActive: data.lastActive,
checkedAt: new Date().toISOString(),
};
// Move to next step
goto('updateStatus', state);
} catch (error) {
state.error = error.message;
goto('error', state);
}
},
updateStatus: async function (state, goto) {
try {
// Update customer status in database
const newStatus =
state.results.checkStatus.lastActive < '2024-01-01'
? 'inactive'
: 'active';
await app.datastore.updateOne(
'customers',
{ customerId: state.customerId },
{ $set: { status: newStatus, updatedAt: new Date() } }
);
state.results.statusUpdated = {
newStatus,
updatedAt: new Date().toISOString(),
};
// Move to final step
goto('transferData', state);
} catch (error) {
state.error = error.message;
goto('error', state);
}
},
transferData: async function (state, goto) {
try {
// Transfer customer data to archive system
const response = await fetch('https://archive.example.com/transfer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerId: state.customerId,
status: state.results.statusUpdated.newStatus,
lastActive: state.results.checkStatus.lastActive,
}),
});
state.results.dataTransfer = {
success: response.ok,
transferredAt: new Date().toISOString(),
};
// Complete the process
goto(null, state);
} catch (error) {
state.error = error.message;
goto('error', state);
}
},
error: function (state, goto) {
// Log error and end process
console.error('Batch process failed:', state.error);
state.status = 'failed';
goto(null, state);
},
}
);
// API endpoint to start batch process
app.post('/cleanup-customer', async (req, res) => {
const { customerId } = req.body;
const result = await steps.start({ customerId });
res.send(result);
});
// Helper endpoint to check batch status
app.get('/cleanup-status/:customerId', async (req, res) => {
const instances = await steps.getInstances({
customerId: req.params.customerId,
status: { $ne: 'failed' },
});
if (instances.length > 0) {
res.send(instances[0]);
} else {
res.status(404).send({ error: 'No cleanup process found' });
}
});
export default app.init();
```
This example demonstrates a straight-through batch process that:
1. **Checks Customer Status**
- Calls external API to verify customer status
- Records last active date
- Handles API errors
2. **Updates Database**
- Sets new status based on activity
- Updates customer record
- Tracks update timestamp
3. **Transfers Data**
- Sends data to archive system
- Records transfer success
- Handles transfer errors
### Sub-workflows
Sub-workflows allow you to compose complex workflows by breaking them into smaller, reusable components. A main workflow can start a sub-workflow and wait for it to complete before continuing. This pattern is useful for:
- **Modular Design**: Break complex workflows into smaller, manageable pieces
- **Reusability**: Create sub-workflows that can be used by multiple parent workflows
- **Separation of Concerns**: Keep different business logic in separate workflow definitions
- **Parallel Processing**: Run multiple sub-workflows simultaneously
- **Error Isolation**: Contain errors within sub-workflows without affecting the parent
The example below shows a main workflow that processes a number and then waits for a sub-workflow to complete before finishing. The sub-workflow updates the parent's state to signal completion.
```mermaid
graph TD
%% Main Workflow
Begin[begin] --> Decide[decide]
Decide -->|even| EvenPath[evenPath]
Decide -->|odd| OddPath[oddPath]
EvenPath --> End[end]
OddPath --> End
%% Subflow
SubBegin[subflow.begin] --> SubEnd[subflow.end]
%% Connection between workflows
End -->|wait for subflow| SubBegin
SubEnd -->|update parent state| End
%% Styling
style Begin fill:#2d3748,stroke:#4a5568,stroke-width:2px,color:#e2e8f0
style End fill:#374151,stroke:#4b5563,stroke-width:2px,color:#e2e8f0
style Decide fill:#1e3a3a,stroke:#2d4a4a,stroke-width:2px,color:#e2e8f0
style EvenPath fill:#22543d,stroke:#276749,stroke-width:2px,color:#e2e8f0
style OddPath fill:#22543d,stroke:#276749,stroke-width:2px,color:#e2e8f0
style SubBegin fill:#4a2d3a,stroke:#5a3d4a,stroke-width:2px,color:#e2e8f0
style SubEnd fill:#4a2d3a,stroke:#5a3d4a,stroke-width:2px,color:#e2e8f0
```
The diagram above shows the flow where the main workflow processes a number (even/odd branching), then **waits** for a sub-workflow to complete. The sub-workflow runs independently but communicates back to the parent through state updates. In the implementation below, you'll see how:
- The main workflow starts a sub-workflow and passes its own ID as `parentId`
- The sub-workflow completes its task and updates the parent's state
- The parent workflow continues once it detects the sub-workflow has finished
- Both workflows use the same state management system for coordination
- The main workflow uses the `wait()` function to pause execution until the sub-workflow signals completion
```javascript {54,55,68} title="subflow.js"
import { app } from 'codehooks-js';
// Create a main workflow definition
const workflow = app.createWorkflow('simpleTask', 'Basic workflow example', {
// Step 1: Initialize the workflow
begin: async function (state, goto) {
state = {
message: 'Starting workflow',
// Add a random number to demonstrate branching
value: Math.floor(Math.random() * 10),
};
goto('decide', state);
},
// Step 2: Decide which path to take
decide: async function (state, goto) {
// Branch based on whether the value is even or odd
if (state.value % 2 === 0) {
goto('evenPath', state);
} else {
goto('oddPath', state);
}
},
// Step 3a: Handle even numbers
evenPath: async function (state, goto) {
state = {
message: 'Processing even number',
path: 'even',
processed: true,
};
goto('end', state);
},
// Step 3b: Handle odd numbers
oddPath: async function (state, goto) {
state = {
message: 'Processing odd number',
path: 'odd',
processed: true,
};
goto('end', state);
},
// Step 4: End the workflow
end: async function (state, goto, wait) {
if (state.subflowCompleted) {
state = {
final_message: `Workflow completed! Processed ${state.path} number: ${state.value}`,
};
goto(null, state); // workflow complete
} else {
// Start the subflow and wait for it to complete, set the parentId in the subflow state
await subflow.start({ parentId: state._id });
wait({ wait_message: 'Waiting for subflow to complete' });
}
},
});
// Create a subflow definition
const subflow = app.createWorkflow('subflow', 'Subflow', {
begin: async (state, goto) => {
state.message = 'Hello from subflow';
goto('end', state);
},
end: async (state, goto) => {
// Update the main workflow state to indicate that the subflow has completed
workflow.updateState(
state.parentId,
{
subflowCompleted: true,
},
{ continue: true }
);
goto(null, state);
},
});
// emitted event when a workflow completes
workflow.on('completed', (data) => {
console.log('Workflow completed:', data);
});
// REST API to start a new workflow instance
app.post('/start', async (req, res) => {
const result = await workflow.start({ foo: 'bar' });
res.json(result);
});
// export app interface to serverless execution
export default app.init();
```
## Best Practices
1. **State Management**
- Keep state objects simple and serializable
- Use atomic updates for state changes
- Validate state at each step
- Monitor state size and complexity
2. **Error Handling**
- Monitor errors in logs and database
- Use try/catch in steps that might fail
- Set appropriate timeouts
- Implement retry logic for transient failures
3. **Scalability**
- Design stateless step functions
- Use appropriate queue configurations
- Monitor worker performance
- Implement proper error recovery
4. **Monitoring**
- Track workflow completion rates
- Monitor error frequencies
- Watch queue lengths
- Set up alerts for critical failures
## Related Topics
- [Queue API](/docs/queueapi) - Learn about message queues
- [Job Hooks](/docs/jobhooks) - Schedule recurring tasks
- [Data Operators](/docs/dataoperators) - Work with data in your steps
- [CLI Tools](/docs/cli) - Command-line tools for monitoring workflows and queues
- [workflow-status](/docs/cli#workflow-status) - Visualize and monitor workflow instances
- [queue-status](/docs/cli#queue-status) - Monitor queue health and workers
### Other popular Workflow Engines
- [Temporal](https://temporal.io/) - Open source workflow engine
- [Zeebe](https://camunda.com/platform/zeebe/) - Cloud-native workflow engine
- [AWS Step Functions](https://aws.amazon.com/step-functions/) - Serverless workflow service
- [Zapier](https://zapier.com/) - No-code workflow automation platform
- [Pipedream](https://pipedream.com/) - Developer-first workflow automation
- [n8n](https://n8n.io/) - Open source workflow automation
- [Airflow](https://airflow.apache.org/) - Platform for data pipelines
- [Prefect](https://www.prefect.io/) - Modern workflow orchestration
- [Dagster](https://dagster.io/) - Data orchestration platform
- [Make (Integromat)](https://www.make.com/) - Visual workflow automation
---
## Overview
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
import myImageUrl from './images/quickstart/collection-with-data.webp';
import CodeBlock from '@theme/CodeBlock'
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import Link from '@docusaurus/Link';
import Badge from '@mui/material/Badge';
import {Button, Grid} from '@mui/material';
import Studio1Png from './images/studio/studio1.webp';
import Studio2Png from './images/studio/studio2.webp';
import Studio3Png from './images/studio/studio3.webp';
import Studio4Png from './images/studio/json-schema.webp';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import LayersIcon from '@mui/icons-material/Layers';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import Admonition from '@theme/Admonition';
import {
CogIcon,
CheckCircleIcon,
CheckIcon,
DocumentCheckIcon,
DocumentTextIcon,
RocketLaunchIcon,
CloudArrowUpIcon,
MagnifyingGlassPlusIcon,
LockClosedIcon,
PlayIcon,
CircleStackIcon,
DocumentPlusIcon,
KeyIcon,
ClockIcon,
RectangleStackIcon,
ChartBarSquareIcon,
LinkIcon,
QueueListIcon,
SparklesIcon
} from '@heroicons/react/24/outline';
export const NumberIcon = ({ index = -1, style = {}}) => (
{index}
);
export const codex0 = `import {app} from 'codehooks-js'
// Use Crudlify to create a REST API for any database collection
app.crudlify()
// bind to serverless runtime
export default app.init();
`;
export const codex1 = `const URL = 'https://myproject-fafb.api.codehooks.io/dev/orders';
response = await fetch(URL+'?status=OPEN', /* options */)
const result = await response.json()
`;
export const codex2 = `import {app, datastore} from 'codehooks-js'
// REST API route
app.get('/sales', async (req, res) => {
// highlight-next-line
const conn = await datastore.open();
const query = { status: "OPEN" };
const options = { sort: { date: 1 } }
conn.getMany('orders', query, options).json(res); // stream JSON to client
})
// bind to serverless runtime
export default app.init();
`;
export const codex3 = `import {app, datastore} from 'codehooks-js'
// REST API route
app.post('/cacheit', async (req, res) => {
const conn = await datastore.open();
const opt = {ttl: 60*1000};
// highlight-next-line
await conn.set('my_cache_key', '1 min value', opt);
res.send('Look dad, I have my own cache now!');
})
// bind to serverless runtime
export default app.init();
`;
export const codex4 = `import {app} from 'codehooks-js'
// serve a direcory of Blob storage files
// highlight-next-line
app.storage({route:"/documents", directory: "/myblobs"})
// CRUD REST API
app.crudlify()
// bind to serverless runtime
export default app.init();`;
export const codex5 = `import app from 'codehooks-js'
// Trigger a function with a CRON expression
// highlight-next-line
app.job('*/5 * * * *', (_, job) => {
console.log('Hello every 5 minutes')
// Do stuff ...
job.end()
});
export default app.init();`;
export const codex6 = `import { app, Datastore } from 'codehooks-js'\n
// register a Queue task worker
// highlight-next-line
app.worker('ticketToPDF', (req, res) => {
const { email, ticketid } = req.body.payload;
//TODO: implement code to fetch ticket data, produce PDF
// and send email with attachment using Mailgun, Sendgrid or Amazon SES
// ...and implement error handling
res.end(); // done processing queue item
})\n
// REST API
app.post('/createticket', async (req, res) => {
const { email, ticketid } = req.body;
const conn = await Datastore.open();
// highlight-next-line
await conn.enqueue("ticketToPDF", { email, ticketid });
res.status(201).json({"message": \`Check email $\{email\}\`});
})\n
export default app.init();
`;
export const codex7 = `import { app, Datastore, aggregation } from 'codehooks-js'
... // collapsed code
const spec = {
$group: {
$field: "month",
$max: ["sales", "winback"],
$sum: "sales"
}
}
const db = await datastore.open();
const dbstream = db.getMany('salesData');
// highlight-next-line
const result = await aggregation(dbstream, spec)
... // collapsed code
`;
export const codex8 = `{
"oct": {
"sales": {
"max": 1477.39,
"sum": 1234.05
},
"winback": {
"max": 22.0
}
},
"nov": {
"sales": {
"max": 2357.00,
"sum": 5432.00
},
"winback": {
"max": 91.0
}
},
}`;
export const codex9 = `import {app} from 'codehooks-js'
// serve /dist for react frontend
app.static({
route: '/',
directory: '/dist',
default: 'index.html',
notFound: '/index.html'
}, cacheFunction)
// Optionally define cache function for static assets
function cacheFunction(req, res, next) {
const ONE_DAY = 24 \* 60 \* 60 \* 1000; // 24 hours in milliseconds
res.set('Cache-Control', \`public, max-age=\${ONE_DAY}, s-maxage=\${ONE_DAY}\`)
res.set('Expires', new Date(Date.now() + ONE_DAY).toUTCString())
res.removeHeader('Pragma');
next()
}
// CRUD REST API (if you need it)
app.crudlify({}, {prefix: '/api'})
// bind to serverless runtime
export default app.init();
`;
export const codex10 = `export default {
build: {
outDir: '../backend-src/dist', // Points to your codehooks source folder
},
};`;
export const webhookStripe = `import {app} from 'codehooks-js'
import Stripe from 'stripe';
app.post('/webhooks/stripe', async (req, res) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
} catch (err) {
console.log('Webhook signature verification failed:', err.message);
return res.status(400).send('Webhook Error: ' + err.message);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('PaymentIntent was successful!', paymentIntent.id);
break;
case 'customer.subscription.created':
const subscription = event.data.object;
console.log('New subscription created:', subscription.id);
break;
default:
console.log('Unhandled event type', event.type);
}
res.json({received: true});
});
export default app.init();
`;
export const SmallFeature = ({
icon,
link = '#',
title = 'No title',
content =
);
};
## The agent-native backend platform
Codehooks.io is the **agent-native backend platform**. A complete, isolated backend — API routes, NoSQL database, key-value store, worker queues, cron jobs, and static asset hosting — that deploys in under 5 seconds from a single CLI command. Everything is built in. Nothing needs to be wired together.
The entire lifecycle is CLI-native:
- Create a project (`coho create`)
- Deploy code and assets (`coho deploy`)
- Inspect logs in real time (`coho log -f`)
- Query the database directly (`coho query`)
- Manage secrets, schemas, settings — all from the terminal
This means any tool that can run shell commands — Claude Code, Cursor, Codex, Cline, or a custom agent — can autonomously create, deploy, verify, and iterate on a production backend without human intervention. For the developer, the job becomes: specify what you want, review what the agent built, and verify it works.
Create projects using the CLI or the web-based [Studio](/docs/studio). A Codehooks.io project contains one or more isolated Spaces — complete self-contained environments with their own database, settings, and code.
The **[`codehooks-js`](https://www.npmjs.com/package/codehooks-js)** library is the foundation for all Codehooks development, offering 8 specialized APIs for building APIs, webhooks, event-driven architectures, and workflow automation. Explore each capability below to see how Codehooks makes development effortless.
:::tip Start Building in Minutes
Get up to speed fast with these essential resources:
- 🤖 [AI Agent Setup](/docs/ai-agent-setup) - Set up Claude Code, Cursor, Codex, or any AI agent to build on Codehooks
- 📋 [API Cheat Sheet](/docs/apicheatsheet) - All essential APIs on one page
- 🎥 [7-minute quick introduction](https://www.youtube.com/watch?v=3LGRhLU5a5A) - Join us building a complete CRUD API from scratch (it's easy)
:::
:::tip Codehooks CLI tool
To deploy code (and much more), you use the Codehooks CLI tool:
`npm install codehooks -g`
Code editing and deployment can also be done from the web based UI.
:::
## API and Webhook Development
}
title="Handle webhooks from any service"
content={
Codehooks makes it easy to receive and process webhooks from popular
services like Stripe, GitHub, Shopify, and more. The{' '}
req.rawBody property provides access to the raw request
body, which is essential for webhook signature verification.
The example below shows a Stripe webhook handler that verifies the webhook
signature using req.rawBody and processes different event types:
{webhookStripe}
This same pattern works for any webhook provider that requires signature
verification, including:
Stripe - Payment events and subscription updates
GitHub - Repository events, pull requests, and
CI/CD triggers
Shopify - Order notifications and inventory updates
Twilio - SMS and voice event notifications
And many more...
}
nextsteps={[
Codehooks webhook templates,
Stripe webhooks example,
Custom REST API routes,
Process webhooks asynchronously with Queues
,
Chain webhook events into Workflows,
]}
/>
}
title="Get a flying start with an automatic CRUD REST API"
content={
Consider a Codehooks project, for example myproject-fafb.
Under the hood, Codehooks deploys an automatic REST API which is made up
of the following really short - but complete application code:
{codex0}
It's just 3 lines of code 🔥, and it delivers a fully secure CRUD REST
API with a NoSQL database for JSON queries and updates. Client apps can
instantly connect to your data API, for example from a JavaScript app
like the example code snippet shown below:
{codex1}
}
nextsteps={[
Database REST API detailed docs,
Data validation and schema support
,
Develop custom REST APIs,
Auto-generate OpenAPI documentation,
Cheat Sheet for all the APIs,
]}
/>
Generate interactive Swagger UI documentation directly from your code.
Define schemas with Zod, Yup, or JSON Schema and Codehooks automatically
creates OpenAPI 3.0 specs. No manual YAML writing required.
}
title="Easy database API inspired by MongoDB"
content={
Sometimes a standard CRUD REST API isn't enough. With custom JavaScript
functions you can create logic that integrates directly to the NoSQL
datastore. No need to think about drivers, versions, protocol or
security, the database is just there, available as an intrinsic object
in every serverless function.
{codex2}
}
nextsteps={[
Getting started,
Database API,
NoSQL query language
,
Database indexing,
Check output logs with the CLI,
Query data with the CLI,
]}
/>
}
title="Speed up your application with a Key-Value cache"
content={
The Codehooks data engine includes a simple key-value datastore. A
key-value datastore has many use-cases, and is ideal for caching due to
the swift data access and simple, maintenance-friendly architecture. A
simple example is shown below, where a string is stored for 60 seconds
with the ttl option.
{codex3}
}
nextsteps={[
Key-Value API,
Tutorials,
]}
/>
## Background Jobs
}
title="CRON expressions + your function keeps running"
content={
Use simple CRON expressions to automate and manage the execution of
routine tasks. You can schedule a job to run at multiple, disparate
times. For example, you can schedule a task to run at 2 AM on Sundays
and 4 PM on Wednesdays with the same CRON job.
In the simple example below, the CRON expression{' '}
*/5 * * * * will trigger a JavaScript function each 5
minutes.
}
title="Scale workloads with queues and worker functions"
content={
Use persistent queues and "worker" functions to create robust,
efficient, and highly responsive back-end systems. Queues scales
processing across multiple nodes and will restore and continue working
in case of errors and system failures.
Check out the example below, where a REST API request starts a complex
task by adding it to the Queue for later processing.
Create and deploy robust, scalable workflows using queued functions and state management.
Build reliable backend systems with automatic retry, state persistence, and distributed processing.
The example below shows a simple workflow that processes customer onboarding with email validation.
{`import { app } from 'codehooks-js';
// define the workflow
const workflow = app.createWorkflow('customerOnboarding', 'Customer signup and email validation', {
START: async function (state, goto) {
state = { email: state.email, validated: false };
await sendWelcomeEmail(state.email);
goto('waitForValidation', state);
},
waitForValidation: async function (state, goto) {
if (state.validated) {
goto('complete', state);
} else {
wait({wait_message: 'Waiting for subflow to complete'});
}
},
complete: function (state, goto) {
state.status = 'completed';
goto(null, state); // workflow complete
}
});
// Start workflow from REST API
app.post('/start-onboarding', async (req, res) => {
const result = await workflow.start({ email: req.body.email });
res.send(result);
});
export default app.init();`}
}
nextsteps={[
Workflow API,
Example Workflows
]}
/>
## Blob Storage API
}
title="Store and manage files programmatically"
content={
Codehooks provides a built-in blob storage system for uploading,
storing, and serving files through your applications. Use the File API
to handle file operations programmatically in your serverless functions.
You can upload files to blob storage using the CLI command:
The example below shows how to handle file uploads, downloads, and
management through the File API in your application code:
{codex4}
}
nextsteps={[
File API,
Upload files with the CLI,
]}
/>
## Data Aggregation
}
title="Easy to use data aggregation format"
content={
Transform your live data streams into insights with just a few lines of
code. Codehooks has a simple but powerful JSON-based aggregation
expression language to specify data transformations. Groups and sums can
be nested to create both simple and advanced data outputs. The
aggregation API can process input from any JSON data-stream, for example
the query output from the NoSQL datastore. 🔥
{codex7}
The example aggreagation transformation produces JSON output similar to
the example below.
{codex8}
}
nextsteps={[Aggregation API]}
/>
## Frontend Hosting Setup
}
title="Host your frontend applications alongside your API"
content={
There are several benefits to hosting the frontend along with the backend API:
No CORS issues - Frontend and API share the same domain
Simplified deployment - Single command deploys both frontend and backend
Cost effective - No need for separate hosting services
Codehooks can serve your frontend applications as static assets, making
it easy to deploy full-stack SPA and PWA applications from a single codebase. All you need to do
is to include the HTML and CSS files in the source code and
then use the app.static file handler to serve them along with the backend code.
This hosting option is perfect if you don't need the high scalability offered by CDN-hosted frontends.
Important: Codehooks only serves static assets, not server-side
rendered content. For frameworks like Next.js, Nuxt and SvelteKit, you must configure
them for static generation/export (SSG).
Simply copy your framework's build or dist folder into your Codehooks
project source and configure the static handler to serve it. When you run codehooks deploy, both your API and your frontend will be deployed:
{codex9}
Note: The default and{' '}
notFound settings are crucial for client-side routing. They
ensure that all routes (like /about or{' '}
/users/123) serve the index.html file,
allowing your frontend router to handle navigation instead of the server
returning 404 errors.
This setup works seamlessly with popular frameworks configured for
static builds:
Pure HTML/CSS: Just keep the source with backend code from the start (in the dist/ folder in this example)
React: Copy your build/ folder (after{' '}
npm run build)
Vue: Copy your dist/ folder (after{' '}
npm run build)
Next.js: Configure static export (
next export), then copy your out/ folder
Nuxt: Configure static generation (
nuxt generate), then copy your dist/{' '}
folder
Svelte: Copy your build/ folder (after{' '}
npm run build)
SvelteKit: Configure static adapter, then copy your{' '}
build/ folder
Angular: Copy your dist/ folder (after{' '}
ng build)
Astro: Copy your dist/ folder (after{' '}
npm run build)
Gatsby: Copy your public/ folder
(after gatsby build)
We recommend automating this step by setting your frontend framework's output directory to point directly to your backend source folder.
This eliminates the manual copy step in your deployment workflow.
Example with Vite (in vite.config.js):
{codex10}
The example shown here is without any authentication. For most useful applications you would need to set up your frontend to use an authentication framework (Clerk, Auth0, Stack-Auth, codehooks-auth).
}
nextsteps={[
Example of static site hosting with Codehooks, React and codehooks-auth
,
Static file serving documentation
,
Authentication
,
]}
/>
);
}
export default App;
```
The React app is almost ready, we just have to create the backend API and database that serve the content of the `message` and `visits` variables.
## Create the React backend API
The React backend JavaScript example code is located in the `index.js` file in the `reactback` directory. The complete code (14 lines and zero config) for the backend API and database is shown below:
```js title="reactback/index.js"
// React backend example app
import { app, Datastore } from 'codehooks-js'
// REST API
app.get('/hello', async (req, res) => {
const db = await Datastore.open();
// increment visit counter in key-value database
const visits = await db.incr('hits', 1);
res.json({ "message": "Hello React world!", "visits": visits });
});
// bind to serverless runtime
export default app.init();
```
Now that we have the code for the backend API, we can create our new backend with the `init` [CLI command](/docs/cli).
[Sign up](https://account.codehooks.io/login?signup) for a new account and create your first project.
Next you need to install the CLI.
```bash
npm install -g codehooks
```
Then run the init command, this lets you select a project and environment for your new backend code.
```bash
cd reactback
coho init
```
This command creates a new backend application in the `reactback` directory, replace the generated index.js code with our code from the above index.js.
Before we can deploy our React backend to the cloud, we must add our npm package dependencies, in this simple example we just have to add the [codehooks-js](https://www.npmjs.com/package/codehooks-js) package:
```bash
npm i codehooks-js
# deploy the backend application to the codehooks.io cloud service
coho deploy
```
We also need a read-only `API_KEY` key for secure access to our API from the React example app, the `add-token` command fixes this:
```bash
coho add-token --readonly
```
:::tip
You can also add tokens, set environment variables and more for the backend using the UI at [account.codehooks.io](https://account.codehooks.io)
:::
To view our API endpoint and API token(s) we run the `info` command. If we also add the `--examples` parameters the CLI will generate example `curl` commands to test our API 🤖. Command example output is shown below:
```bash
coho info --examples
Project name: reactback-nyly
Team: Eric Jones (personal account)
API endpoint: https://reactback-nyly.api.codehooks.io/dev
Examples in cURL:
curl https://reactback-nyly.api.codehooks.io/dev/myroute -H 'x-apikey: a4679c85-b4c8-49fb-b8ac-63230b269dd7' -H 'Content-Type: application/json'
curl --request POST https://reactback-nyly.api.codehooks.io/dev/myroute -H 'x-apikey: ' -H 'Content-Type: application/json' --data-raw '{"name": "Mr. Code Hooks", "active": true }'
Spaces:
┌──────────────┬───────────────────────────────────────────┬───────────────┬──────┬──────┐
│ Name │ Tokens │ Allowed Hosts │ Jwks │ Env │
├──────────────┼───────────────────────────────────────────┼───────────────┼──────┼──────┤
│ dev (active) │ a4679c85-b4c8-49fb-b8ac-63230b269dd7 (R) │ │ │ │
└──────────────┴───────────────────────────────────────────┴───────────────┴──────┴──────┘
```
Replace the following variables in the React `App.js` file with yours from the `info` command output:
```js
const API_ENDPOINT = 'https://.api.codehooks.io/dev/hello';
const API_KEY = '';
```
## Start your React app
We are now ready to start the React example app. From the root directory of your React app run the npm start command:
```bash
npm start
# your app runs at http://localhost:3000/
```
## Changing your React backend code is easy
Having the data pipeline up and ready between the React app and the backend API makes it easy to make changes and re-deploy your application. If you make changes in the backend code, there is only 1 step you need to do:
1. From the backend app directory `reactback`, run the `coho deploy` command again and the changes to your React backend API are applied immediately 🔥
## Summary
Creating a React front-end and backend application is quite easy. Using your favorite tools together with effective CLI commands is a good workflow that supports rapid iterations and faster development.
Full source code is [available on GitHub here](https://github.com/RestDB/codehooks-io-examples/tree/main/react-backend-example).
Happy coding!
---
## Typescript support
*(Note: This content contains MDX/JSX code)*
Codehooks.io supports Typescript (version 5). This example shows how easy it is to use Typescript to create serverless functions.
1. Start with creating a new project using the `coho create` or `coho init` CLI command.
2. Change to the project directory and install `npm i codehooks-js`.
3. Rename the autogenerated `index.js` file to `index.ts`. That's it. You are now using Typescript as a language.
In the following example we create two files; `index.ts` with two serverless API functions and `sum.ts` with one exported function.
```js title=index.ts
import {app, httpRequest, httpResponse} from 'codehooks-js';
import { sum } from './sum';
function helloFunc(req: httpRequest, res: httpResponse){
res.end(`Typescript sum is ${sum(42, 42)}`);
}
app.get('/helloworld', helloFunc);
app.all('/echo', (req: httpRequest, res: httpResponse) => {
res.end(JSON.stringify(req, null, " ")); // format JSON string nicely
})
export default app.init();
```
```js title=sum.ts
export function sum(a: number, b: number): number {
return a + b;
}
```
Deploy with the CLI command `coho deploy`.
Run `coho info` to see your API token.
## Testing the helloworld API with curl
```js title="Shell command"
curl https://.api.codehooks.io/dev/helloworld -H 'x-apikey: '
```
```bash title="Output"
Typescript sum is 84
```
## Testing the echo API with curl
```js title="Shell command"
curl https://.api.codehooks.io/dev/echo -H 'x-apikey: '
```
```js title="Example output"
{
"hostname": "myproject-abcd.api.codehooks.io",
"headers": {
"host": "myproject-abcd.api.codehooks.io",
"user-agent": "curl/7.77.0",
"accept": "*/*",
"x-apikey": "6e111ea6-6b3a-412d-8f6b-061c816c67c8"
},
"query": {},
"path": "/dev/echo",
"apiPath": "/echo",
"originalUrl": "/dev/echo",
"params": null,
"body": {},
"method": "GET"
}
```
---
## File API
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
The File API enables your application to access and manage folders and files.
The API is automatically available from the inside of any Codehook function.
**Install**
Run `npm install codehooks-js` in your project code folder.
:::info Bulk upload files
Use the CLI command [coho upload](/docs/cli#file-upload) to upload files from a local machine to the application.
:::
**API quick overview**
## readFile(path)
Read file content as text from the file system.
**Parameters**
- **path**: full path to file, e.g. '/documents/doc.html'
**Returns**: Promise with text content from file.
**Error**: throws exception if file is not found.
**Code example for reading a file**
```js {6} title="index.js"
import { app, filestore } from 'codehooks-js';
// get a file text content
app.get('/txt', async (req, res) => {
try {
const file = await filestore.readFile('/somefolder/myfile.txt');
res.set('content-type', 'text/plain');
res.end(file);
} catch (error) {
res.status(404).end('No file here');
}
});
// bind to serverless runtime
export default app.init();
```
## getReadStream(path)
Read file content as a binary stream the file system.
**Parameters**
- **path**: full path to file, e.g. `/private/dummy.pdf`
**Returns**: Promise with file stream handle.
**Error**: throws exception if file is not found.
**Code example for reading a binary file stream**
```js {6} title="index.js"
import { app, filestore } from 'codehooks-js';
// get a file binary content as stream
app.get('/pdf', async (req, res) => {
try {
const filestream = await filestore.getReadStream('/private/dummy.pdf');
res.set('content-type', 'application/pdf');
// stream content back to client
filestream
.on('data', (buf) => {
res.write(buf, 'buffer');
})
.on('end', () => {
res.end();
});
} catch (error) {
console.error(error);
res.status(404).end('No file here');
}
});
// bind to serverless runtime
export default app.init();
```
## saveFile(path, filestream)
Write a file binary stream to the file system.
**Parameters**
- **path**: full path to file, e.g. `/private/dummy.pdf`
- **filestream**: inputstream of binary buffers
**Returns**: Promise with upload result text.
**Error**: throws exception if an error occured.
**Code example for writing a binary inputstream as a new file to the file system**
```js {10} title="index.js"
import { app, filestore } from 'codehooks-js';
import { PassThrough } from 'stream';
// post a file binary content
app.post('/file', async (req, res) => {
try {
const { name } = req.query; /* e.g. /dev/file?name=/upload/dummy.pdf */
const stream = new PassThrough();
req.pipe(stream);
const result = await filestore.saveFile(name, stream);
res.end(result);
} catch (error) {
console.error(error);
res.status(404).end('Upload error');
}
});
// bind to serverless runtime
export default app.init();
```
## deleteFile(path)
Delete a file binary from the file system.
**Parameters**
- **path**: full path to file, e.g. `/private/dummy.pdf`
**Returns**: Promise with delete result text.
**Error**: throws exception if an error occured.
**Code example for deleting a file from the file system**
```js {7} title="index.js"
import { app, filestore } from 'codehooks-js';
// delete a file
app.delete('/file', async (req, res) => {
try {
const { name } = req.query;
const result = await filestore.deleteFile(name);
res.end(result);
} catch (error) {
console.error(error);
res.status(404).end('Not found');
}
});
// bind to serverless runtime
export default app.init();
```
## list(path)
List files in folders from the file system.
**Parameters**
- **path**: full path to folder, e.g. `/upload`
**Returns**: Promise with JSON array of files matching path to folder.
**Error**: throws exception if an error occured.
**Code example for listing files in a folder from the file system**
```js {6} title="index.js"
import { app, filestore } from 'codehooks-js';
// list dir
app.get('/list', async (req, res) => {
try {
const result = await filestore.list('/');
res.end(result);
} catch (error) {
console.error(error.message);
res.status(400).end('sorry list');
}
});
// bind to serverless runtime
export default app.init();
```
**Example output shows folders and files**:
```title="JSON array"
[
{"path":"/robots.txt","size":114,"date":"2023-09-29T12:12:10.000Z","_id":"6516f3b26c816e0015ca7b60"},
{"path":"/upload/dummy.pdf","size":1234,"date":"2023-09-29T16:22:10.499Z","_id":"6516f9b26c816e0015ce7b6d"},
{"path":"/upload/logo.pdf","size":259096,"date":"2023-09-30T11:48:18.003Z","_id":"6517155cd8c8a40071d6bc79"},
{"path":"/upload/report.html","size":1320,"date":"2023-09-30T12:46:31.640Z","_id":"65180b50d8fc2b01b3b2515e"}
]
```
## readFileAsBuffer(path)
Read file content into a Buffer.
**Parameters**
- **path**: full path to folder, e.g. `/upload`
**Returns**: Promise with Buffer.
**Error**: throws exception if an error occured.
**Code example for reading file content into a Buffer**
```js
// collapsed code
const buf = await filestore.readFileAsBuffer('/images/logo.png');
console.log(buf.length);
```
## Postman examples using the File API
### Upload a new PDF file

### Download a PDF file

---
## Indexing API
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
Create fast lookup indexes in a datastore. Combined with [streaming queries](nosql-database-api#getmanycollection-options), indexing can be a big improvement for your application performance.
**API quick overview**
## Datastore.open()
Datastore.open() opens a connection to a datastore in the active project space.
```js
import {Datastore} from 'codehooks-js'
async function myfunc() {
const conn = await Datastore.open();
// use conn to call API functions
...
}
```
**Returns** Promise / Datastore connection
## createIndex(collection, indexes)
Create index(es) on a collection in the current Datastore
**Parameters**
- **collection**: collection string
- **indexes**: Array of field names to index
Returns **Promise / Indexes**
**Code example**
```js
const conn = await Datastore.open();
const idx = await conn.createIndex('stocks', ['Name', 'Volume']);
```
:::warning
Index creation can take a long time on big collections.
It's better to create indexes before adding data.
:::
## removeIndex(collection, indexes)
Remove index(es) on a collection in the current Datastore
**Parameters**
- **collection**: collection string
- **indexes**: Array of field names to remove from index
Returns **Promise / Indexes**
**Code example**
```js
const conn = await Datastore.open();
const idx = await conn.removeIndex('stocks', ['Name', 'Volume']);
```
:::tip
You can also use the [CLI](cli#createindex) to manage indexes.
:::
---
## Job background workers
*(Note: This content contains MDX/JSX code)*
Job workers lets you schedule background worker functions as recurring `cron` jobs or as one-time `runAt` jobs. When a job event _triggers_, your function is called accordingly.
## Cron jobs
### Example cron jobhook
The example job worker function prints to the log each time the cron triggers.
```js
import app from 'codehooks-js';
app.job('* * * * *', minute);
app.job('*/30 * * * *', halfPast);
function minute(req, res) {
console.log('Each minute');
res.end(); // done
}
function halfPast(req, res) {
console.log('Each half hour');
res.end(); // done
}
export default app.init();
```
:::info tip
Utility to create CRON expressions [https://crontab.guru/](https://crontab.guru/)
:::
- Running jobs are stopped and re-started on each deployment (`coho deploy`)
- Running jobs are stopped on undeployment (`coho undeploy`)
### Cron Syntax
Quick reference to cron syntax.
```
┌────────────── second (optional) 0-59
│ ┌──────────── minute 0-59
│ │ ┌────────── hour 0-23
│ │ │ ┌──────── day of month 1-13
│ │ │ │ ┌────── month 1-12
│ │ │ │ │ ┌──── day of week 0-7 (0 or 7 are sunday)
│ │ │ │ │ │
│ │ │ │ │ │
* * * * * *
```
### Example cron expressions
**Run five minutes after midnight, every day**
`5 0 * * *`
**Run at 2:15pm on the first of every month**
`15 14 1 * *`
**Run at 10 pm on weekdays**
`0 22 * * 1-5`
## Schedule delayed worker function dynamically with _runAt_
Cron jobs are declare at deploy-time and they run forever, or until they are changed or removed by a new deployment.
The `schedule.runAt` API is another way to create a one time job that is executed by a worker function.
### Example runAt scheduled worker function
The example worker function prints the data payload to the log each time the scheduled datetime triggers.
```js {4,17}
/*
* runAt example.
*/
import { app, schedule } from 'codehooks-js';
app.worker('myworker', (data, job) => {
const { payload } = data.body;
console.log('Delayed job', payload);
job.end();
});
// REST hook
app.get('/hello', async (req, res) => {
const when = new Date(Date.now() + 5000);
const data = { message: 'Hello later!' };
const worker = 'myworker';
await schedule.runAt(when, data, worker);
res.end();
});
// bind to serverless runtime
export default app.init();
```
If we call the `/hello` API the `coho logs` command will display the following:
```bash
dev 2023-03-23T17:37:54.198Z Delayed job {
"message": "Hello later!"
}
dev 2023-03-23T17:37:54.940Z Delayed job {
"message": "Hello later!"
}
dev 2023-03-23T17:37:55.652Z Delayed job {
"message": "Hello later!"
}
...
```
## Schedule worker function to run immediate
You can also execute a worker function without any delay like this:
```js
...
schedule.run({data}, 'myworker')
...
```
---
## Codehooks.io Local Development with Docker
*(Note: This content contains MDX/JSX code)*
# Codehooks.io Local Development with Docker
This guide explains how to set up and develop Codehooks.io applications locally using Docker for a seamless development experience. Local development enables instant hot reloading and rapid iteration without deployment delays, dramatically speeding up your development workflow.
## Account Requirements
**Good news!** You can create and develop Codehooks applications locally without registering any account at Codehooks.io. The local development environment allows you to:
- Build and test your APIs locally
- Use all Codehooks features and libraries
- Develop and debug your application completely offline
- Access your API endpoints at `http://localhost:8080/dev/*`
**However**, to deploy your application to the Codehooks.io cloud platform, you will need to:
1. Create a free account at [codehooks.io](https://codehooks.io)
2. Use the Codehooks CLI to authenticate and deploy your app
3. Configure your production environment variables and settings
This means you can start building and experimenting with Codehooks right away, and only create an account when you're ready to share your application with the world!
## 🚀 Quick Start
Get up and running in 2 minutes:
### 1. Install Requirements
Install the necessary packages in your app directory:
```bash
npm install -g codehooks
npm install codehooks-js
npm install -D nodemon
```
### 2. Create a Simple App File
Create `index.js` with your text editor and add:
```javascript
import { app } from "codehooks-js";
app.get("/", (req, res) => res.send("Hello World!"));
export default app.init();
```
### 3. Setup Package.json for Hot Reloading
Add these scripts to your `package.json`:
```js
{
"scripts": {
"build": "codehooks compile --tofile index.cjs",
"dev": "nodemon --watch . --ext js,html,css,json --exec \"npm run build\"",
"start": "docker compose up -d",
"stop": "docker compose down",
"logs": "docker compose logs -f coderunner-service"
}
}
```
### 4. Get Docker Setup
Download the Docker compose configuration:
```bash
curl -o docker-compose.yml https://codehooks.io/download/docker-compose.yml
```
### 5. Initial Compile
Build your application for the first time:
```bash
npm run build
```
### 6. Start Local Server
Start the local Codehooks server:
```bash
npm run start
```
### 7. Start Hot Reloading
In a new terminal, start the development watcher:
```bash
npm run dev
```
### 8. Test It Works
Verify your API is running:
```bash
curl http://localhost:8080/dev
```
### 9. Make Changes and Watch Auto-Reload
Edit `index.js`, save, and see your changes instantly reflected in the browser!
### 10. Stop Everything When Done
Stop all services:
```bash
npm run stop
```
**That's it!** Your API is running at `http://localhost:8080/dev`
## Prerequisites
This section will guide you through installing everything you need for local Codehooks development. We'll explain what each tool does and provide installation instructions for Windows, macOS, and Linux.
### Step 1: Install Node.js
**What it does**: Node.js is the JavaScript runtime that powers Codehooks applications.
**Installation**:
🪟 Windows
**Option A: Download installer (recommended for beginners)**
1. Go to [nodejs.org](https://nodejs.org/)
2. Download the LTS version for Windows
3. Run the installer and follow the prompts
4. Open Command Prompt or PowerShell and verify: `node --version`
**Option B: Using Chocolatey**
```powershell
# Install Chocolatey first: https://chocolatey.org/install
choco install nodejs
```
**Option C: Using winget**
```powershell
winget install OpenJS.NodeJS
```
🍎 macOS
**Option A: Download installer**
1. Go to [nodejs.org](https://nodejs.org/)
2. Download the LTS version for macOS
3. Run the installer
**Option B: Using Homebrew (recommended)**
```bash
# Install Homebrew first: https://brew.sh/
brew install node
```
**Option C: Using MacPorts**
```bash
sudo port install nodejs18
```
🐧 Linux
**Ubuntu/Debian:**
```bash
# Using NodeSource repository (recommended)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
# Or using snap
sudo snap install node --classic
```
**CentOS/RHEL/Fedora:**
```bash
# Using NodeSource repository
curl -fsSL https://rpm.nodesource.com/setup_lts.x | sudo bash -
sudo dnf install nodejs
# Or using package manager
sudo dnf install nodejs npm
```
**Arch Linux:**
```bash
sudo pacman -S nodejs npm
```
**Verify Node.js installation:**
```bash
node --version # Should show v18.x.x or higher
npm --version # Should show 9.x.x or higher
```
### Step 2: Install Codehooks CLI
**What it does**: The Codehooks CLI compiles your code and manages deployments.
```bash
npm install -g codehooks
```
**Verify installation:**
```bash
codehooks --version
```
### Step 3: Install Docker
**What it does**: Docker runs the local Codehooks server environment, mimicking the cloud platform.
🪟 Windows
**Requirements**: Windows 10/11 with WSL 2 enabled
1. **Enable WSL 2** (if not already enabled):
```powershell
# Run as Administrator
wsl --install
# Restart computer if prompted
```
2. **Install Docker Desktop**:
- Download from [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/)
- Run installer and follow setup wizard
- Ensure "Use WSL 2 instead of Hyper-V" is checked
- Start Docker Desktop from Start Menu
3. **Verify installation**:
```powershell
docker --version
docker compose version
```
🍎 macOS
**Option A: Docker Desktop (easiest)**
1. Download from [docker.com/products/docker-desktop](https://www.docker.com/products/docker-desktop/)
2. Drag to Applications folder
3. Launch Docker Desktop and complete setup
**Option B: Colima (lightweight alternative)**
```bash
# Install via Homebrew
brew install colima docker docker-compose
# Start Colima
colima start
```
**Verify installation:**
```bash
docker --version
docker compose version
```
🐧 Linux
**Ubuntu/Debian:**
```bash
# Remove old versions
sudo apt-get remove docker docker-engine docker.io containerd runc
# Install dependencies
sudo apt-get update
sudo apt-get install ca-certificates curl gnupg lsb-release
# Add Docker's official GPG key
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Set up repository
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Add user to docker group (optional, avoids using sudo)
sudo usermod -aG docker $USER
# Log out and back in for this to take effect
```
**CentOS/RHEL/Fedora:**
```bash
# Install Docker
sudo dnf install docker docker-compose-plugin
# Start and enable Docker
sudo systemctl start docker
sudo systemctl enable docker
# Add user to docker group (optional)
sudo usermod -aG docker $USER
```
**Verify installation:**
```bash
docker --version
docker compose version
# Test Docker works
docker run hello-world
```
### Step 4: Create Your Project Directory
Follow the [Quick Start](#-quick-start) guide above to create and set up your project directory. It will walk you through creating a new Codehooks application from scratch.
**✅ You're now ready to start developing!** All the prerequisites are installed and configured.
## Development Workflow
### Daily Routine
```bash
# Start the server (if not running)
npm run start
# Start hot reloading (in a separate terminal)
npm run dev
# Your app is now available at:
# http://localhost:8080/dev
```
**During development:**
- Edit your `index.js` file
- Save → hot reload rebuilds automatically
- Check logs: `npm run logs`
- Stop everything: `npm run stop`
### Local URLs
| URL | Purpose |
|-----|---------|
| `http://localhost:8080/dev` | Main endpoint |
| `http://localhost:8080/dev/api/*` | API routes |
### Common Tasks
**Environment variables:**
```bash
# .env file
MY_API_KEY=your-secret-key
# In your code
res.json({ apiKey: process.env.MY_API_KEY });
```
**Database example:**
```javascript
import { app, Datastore } from "codehooks-js";
app.post("/api/users", async (req, res) => {
const db = await Datastore.open();
const user = await db.insertOne("users", req.body);
res.json(user);
});
export default app.init();
```
## Environment Variables
Create a `.env` file in your project root:
```env
# Example variables
PORT=8080
DBSECRET=password1234
EXTERNAL_API_KEY=your-api-key
```
Access in your code:
```javascript
const apiKey = process.env.EXTERNAL_API_KEY;
```
**Notes:**
- Add `.env` to `.gitignore`
- Restart containers after changes: `docker compose restart`
## Useful Commands
```bash
# Development
npm run start # Start server
npm run stop # Stop server
npm run dev # Hot reloading
npm run build # Manual build
npm run logs # View logs
# Docker
docker compose ps # Check status
docker compose restart # Restart services
docker compose down --volumes # Reset everything
# Debugging
codehooks --version # Check CLI version
docker --version # Check Docker version
```
## Troubleshooting
### Quick Diagnostics
```bash
# Check services status
docker compose ps
npm run logs
# Check versions
node --version
docker --version
codehooks --version
```
### Common Issues
**Port already in use:**
```bash
# Change port in .env file
PORT=3000
# Or find and kill process using port 8080
# macOS/Linux: lsof -i :8080
# Windows: netstat -ano | findstr :8080
```
**Docker not running:**
- **Windows**: Start Docker Desktop from Start Menu
- **macOS**: Launch Docker Desktop or `colima start`
- **Linux**: `sudo systemctl start docker`
**Hot reloading not working:**
```bash
# Check if nodemon is running
npm run dev
# Manual rebuild
npm run build
```
**App not loading:**
1. Check: `docker compose ps` (should show "Up")
2. Check: `npm run logs` for errors
3. Use correct URL: `http://localhost:8080/dev`
**Reset everything:**
```bash
npm run stop
docker compose down --volumes
npm run start
```
## Next Steps
You're now ready to:
- Build your API endpoints
- Test with curl or Postman
- [Deploy to production](https://codehooks.io/docs/quickstart-cli#deploy-the-code-to-the-serverless-cloud)
Happy coding! 🚀
---
## Key-Value Store API
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
With the Key-Value Store API, you can run Redis-like operations against a Key-Value database.
This API automatically available from the inside of any Codehook function.
:::tip Check out the Key-Value Store tutorial - 7 parts
- [Part 1: What is a Key-Value Store and how do you use it?](/docs/tutorials/key-val-store/part-1)
- [Part 2: Basic operations](/docs/tutorials/key-val-store/part-2-basic-operations)
- [Part 3: Increment and decrement operations](/docs/tutorials/key-val-store/part-3-increment-and-decrement-operations)
- [Part 4: Working with multiple values and streams](/docs/tutorials/key-val-store/part-4-working-with-multiple-values-and-streams)
- [Part 5: Managing data with TTL options](/docs/tutorials/key-val-store/part-5-managing-data-with-ttl-options)
- [Part 6: Multiple key spaces](/docs/tutorials/key-val-store/part-6-multiple-key-spaces)
- [Part 7: Interaction with the key-val store from the CLI](/docs/tutorials/key-val-store/part-7-key-value-store-interaction-from-cli)
:::
**API quick overview**
## Datastore.open()
Datastore.open() opens a connection to a datastore in the active project space.
```js
import {Datastore} from 'codehooks-js'
async function myfunc() {
const conn = await Datastore.open();
// use conn to call API functions
...
}
```
**Returns** a Promise with a Datastore connection
## set(key, value, options)
Set a key-value **string** pair in a datastore.
**Parameters**
- **key**: Unique key in keyspace
- **value**: Any string value
- **options**: (optional)
- **ttl**: Time to live in milliseconds
- **keyspace**: Name of isolated keyspace
**Returns** A Promise with result string value
**Code example using a string value**
```js
const ONE_DAY = 86400000; // 1000 * 60 * 60 * 24
const conn = await Datastore.open();
const opt = {
ttl: ONE_DAY,
keyspace: 'sessions',
};
const result = await conn.set(
'my_special_key',
'f439a2d4-d21b-44ea-97ac-f210120f10f3',
opt
);
```
## setObj(key, value, options)
Set a key-value **string / Object** pair in a datastore.
**Parameters**
- **key**: Unique key in keyspace
- **value**: Any JSON Object value
- **options**: (optional)
- **ttl**: Time to live in milliseconds
- **keyspace**: Name of isolated keyspace
**Returns** A Promise with result string value
**Code example using a JSON Object**
```js
const ONE_DAY = 86400000; // 1000 * 60 * 60 * 24
const conn = await Datastore.open();
const opt = {
ttl: ONE_DAY,
keyspace: 'sessions',
};
const result = await conn.setObj(
'my_special_key',
{
session_id: 'f439a2d4-d21b-44ea-97ac-f210120f10f3',
user_id: '1234567890',
},
opt
);
```
## get(key, options)
Get a key-value **string** pair in a datastore.
**Parameters**
- **key**: Unique key in keyspace
- **options**: (optional)
- **keyspace**: Name of isolated keyspace
**Returns** A Promise with result string value
**Code example using a string value**
```js
const conn = await Datastore.open();
const opt = {
keyspace: 'sessions',
};
const kval = await conn.get('my_special_key', opt);
console.log('key-val', kval);
```
## getObj(key, options)
Get a key-value **string / Object** pair in a datastore.
**Parameters**
- **key**: Unique key in keyspace
- **options**: (optional)
- **keyspace**: Name of isolated keyspace
**Returns** A Promise with result JSON Object
**Code example using a JSON Object**
```js
const conn = await Datastore.open();
const opt = {
keyspace: 'sessions',
};
const { session_id, user_id } = await conn.getObj('my_special_key', opt);
console.log('key-val', session_id, user_id);
```
## getAll(keypattern, options)
Get all key-value pair that matches keypattern in a datastore.
**Parameters**
- **keypattern**: Key pattern matches key starting with **string**
- **options**: (optional)
- **keyspace**: Name of isolated keyspace
Returns **Promise / JSON object stream**
**Code example**
```js
const conn = await Datastore.open();
const opt = {
keyspace: 'sessions',
};
const stream = await conn.getAll('my_special_', opt);
stream
.on('data', (data) => {
console.log(data);
})
.on('end', () => {
console.log('The end');
});
```
## incr(key, number, options)
Increment a key-value pair in a datastore.
**Parameters**
- **key**: Unique key in keyspace
- **number**: Value to increment >= 1
- **options**: (optional)
- **ttl**: Time to live in milliseconds
- **keyspace**: Name of isolated keyspace
**Returns** A Promise with result string value
**Code example**
```js
const conn = await Datastore.open();
const kval = await conn.incr('counter', 1);
console.log('key-val', kval);
```
## decr(key, number, options)
Decrement a key-value pair in a datastore.
**Parameters**
- **key**: Unique key in keyspace
- **number**: Value to decrement <= 1
- **options**: (optional)
- **ttl**: Time to live in milliseconds
- **keyspace**: Name of isolated keyspace
**Returns** A Promise with result string value
**Code example**
```js
const conn = await Datastore.open();
const kval = await conn.decr('counter', 1);
console.log('key-val', kval);
```
## del(key, options)
Delete a key-value pair in a datastore.
**Parameters**
- **key**: Unique key in keyspace
- **options**: (optional)
- **keyspace**: Name of isolated keyspace
**Returns** A Promise with result string value
**Code example**
```js
const conn = await Datastore.open();
const kval = await conn.del('my_special_key');
console.log('key-val', kval);
```
## delAll(key, options)
Delete a range of key-value pairs in a datastore.
**Parameters**
- **keypattern**: Key pattern in keyspace that matches start of **string**
- **options**: (optional)
- **keyspace**: Name of isolated keyspace
**Returns** A Promise with result JSON object
**Code example**
```js
const conn = await Datastore.open();
const kval = await conn.delAll('my_special_');
console.log('key-val', kval);
```
---
## Database API
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
The Database API provides a powerful and flexible interface for interacting with the built-in NoSQL datastore in your Codehooks project. This API allows you to perform a wide range of operations, from basic CRUD (Create, Read, Update, Delete) to more advanced querying and data manipulation.
## Key Features
- **NoSQL Flexibility**: Work with schema-less data structures for maximum adaptability.
- **CRUD Operations**: Easily insert, retrieve, update, and delete documents.
- **Advanced Querying**: Use MongoDB-style query language for complex data retrieval.
- **Data Streaming**: Efficiently handle large datasets with streaming capabilities.
- **Schema Validation**: Optionally enforce data structure with JSON Schema.
- **Collection Management**: Programmatically create and manage database collections.
## Getting Started
To use the Database API, you'll first need to open a connection to the datastore:
```js
import { Datastore } from 'codehooks-js';
async function myDatabaseFunction() {
const conn = await Datastore.open();
// Use conn to interact with the database
// ...
}
```
Once you have a connection, you can use various methods to interact with your data. Here's a quick overview of some common operations:
- `insertOne()`: Add a new document to a collection
- `getOne()`: Retrieve a single document by ID or query
- `getMany()`: Retrieve multiple documents matching a query
- `updateOne()`: Modify a single document
- `removeOne()`: Delete a single document
For more detailed information on each method and advanced usage, refer to the specific sections below.
**API quick overview**
## Datastore.open()
Datastore.open() opens a connection to a datastore in the active project space.
```js
import {Datastore} from 'codehooks-js'
async function myfunc() {
const conn = await Datastore.open();
// use conn to call API functions
...
}
```
**Returns** Promise / Datastore connection
## insertOne(collection, document)
Insert a new data object into a collection in a datastore.
**Parameters**
- **collection**: collection name
- **document**: JSON document or array of JSON documents
**Returns:** Promise with the new JSON document and an unique `_id` value.
**Validation exceptions:** [See JSON-schema validation](#data-validation)
**Code example**
```js {6}
import { app, Datastore } from 'codehooks-js';
async function createSale(req, res) {
const conn = await Datastore.open();
try {
const doc = await conn.insertOne('sales', req.body);
// return new document to client
res.status(201).json(doc);
} catch (error) {
if (error.schemaError) {
// JSON-schema validation error
res.status(400).json(error);
} else {
// unknown error
res.status(500).json({ error: error.message });
}
}
}
// Serverless route hook
app.post('/sales', createSale);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
POST
https://myproject-ff00.api.codehooks.io/dev/sales
Example input:
{
"Region": "North America",
"Item Type": "Cereal",
"Volume": 200
}
Example output:
{
"_id": "640f60e2b4fdc00015c4280f",
"Region": "North America",
"Item Type": "Cereal",
"Volume": 200
}
```
## getOne(collection, ID | Query)
Get one data object by ID or by Query from a collection in a datastore.
**Parameters**
- **collection**: name of collection
- **ID** or **Query**: the `_id` value, or a Query `{value: "some value"}`
**Returns** Promise with json document
:::warning
Throws an exception if no document is found. Always wrap in a try-catch block or handle the rejection.
:::
**Code example**
```js {7}
import { app, Datastore } from 'codehooks-js';
async function getOne(req, res) {
try {
const conn = await Datastore.open();
const { id } = req.params;
const data = await conn.getOne('customer', id); // same as conn.getOne('customer', {_id: id})
res.json(data);
} catch (error) {
// not found
res.status(404).json({ error: error.message });
}
}
// Serverless route hook
app.get('/cust/:id', getOne);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
GET
https://myproject-ff00.api.codehooks.io/dev/cust/640f60f2b4fdc00015c42811
Example output:
{
"name": "Acme Inc.",
_id: "640f60f2b4fdc00015c42811"
}
```
## findOne(collection, ID | Query)
Alias for [getOne](#getonecollection-id--query)
## findOneOrNull(collection, ID | Query)
Get one data object by ID or by Query from a collection, returning `null` if not found instead of throwing an exception.
**Parameters**
- **collection**: name of collection
- **ID** or **Query**: the `_id` value, or a Query `{value: "some value"}`
**Returns** Promise with json document, or `null` if no document matches
:::tip Prefer this over findOne/getOne
This method is safer for checking existence since it doesn't require try-catch blocks. Use it when a missing document is a normal condition, not an error.
:::
**Code example**
```js {7-11}
import { app, Datastore } from 'codehooks-js';
async function getCustomer(req, res) {
const conn = await Datastore.open();
const { id } = req.params;
const customer = await conn.findOneOrNull('customer', id);
if (!customer) {
return res.status(404).json({ error: 'Customer not found' });
}
res.json(customer);
}
app.get('/cust/:id', getCustomer);
export default app.init();
```
## find(collection, query, options)
Alias for [getMany](#getmanycollection-query-options)
## getMany(collection, query, options)
Stream multiple data objects from a collection in a datastore.
Data are sorted naturally by the index for the collection, default index is by `_id` which will stream data in chronological order.
**Parameters**
- **collection**: collection name (mandatory)
- **query**: mongoDB query object, e.g. `{name: "Jane"}` [(Read query language docs)](nosql-database-query-language). Default query is empty `{}`, i.e. all objects
- **options** (optional settings)
- **sort**: mongoDB sort object. E.g. sort by name ascending `{"name": 1}`
- **limit**: how many items to return
- **hints**: include or omit fields
- Include fields example: `{$fields: {name: 1, address: 1}}`
- Omit fields example: `{$fields: {name: 0, _id_: 0}}`
- **offset**: how many items to skip before returning any
**Returns** a JSON array data stream.
### Simple code example
```js {5}
import { app, Datastore } from 'codehooks-js';
async function getSales(req, res) {
const conn = await Datastore.open();
conn.getMany('sales').json(res); // pipe json stream to client
}
// Serverless route hook
app.get('/sales', getSales);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
GET
https://myproject-ff00.api.codehooks.io/dev/sales
Example output:
[
{
"Region": "France",
"Item Type": "Veal",
"Volume": 130,
"Unit Price": 2005.7,
"Unit Cost": 11s7.11,
"_id": "640f60e2b4fdc00015c4280f"
},
{
"Region": "Germany",
"Item Type": "Haxe",
"Volume": 30,
"Unit Price": 30.4,
"Unit Cost": 32.43,
"_id": "640f60e2b4fdc00015c4282f"
}
]
```
### Advanced code example
```js {10}
import { app, Datastore } from 'codehooks-js';
async function getSales(req, res) {
const conn = await Datastore.open();
const query = { Volume: { $gt: req.params.vol } };
const options = {
limit: 1000,
hints: { $fields: { status: 0, _id: 0 } },
};
conn.getMany('sales', query, options).json(res); // json stream is piped back to client
}
// Serverless route hook
app.get('/sales/:vol', getSales);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
GET
https://myproject-ff00.api.codehooks.io/dev/sales/1000
Example output:
[
{
"Region": "North America",
"Item Type": "Cereal",
"Volume": 1304,
"Unit Price": 205.7,
"Unit Cost": 117.11
},
{
"Region": "North America",
"Item Type": "Cereal",
"Volume": 1306,
"Unit Price": 10.3,
"Unit Cost": 3.3
}
]
```
### Streaming data code example
With the steaming API you're in full control over how to deliver data back to a client.
In this example the `content-type` is set first, next the code is using `forEach` to iterate the stream to output one line of data per database object.
```js {8,10}
import { app, Datastore } from 'codehooks-js';
async function getSales(req, res) {
res.set('content-type', 'text/plain');
const conn = await Datastore.open();
let count = 0;
const cursor = conn.getMany('sales', { 'Item Type': 'Cereal' });
await cursor.forEach((item) => {
// use the response.write to stream data back to client
res.write(`Item ${count++} has volume ${item.Volume}\n`);
});
res.end();
}
// Serverless route hook
app.get('/sales', getSales);
export default app.init(); // Bind functions to the serverless runtime
```
Output from the streaming data code example:
```
Item 0 has volume 1304
Item 1 has volume 1306
...
```
## JSONL (JSON Lines) Data Format
JSONL (JSON Lines) is a convenient format for storing structured data that can be processed one record at a time. Each line is a valid JSON value, making it perfect for streaming large datasets efficiently.
### Why JSONL Matters for Scale and Speed
**Streaming Efficiency**: JSONL allows processing of large datasets without loading everything into memory at once. This is crucial for applications dealing with millions of records.
**Real-time Processing**: Each JSON object can be processed immediately as it's received, enabling real-time data pipelines and analytics.
**Memory Optimization**: Unlike traditional JSON arrays that require parsing the entire document, JSONL can be processed line-by-line, significantly reducing memory footprint.
**Parallel Processing**: JSONL files can be easily split and processed in parallel across multiple workers or nodes.
### AI/LLM/MCP Applications
The rise of Large Language Models (LLMs), AI applications, and Model Context Protocols (MCP) has made JSONL increasingly important:
- **Training Data**: LLMs often require massive datasets in JSONL format for fine-tuning and training
- **Inference Pipelines**: AI applications need to process large volumes of data for real-time inference
- **MCP Integration**: Model Context Protocols often expect streaming data formats for efficient data exchange
- **Vector Databases**: Many vector databases and embedding services work optimally with JSONL streams
- **Batch Processing**: AI workloads frequently involve processing large batches of documents or records
### JSONL Streaming Example
Here's how to stream data in JSONL format with the correct content-type header:
```js {8,10}
import { app, Datastore } from 'codehooks-js';
async function getSalesJSONL(req, res) {
// Set content-type for JSONL/NDJSON format
res.set('content-type', 'application/x-ndjson');
const conn = await Datastore.open();
const query = { Region: 'North America' };
const cursor = conn.getMany('sales', query);
// Stream each document as a separate JSON line
await cursor.forEach((item) => {
// Each line is a complete JSON object followed by newline
res.write(JSON.stringify(item) + '\n');
});
res.end();
}
// Serverless route hook
app.get('/sales-jsonl', getSalesJSONL);
export default app.init(); // Bind functions to the serverless runtime
```
**Example output**:
```json
{"Region": "North America", "Item Type": "Cereal", "Volume": 1304, "Unit Price": 205.7, "Unit Cost": 117.11, "_id": "640f60e2b4fdc00015c4280f"}
{"Region": "North America", "Item Type": "Beverages", "Volume": 856, "Unit Price": 10.3, "Unit Cost": 3.3, "_id": "640f60e2b4fdc00015c4282f"}
{"Region": "North America", "Item Type": "Snacks", "Volume": 2341, "Unit Price": 15.8, "Unit Cost": 8.2, "_id": "640f60e2b4fdc00015c4283f"}
```
### Advanced JSONL with Filtering and Transformation
For AI/LLM applications, you might need to transform or filter data before streaming:
```js {12,15}
import { app, Datastore } from 'codehooks-js';
async function getProcessedDataJSONL(req, res) {
res.set('content-type', 'application/x-ndjson');
const conn = await Datastore.open();
const query = { Volume: { $gt: 1000 } }; // Only high-volume items
const options = {
sort: { Volume: -1 }, // Sort by volume descending
hints: { $fields: { Region: 0, _id: 0 } }, // Exclude certain fields
};
const cursor = conn.getMany('sales', query, options);
await cursor.forEach((item) => {
// Transform data for AI processing
const processedItem = {
text: `${item['Item Type']} with volume ${item.Volume}`,
metadata: {
volume: item.Volume,
unitPrice: item['Unit Price'],
unitCost: item['Unit Cost'],
},
timestamp: new Date().toISOString(),
};
res.write(JSON.stringify(processedItem) + '\n');
});
res.end();
}
app.get('/processed-data-jsonl', getProcessedDataJSONL);
export default app.init();
```
This approach is particularly valuable for:
- **Data Preprocessing**: Preparing datasets for machine learning models
- **Feature Extraction**: Converting raw data into features suitable for AI processing
- **Real-time Analytics**: Streaming processed data to analytics dashboards
- **API Integration**: Providing data to external AI services and LLM APIs
## toArray(collection, query, options)
Utility function to return multiple data objects as an array of objects from the [getMany](#getmanycollection-query-options) API call.
**Simple code example**
```js {6}
import { app, Datastore } from 'codehooks-js';
async function getSales(req, res) {
const conn = await Datastore.open();
const query = { product: 'foo' }; // query for foo products
const array = await conn.getMany('sales', query).toArray(); // as array
// return json array
res.json(array);
}
// Serverless route hook
app.get('/sales', getSales);
export default app.init(); // Bind functions to the serverless runtime
```
## updateOne(collection, ID | Query, updateoperators, options)
Update one data object by ID or by Query in a datastore collection.
Input document is patched with the matched database object.
**Parameters**
- **collection**: name of collection
- **ID** or **Query**: the `_id` value, or a Query `{value: "some value"}`
- **updateoperators**: new json document to update or [manipulate data operators](dataoperators)
- **options**: (optional settings)
- **upsert**: Create a new document if ID does not exist by `{upsert: true}`
**Returns:** Promise with updated json document
**Validation exceptions:** [See JSON-schema validation](#data-validation)
### Code example: update by ID
```js {7}
import { app, Datastore } from 'codehooks-js';
async function myFunc(req, res) {
const { id } = req.params;
const { body } = req;
const conn = await Datastore.open();
const data = await conn.updateOne('customer', { _id: id }, { $set: body });
res.json(data);
}
// Serverless route hook
app.put('/cust/:id', myFunc);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
POST
https://myproject-ff00.api.codehooks.io/dev/cust/640f60f2b4fdc00015c42811
Example input:
{
"name": "Acme Inc.",
"sales": 42
}
```
### Code example: update using the upsert option
```js {17}
// upsert api route
app.post('/update', async (req, res) => {
const { email } = req.body;
console.log('upsert', email);
const conn = await datastore.open();
const upsertResult = await conn.updateOne(
'users',
// query
{
emailAddress: email,
},
// update operators
{
$set: { emailAddress: email },
$inc: { visits: 1 },
},
// upsert option
{ upsert: true }
);
res.json(upsertResult);
});
```
**Example input and result**
```js
POST
https://myproject-ff00.api.codehooks.io/dev/update
# example input:
{
"email": "jane@examle.com"
}
# example output:
{
"emailAddress": "jane@example.com",
"visits": 8,
"_id": "658af419d44110bef4e88beb"
}
```
## updateMany(collection, query, document, options)
Update multiple data objects in a datastore collection.
Input document is patched with the matched database objects.
**Parameters**
- **collection**: name of collection
- **query**: mongoDB query object [(docs)](nosql-database-query-language)
- **document**: new json document to update or [manipulate](dataoperators)
- **options** (optional settings)
**Returns:** Promise with count of objects updated
**Validation exceptions:** [See JSON-schema validation](#data-validation)
**Code example**
```js {12}
import { app, Datastore } from 'codehooks-js';
async function myFunc(req, res) {
const conn = await Datastore.open();
const query = {
customerStatus: 'GOLD',
};
const doc = {
$inc: { bonus: '20' },
$set: { customerStatus: 'PLATINUM' },
};
const data = await conn.updateMany('customer', query, doc);
res.json(data);
}
// Serverless route hook
app.put('/cust', myFunc);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
PUT
https://myproject-ff00.api.codehooks.io/dev/cust
Example output:
[
{
"name": "Acme Inc.",
"customerStatus": "PLATINUM",
"bonus": "20",
_id: "640f60f2b4fdc00015c42811"
},
{
"name": "Colo Inc.",
"customerStatus": "PLATINUM",
"bonus": "30",
_id: "640f60f2b4fdc00015c42f00"
},
...
]
```
## replaceOne(collection, ID | Query, document, options)
Replace one data object by ID or by Query in a datastore collection.
Input document overwrites the matched database object, the `_id` value is not overwritten.
**Parameters**
- **collection**: name of collection
- **ID** or **Query**: the `_id` value, or a Query `{value: "some value"}`
- **document**: new json document to replace
- **options**: (optional settings)
- **upsert**: Create a new document if ID does not exist by `{upsert: true}`
**Returns** Promise with updated json document
**Validation exceptions:** [See JSON-schema validation](#data-validation)
**Code example**
```js {5}
import { app, Datastore } from 'codehooks-js';
async function myFunc(req, res) {
const conn = await Datastore.open();
const data = await conn.replaceOne(
'customer',
{ _id: req.params.id },
req.body
);
res.json(data);
}
// Serverless route hook
app.put('/cust/:id', myFunc);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
POST
https://myproject-ff00.api.codehooks.io/dev/cust/640f60f2b4fdc00015c42811
Example input:
{
"name": "Acme Replacement Inc.",
"sales": 0
}
```
## replaceMany(collection, query, document, options)
Replace multiple data objects in a datastore collection.
Input document overwrites the matched database objects. The `_id` value is not overwritten.
**Parameters**
- **collection**: name of collection
- **query**: mongoDB query object [(docs)](nosql-database-query-language)
- **document**: new json document to replace
- **options** (optional settings)
**Returns** Promise with count of objects updated
**Validation exceptions:** [See JSON-schema validation](#data-validation)
**Code example**
```js {11}
import { app, Datastore } from 'codehooks-js';
async function myFunc(req, res) {
const conn = await Datastore.open();
const query = {
customerStatus: 'GOLD',
};
const doc = {
Acme: 'was here',
};
const data = await conn.replaceMany('customer', query, doc);
res.json(data);
}
// Serverless route hook
app.put('/cust', myFunc);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
PUT
https://myproject-ff00.api.codehooks.io/dev/cust
Example output:
[
{
"Acme": "was here"
_id: "640f60f2b4fdc00015c42811"
},
{
"Acme": "was here"
_id: "640f60f2b4fdc00015c400fe"
},
...
]
```
## removeOne(collection, ID | Query)
Remove one data object by ID or by Query in a datastore collection.
**Parameters**
- **collection**: name of collection
- **ID** or **Query**: the `_id` value, or a Query `{value: "some value"}`
**Returns** Promise with document ID
**Code example**
```js {5}
import { app, Datastore } from 'codehooks-js';
async function myFunc(req, res) {
const conn = await Datastore.open();
const data = await conn.removeOne('customer', { _id: req.params.id });
res.json(data);
}
// Serverless route hook
app.delete('/cust/:id', myFunc);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
DELETE
https://myproject-ff00.api.codehooks.io/dev/cust/640f60f2b4fdc00015c42811
Example output:
640f60f2b4fdc00015c42811
```
## removeMany(collection, query, options)
Remove multiple data objects in a datastore collection.
**Parameters**
- **collection**: name of collection
- **query**: mongoDB query object [(docs)](nosql-database-query-language)
- **options** (optional settings)
**Returns** Promise with count of objects deleted
**Code example**
```js {8}
import { app, Datastore } from 'codehooks-js';
async function myFunc(req, res) {
const conn = await Datastore.open();
const query = {
customerStatus: 'GOLD',
};
const data = await conn.removeMany('customer', query);
res.json(data);
}
// Serverless route hook
app.delete('/cust', myFunc);
export default app.init(); // Bind functions to the serverless runtime
```
**Example input and result**
```js
DELETE
https://myproject-ff00.api.codehooks.io/dev/cust
Example output:
{"count": 3}
```
## Data validation
Add data validation (JSON-schema) to any database collection directly to the database collection using the [CLI command](/docs/cli#add-schema) `add-schema` or the Codehooks Studio application navigating to Collection/Settings/JSON-Schema.

### Example: Defining a JSON Schema using the CLI
To ensure your data is consistent and secure, you can define a [JSON Schema](https://json-schema.org/) for your collections. This schema will validate the data structure and enforce data types.
Create a schema file (e.g., `personSchema.json`) defining the structure of your data:
```js
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"firstName": {
"type": "string",
"description": "The person's first name."
},
"lastName": {
"type": "string",
"description": "The person's last name."
},
"age": {
"type": "integer",
"description": "The person's age.",
"minimum": 0
},
"email": {
"type": "string",
"format": "email",
"description": "The person's email address."
},
},
"required": [
"firstName",
"lastName",
"email"
]
}
```
Use the CLI to add this JSON Schema to your collection:
```bash
codehooks add-schema --collection 'person' --schema './personSchema.json'
```
### JSON-Schema validation errors and status code
In the event that clients sends data not following the JSON-Schema, a validator error will be thrown.
E.g. when the API posts a JSON object missing a required field a validator `schemaError` error array of objects will be returned.
```js
{
"schemaError": [
{
"instancePath": "",
"schemaPath": "#/required",
"keyword": "required",
"params": {
"missingProperty": "firstName"
},
"message": "must have required property 'firstName'"
}
]
}
```
### Data validation JSON schema API
The following API methods allow you to programmatically manage JSON schemas for your collections, providing an alternative to using the CLI or Codehooks Studio for schema management.
#### setSchema(collection, schema)
Set a JSON schema for a collection to enforce data validation.
**Parameters**
- **collection**: name of the collection
- **schema**: JSON schema object
**Returns** Promise with the result of the operation
**Code example**
```js
import { app, Datastore } from 'codehooks-js';
import { mySchema } from './schema.json';
async function setCollectionSchema(collection) {
const conn = await Datastore.open();
const result = await conn.setSchema(collection, mySchema);
return result;
}
export default app.init(async () => {
try {
const result = await setCollectionSchema('person');
console.log('Schema set successfully:', result);
} catch (error) {
console.error('Error setting schema:', error.message);
}
});
```
#### getSchema(collection)
Retrieve the JSON schema for a collection.
**Parameters**
- **collection**: name of the collection
**Returns** Promise with the JSON schema or null if not set
**Code example**
```js
import { app, Datastore } from 'codehooks-js';
async function getCollectionSchema(req, res) {
const conn = await Datastore.open();
const { collection } = req.params;
try {
const schema = await conn.getSchema(collection);
res.json(schema);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
app.get('/get-schema/:collection', getCollectionSchema);
export default app.init();
```
#### removeSchema(collection)
Remove the JSON schema from a collection.
**Parameters**
- **collection**: name of the collection
**Returns** Promise with the result of the operation
**Code example**
```js
import { app, Datastore } from 'codehooks-js';
async function removeCollectionSchema(req, res) {
const conn = await Datastore.open();
const { collection } = req.params;
try {
const result = await conn.removeSchema(collection);
res.json(result);
} catch (error) {
res.status(400).json({ error: error.message });
}
}
app.delete('/remove-schema/:collection', removeCollectionSchema);
export default app.init();
```
## Collection Management
The database API provides methods to create and drop collections, allowing you to manage the structure of your database.
### createCollection(collection, options)
Creates a new collection in the database.
**Parameters**
- **collection**: (String) The name of the collection to create.
- **options**: (Object, optional) Additional options for creating the collection.
- **cap**: (Number, optional) Maximum number of documents in the collection.
- **capdelay**: (Number, optional) Milliseconds to wait before capping the collection.
- **schema**: (Object, optional) JSON Schema for validating documents in the collection. Must follow the [JSON Schema standard](https://json-schema.org/).
**Returns** Promise that resolves with the result of the operation.
**Simple code example**
```js
import { Datastore } from 'codehooks-js';
async function createSimpleCollection() {
const conn = await Datastore.open();
try {
const result = await conn.createCollection('simpleCollection');
console.log('Collection created:', result);
} catch (error) {
console.error('Error creating collection:', error);
}
}
```
**Advanced example with schema and cap options**
```js
import { Datastore } from 'codehooks-js';
async function createAdvancedCollection() {
const conn = await Datastore.open();
try {
const options = {
cap: 100000,
schema: {
$schema: 'http://json-schema.org/draft-07/schema#',
title: 'IoTLog',
type: 'object',
properties: {
deviceId: {
type: 'string',
description: 'Unique identifier for the IoT device',
},
timestamp: {
type: 'string',
format: 'date-time',
description: 'Timestamp of the log entry',
},
temperature: {
type: 'number',
description: 'Temperature reading from the device',
},
humidity: {
type: 'number',
description: 'Humidity reading from the device',
},
batteryLevel: {
type: 'number',
minimum: 0,
maximum: 100,
description: 'Battery level of the device',
},
},
required: [
'deviceId',
'timestamp',
'temperature',
'humidity',
'batteryLevel',
],
},
};
await conn.createCollection('iotLogs', options);
console.log('IoT Log collection created successfully');
} catch (error) {
console.error('Error creating IoT Log collection:', error);
}
}
```
These examples demonstrate how to create a simple collection without any options, and how to create an advanced collection with a schema
### dropCollection(collection)
Removes an existing collection from the database.
**Parameters**
- **collection**: (String) The name of the collection to drop.
**Returns** Promise that resolves with the result of the operation.
**Code example**
```js
import { Datastore } from 'codehooks-js';
async function removeCollection() {
const conn = await Datastore.open();
try {
const result = await conn.dropCollection('oldCollection');
console.log('Collection dropped:', result);
} catch (error) {
console.error('Error dropping collection:', error);
}
}
```
These methods allow you to programmatically manage the collections in your database. Use `createCollection` to add new collections and `dropCollection` to remove existing ones. Be cautious when using `dropCollection`, as it will permanently delete the collection and all its data.
---
## OpenAPI Documentation
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
Codehooks provides built-in [OpenAPI 3.0](https://www.openapis.org/) documentation that automatically generates interactive API docs from your code. When combined with `crudlify`, your schemas are automatically converted to OpenAPI specifications and served via [Swagger UI](https://swagger.io/tools/swagger-ui/).
After deployment, visit:
- **`/docs`** — Interactive Swagger UI (supports authentication with API tokens)
- **`/openapi.json`** — Raw OpenAPI 3.0 specification
---
## app.openapi(config, swaggerPath?)
Enable automatic OpenAPI 3.0 documentation generation and serve an interactive Swagger UI.
**Parameters**
- **config** (object): OpenAPI configuration object
- **info** (object, required): API metadata
- **title** (string): API title
- **version** (string): API version
- **description** (string, optional): API description
- **servers** (array, optional): Server URLs for different environments
- **tags** (array, optional): Tags to organize endpoints
- **filter** (function, optional): Filter which operations appear in docs `(op) => boolean`
- **specPath** (string, optional): Path for the JSON spec (default: `/openapi.json`)
- **externalDocs** (object, optional): Link to external documentation
- **security** (array, optional): Security schemes
- **components** (object, optional): Additional component schemas
- **swaggerPath** (string, optional): Path for Swagger UI (default: `/docs`)
**Returns** void
**Code example**
```javascript
import { app } from 'codehooks-js';
import { z } from 'zod';
const todoSchema = z.object({
title: z.string().min(1).max(100),
completed: z.boolean().default(false)
});
// Enable OpenAPI docs
app.openapi({
info: { title: 'Todo API', version: '1.0.0' }
});
// Auto-generate CRUD endpoints with schema validation
app.crudlify({ todos: todoSchema });
export default app.init();
```
---
## openapi(spec) middleware
Add OpenAPI metadata to custom routes. Use this middleware to document individual endpoints that are not auto-generated by `crudlify`.
**Parameters**
- **spec** (object): OpenAPI operation specification
- **summary** (string): Short description of the endpoint
- **description** (string, optional): Detailed description
- **tags** (array): Tags to group the endpoint
- **requestBody** (object, optional): Request body schema (supports Zod/Yup schemas directly)
- **parameters** (array, optional): Query/path parameter definitions
- **responses** (object): Response definitions by status code
- **security** (array, optional): Security requirements (use `[]` for public endpoints)
**Returns** Middleware function
**Code example**
```javascript
import { app, openapi } from 'codehooks-js';
app.openapi({
info: { title: 'My API', version: '1.0.0' }
});
app.get('/health',
openapi({
summary: 'Health check',
tags: ['System'],
responses: {
200: { description: 'Service is healthy' }
}
}),
(req, res) => res.json({ status: 'ok' })
);
export default app.init();
```
---
## Configuration Options
Full configuration example with all available options:
```javascript
app.openapi({
// API metadata (required)
info: {
title: 'My API',
version: '1.0.0',
description: 'API description'
},
// Custom path for the spec (default: '/openapi.json')
specPath: '/openapi.json',
// Server URLs
servers: [
{ url: 'https://myproject.api.codehooks.io/dev', description: 'Development' },
{ url: 'https://myproject.api.codehooks.io/prod', description: 'Production' }
],
// Filter which operations appear in docs
filter: (op) => op.method !== 'delete',
// Global tags
tags: [
{ name: 'Todos', description: 'Todo operations' }
],
// External documentation link
externalDocs: {
description: 'Full documentation',
url: 'https://docs.example.com'
},
// Security schemes
security: [{ apiKey: [] }],
// Additional component schemas
components: {
schemas: {
Error: {
type: 'object',
properties: {
message: { type: 'string' }
}
}
}
}
}, '/docs'); // Second parameter: Swagger UI path (default: '/docs')
```
---
## Filter Function
Use the `filter` function to control which operations appear in the documentation.
**Parameters passed to filter**
| Property | Type | Description |
|----------|------|-------------|
| `op.method` | string | HTTP method: `'get'`, `'post'`, `'put'`, `'patch'`, `'delete'` |
| `op.path` | string | Route path: `'/todos'`, `'/todos/{ID}'` |
| `op.tags` | array | Array of tags: `['Todos']` |
| `op.summary` | string | Operation summary |
| `op.operationId` | string | Operation ID |
**Code examples**
```javascript
// Exclude DELETE operations
filter: (op) => op.method !== 'delete'
// Exclude batch operations
filter: (op) => !op.path.includes('_byquery')
// Exclude internal routes
filter: (op) => !op.path.startsWith('/internal')
// Only include specific tags
filter: (op) => op.tags.includes('Public')
// Combine conditions
filter: (op) => {
if (op.method === 'delete') return false;
if (op.path.startsWith('/admin')) return false;
return true;
}
```
---
## Schema Support
OpenAPI documentation automatically converts schemas from [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), or [JSON Schema](https://json-schema.org/).
### Zod
```javascript
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().min(0).max(150).optional(),
role: z.enum(['admin', 'user']).default('user'),
active: z.boolean().default(true)
});
app.crudlify({ users: userSchema });
```
**Generated OpenAPI schema:**
```json
{
"type": "object",
"properties": {
"name": { "type": "string", "minLength": 1, "maxLength": 100 },
"email": { "type": "string", "format": "email" },
"age": { "type": "number", "minimum": 0, "maximum": 150 },
"role": { "type": "string", "enum": ["admin", "user"], "default": "user" },
"active": { "type": "boolean", "default": true }
},
"required": ["name", "email"]
}
```
### Yup
```javascript
import * as yup from 'yup';
const userSchema = yup.object({
name: yup.string().min(1).max(100).required(),
email: yup.string().email().required(),
age: yup.number().min(0).max(150),
role: yup.string().oneOf(['admin', 'user']).default('user'),
active: yup.boolean().default(true)
});
app.crudlify({ users: userSchema });
```
### JSON Schema
JSON Schema validation works out of the box using ajv (available in Codehooks runtime).
```javascript
const userSchema = {
type: 'object',
required: ['name', 'email'],
properties: {
name: {
type: 'string',
minLength: 1,
maxLength: 100,
description: 'Full name of the user'
},
email: {
type: 'string',
format: 'email',
description: 'Email address'
},
age: {
type: 'integer',
minimum: 0,
maximum: 150,
description: 'Age in years'
},
active: {
type: 'boolean',
default: true,
description: 'Account status'
}
}
};
app.crudlify({ users: userSchema }, { schema: 'json-schema' });
```
---
## Adding Field Descriptions
Add descriptions to schema fields for better API documentation.
### Zod — Use `.describe()`
```javascript
import { z } from 'zod';
const todoSchema = z.object({
title: z.string()
.min(1)
.max(100)
.describe('Title of the todo item'),
completed: z.boolean()
.default(false)
.describe('Whether the todo is completed'),
priority: z.number()
.min(1)
.max(5)
.optional()
.describe('Priority level from 1 (low) to 5 (high)')
});
```
### Yup — Use `.meta()`
```javascript
import * as yup from 'yup';
const todoSchema = yup.object({
title: yup.string()
.min(1)
.max(100)
.required()
.meta({ description: 'Title of the todo item' }),
completed: yup.boolean()
.default(false)
.meta({ description: 'Whether the todo is completed' }),
priority: yup.number()
.min(1)
.max(5)
.meta({ description: 'Priority level from 1 (low) to 5 (high)' })
});
```
---
## Crudlify Integration
When using `crudlify`, OpenAPI documentation is automatically generated for all CRUD operations:
| Method | Path | Operation |
|--------|------|-----------|
| POST | `/{collection}` | Create document |
| GET | `/{collection}` | List documents |
| GET | `/{collection}/{ID}` | Get document by ID |
| PUT | `/{collection}/{ID}` | Replace document |
| PATCH | `/{collection}/{ID}` | Update document |
| DELETE | `/{collection}/{ID}` | Delete document |
| PATCH | `/{collection}/_byquery` | Batch update |
| DELETE | `/{collection}/_byquery` | Batch delete |
**Multiple collections**
```javascript
app.crudlify({
users: userSchema,
posts: postSchema,
comments: commentSchema
});
```
**Schema-less collections**
Collections without a schema accept any valid JSON:
```javascript
app.crudlify({
users: userSchema, // Validated
logs: null // Accepts any JSON
});
```
---
## Documenting Custom Routes
Use the `openapi()` middleware to document custom routes with full OpenAPI specifications.
**Basic example**
```javascript
import { app, openapi } from 'codehooks-js';
app.get('/health',
openapi({
summary: 'Health check',
tags: ['System'],
responses: {
200: { description: 'Service is healthy' }
}
}),
(req, res) => res.json({ status: 'ok' })
);
```
**With request body**
```javascript
app.post('/upload',
openapi({
summary: 'Upload file',
tags: ['Files'],
requestBody: {
required: true,
content: {
'multipart/form-data': {
schema: {
type: 'object',
properties: {
file: { type: 'string', format: 'binary' }
}
}
}
}
},
responses: {
201: { description: 'File uploaded' },
400: { description: 'Invalid file' }
}
}),
async (req, res) => {
// Handle upload
}
);
```
**With query parameters**
```javascript
app.get('/users',
openapi({
summary: 'List users',
tags: ['Users'],
parameters: [
{ name: 'role', in: 'query', schema: { type: 'string', enum: ['admin', 'user'] } },
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } }
],
responses: {
200: { description: 'List of users' }
}
}),
async (req, res) => {
res.json([]);
}
);
```
**Using Zod schema in requestBody**
```javascript
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email()
});
app.post('/users',
openapi({
summary: 'Create user',
tags: ['Users'],
requestBody: {
required: true,
content: {
'application/json': {
schema: UserSchema // Zod schema auto-converts to JSON Schema
}
}
},
responses: {
201: { description: 'User created' },
400: { description: 'Validation error' }
}
}),
async (req, res) => {
res.status(201).json(req.body);
}
);
```
---
## Authentication
By default, all endpoints require the `x-apikey` header. The Swagger UI includes an authorize dialog where you can enter your API token to test authenticated endpoints.
**Mark endpoint as public**
```javascript
app.get('/public-endpoint',
openapi({
summary: 'Public endpoint',
security: [] // No authentication required
}),
(req, res) => res.json({ message: 'Hello!' })
);
// Also bypass auth middleware
app.auth('/public-endpoint', (req, res, next) => next());
```
---
## Swagger UI Path
The Swagger UI path defaults to `/docs`. You can customize it:
```javascript
app.openapi({
info: { title: 'My API', version: '1.0.0' }
}, '/api-docs'); // Swagger UI at /api-docs
```
---
## Complete Example
Full example with multiple collections, custom endpoints, validation middleware, and OpenAPI configuration:
```javascript
import { app, Datastore, openapi } from 'codehooks-js';
import { z } from 'zod';
// ============================================
// Schema Definitions
// ============================================
const UserSchema = z.object({
name: z.string()
.min(1, 'Name is required')
.max(100, 'Name too long')
.describe('Full name of the user'),
email: z.string()
.email('Invalid email format')
.describe('Email address (must be unique)'),
age: z.number()
.int('Age must be an integer')
.min(0, 'Age cannot be negative')
.max(150, 'Age seems unrealistic')
.optional()
.describe('Age in years'),
role: z.enum(['admin', 'user', 'guest'])
.default('user')
.describe('User role for access control')
});
const UpdateUserSchema = UserSchema.partial();
// ============================================
// Validation Middleware
// ============================================
function validate(schema) {
return async (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error.issues) {
return res.status(400).json({
error: 'Validation failed',
details: error.issues.map(e => ({
field: e.path.join('.'),
message: e.message
}))
});
}
return res.status(400).json({ error: 'Invalid request body' });
}
};
}
// ============================================
// OpenAPI Configuration
// ============================================
app.openapi({
info: {
title: 'User Management API',
version: '1.0.0',
description: 'API with Zod validation and auto-generated OpenAPI docs'
},
servers: [
{ url: 'https://myapi.api.codehooks.io/dev', description: 'Development' },
{ url: 'https://myapi.api.codehooks.io/prod', description: 'Production' }
],
tags: [
{ name: 'Users', description: 'User management operations' },
{ name: 'System', description: 'System endpoints' }
],
filter: (op) => !op.path.includes('_byquery')
});
// ============================================
// Custom Routes
// ============================================
// Health check (public)
app.get('/health',
openapi({
summary: 'Health check',
tags: ['System'],
security: [],
responses: {
200: {
description: 'Service status',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
status: { type: 'string', example: 'ok' },
timestamp: { type: 'string', format: 'date-time' }
}
}
}
}
}
}
}),
(req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
}
);
app.auth('/health', (req, res, next) => next());
// Create user
app.post('/users',
openapi({
summary: 'Create a new user',
description: 'Email must be unique. Returns 409 if email exists.',
tags: ['Users'],
requestBody: {
required: true,
content: {
'application/json': {
schema: UserSchema
}
}
},
responses: {
201: { description: 'User created successfully' },
400: { description: 'Validation error' },
409: { description: 'Email already exists' }
}
}),
validate(UserSchema),
async (req, res) => {
const db = await Datastore.open();
const existing = await db.findOneOrNull('users', { email: req.body.email });
if (existing) {
return res.status(409).json({ error: 'Email already exists' });
}
const user = await db.insertOne('users', {
...req.body,
createdAt: new Date().toISOString()
});
res.status(201).json(user);
}
);
// Get user by ID
app.get('/users/:id',
openapi({
summary: 'Get user by ID',
tags: ['Users'],
responses: {
200: { description: 'User found' },
404: { description: 'User not found' }
}
}),
async (req, res) => {
const db = await Datastore.open();
const user = await db.findOneOrNull('users', req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
}
);
// Update user
app.patch('/users/:id',
openapi({
summary: 'Update user',
tags: ['Users'],
requestBody: {
required: true,
content: {
'application/json': {
schema: UpdateUserSchema
}
}
},
responses: {
200: { description: 'User updated' },
400: { description: 'Validation error' },
404: { description: 'User not found' }
}
}),
validate(UpdateUserSchema),
async (req, res) => {
const db = await Datastore.open();
const exists = await db.findOneOrNull('users', req.params.id);
if (!exists) {
return res.status(404).json({ error: 'User not found' });
}
const user = await db.updateOne('users', req.params.id, {
...req.body,
updatedAt: new Date().toISOString()
});
res.json(user);
}
);
// Delete user
app.delete('/users/:id',
openapi({
summary: 'Delete user',
tags: ['Users'],
responses: {
200: { description: 'User deleted' },
404: { description: 'User not found' }
}
}),
async (req, res) => {
const db = await Datastore.open();
const exists = await db.findOneOrNull('users', req.params.id);
if (!exists) {
return res.status(404).json({ error: 'User not found' });
}
await db.removeOne('users', req.params.id);
res.json({ message: 'User deleted', _id: req.params.id });
}
);
// List users
app.get('/users',
openapi({
summary: 'List users',
tags: ['Users'],
parameters: [
{ name: 'role', in: 'query', schema: { type: 'string', enum: ['admin', 'user', 'guest'] } },
{ name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } }
],
responses: {
200: { description: 'List of users' }
}
}),
async (req, res) => {
const db = await Datastore.open();
const query = req.query.role ? { role: req.query.role } : {};
const users = await db.getMany('users', query, {
limit: parseInt(req.query.limit) || 20
}).toArray();
res.json(users);
}
);
export default app.init();
```
---
## Quick Reference
| Concept | Implementation |
|---------|----------------|
| Enable OpenAPI | `app.openapi({ info: { title, version } })` |
| Custom Swagger path | `app.openapi(config, '/api-docs')` |
| Document custom route | `app.get('/path', openapi({ ... }), handler)` |
| Use Zod schema | Pass schema directly to `requestBody.content['application/json'].schema` |
| Add field descriptions | Zod: `.describe()`, Yup: `.meta({ description })` |
| Public endpoint | `security: []` in openapi spec |
| Filter operations | `filter: (op) => op.method !== 'delete'` |
## Response Codes
| Code | When Used |
|------|-----------|
| 200 | Successful GET, PATCH, DELETE |
| 201 | Successful POST (created) |
| 400 | Validation failed |
| 404 | Resource not found |
| 409 | Duplicate resource (conflict) |
---
## ChatGPT & LLM Prompt
# ChatGPT & LLM Prompt
The Codehooks development prompt is a starter template for generating backend APIs using ChatGPT, Claude, Gemini, and other LLMs. Copy and paste it into your LLM of choice, add your request at the bottom, and get working Codehooks.io JavaScript code.
## Quick access
```bash
# Install the Codehooks CLI
npm install -g codehooks
# Display the prompt — pipe to your agent or copy to clipboard
coho prompt
# Copy to clipboard on macOS
coho prompt | pbcopy
```
## Full AI Agent Setup Guide
For the complete guide — including the Claude Code plugin, MCP server, CLI integration, OpenClaw skill, and the full development prompt — see **[AI Agent Setup](/docs/ai-agent-setup)**.
---
## NoSQL Query Tutorial: Examples, Syntax, and Operators Guide
*(Note: This content contains MDX/JSX code)*
**What is a NoSQL query?** A NoSQL query is how you retrieve and filter data from a NoSQL database. This NoSQL query tutorial teaches you the syntax, operators, and practical examples you need to query NoSQL databases effectively.
The Codehooks.io NoSQL database uses a subset of the popular [MongoDB](https://mongodb.com) query syntax. Whether you're new to NoSQL queries or migrating from SQL, this guide covers everything from basic NoSQL query examples to advanced operators.
:::tip REST API query
The NoSQL query language can be used directly for REST API queries when you use the 'crudlify' API. Read more about using REST API queries [on this page](database-rest-api).
:::
## NoSQL Query Example: How to Query NoSQL Database
Here's a complete NoSQL code example showing how to query a NoSQL database. This serverless JavaScript function creates a REST API that runs a NoSQL query to fetch 100 items from the `customers` collection where the `status` equals `GOLD`.
```js {5}
import { app, Datastore } from 'codehooks-js';
async function getData(req, res) {
const conn = await Datastore.open();
const query = { status: 'GOLD' };
const options = {
limit: 100,
};
conn.getMany('customers', query, options).json(res);
}
// Serverless REST API and query route
app.get('/customers', getData);
export default app.init(); // Bind functions to the serverless runtime
```
:::note
All query fields are case sensitive.
:::
## NoSQL Query Syntax: Filtering Data 🔎
NoSQL queries use a combination of filters, logical operators, and conditional operators. Here's the complete NoSQL syntax reference:
### Quick overview
| Operator | Description | Example |
| ----------------------------------- | :------------------------------------ | ------------------------------------------ |
| field | Match a single field value | `{"field": "value"}` |
| fields | Match multiple fields and values | `{"field1": "value1", "field2": "value2"}` |
| [$regex](#regex-operator) | Match field with a regular expression | `{"field" : {$regex : "^foo"}}` |
| [$startsWith](#startswith-operator) | Match field with start string segment | `{"field": {"$startsWith": "value"}}` |
| [$endssWith](#endswith-operator) | Match field with end string segment | `{"field": {"$endsWith": "value"}}` |
### Match multiple fields
Multiple fields are matched by name-value pairs in a query:
```js
const query = { field1: 'value1', field2: 'value2' };
```
This is actually the same as using the `$and` operator:
```js
const query = { $and: [{ field1: 'value1' }, { field2: 'value2' }] };
```
### Match sub fields
Sub fields are matched by dot.property in the URL query parameter:
```js
const query = { 'field1.property': 'value1' };
```
If your sub property is an array, you must use the [$elemMatch](#elemmatch-operator) operator.
### **$regex** operator
Match a [regular expression](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet) against a field. Optional `$options` values [docs.](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#advanced_searching_with_flags)
```js
const query = {"name" : {$regex : "^joe", $options: "i"}}
// or with native JS Regex
const query = {"name" : /^joe/}
```
### **$startsWith** operator
Field is matched by starting string of value:
```js
const query = { Player: { $startsWith: 'Lionel' } };
```
### **$endsWith** operator
Field is matched by ending string of value:
```js
const query = { Player: { $endsWith: 'Messi' } };
```
## NoSQL Operators: Logical Operators
### Quick overview
| Operator | Description | Example |
| --------------------- | :--------------------------- | --------------------------------------------------- |
| [$not](#not-operator) | Negation logical operator | `{"field" : {$not : val}}` |
| [$in](#in-operator) | Match any value in array | `{"field" : {$in : [value1, value2, ...]}}` |
| [$nin](#nin-operator) | Not match any value in array | `{"field" : {$nin : [value1, value2, ...]}}` |
| [$or](#or-operator) | Logical operator | `{$or: [{"status": "GOLD"}, {"status": "SILVER"}]}` |
| [$and](#and-operator) | Logical operator | `{$and: [{"status": "GOLD"}, {"sales": 1000}]}` |
### **$not** operator
Return documents not matching the query.
```js
const query = { name: { $not: 'Joe' } };
```
### **$in** operator
Return documents matching any values.
```js
const query = { name: { $in: ['Joe', 'Jane', 'Donald'] } };
```
### **$nin** operator
Return documents not matching any of the values.
```js
const query = { name: { $nin: ['Joe', 'Jane', 'Donald'] } };
```
### **$or** operator
Return documents that matches one or the other field.
```js
const query = { $or: [{ name: 'Jane' }, { name: 'Donald' }] };
```
### **$and** operator
Return documents both fields.
```js
const query = { $and: [{ name: 'Jane' }, { 'last-name': 'Cassidy' }] };
```
## NoSQL Operators: Conditional Operators
### Quick overview
| Operator | Description | Example |
| ---------- | :--------------------- | ------------------------------------------------------ |
| $gt | Greater than | `{"salary": {$gt: 10000}}` |
| $gte | Greater than or equal | `{"salary": {$gte: 10000}}` |
| $lt | Less than | `{"salary": {$lt: 10000}}` |
| $lte | Less than or equal | `{"salary": {$lte: 10000}}` |
| $ne | Not equal | `{"email": {$ne: ""}}` |
| $exists | Check if field exists | `{"field": {$exists: true`|`false}}` |
| $elemMatch | Array element matching | `{"contact":{$elemMatch:{"name":"Anderson", age:35}}}` |
### **$gt** operator
Return documents that matches each field value greater than numeric value.
```js
const query = { salary: { $gt: 10000 } };
```
### **$gte** operator
Return documents that matches each field value greater than or equal to numeric value.
```js
const query = { salary: { $gte: 10000 } };
```
### **$lt** operator
Return documents that matches each field value less than numeric value.
```js
const query = { salary: { $lt: 10000 } };
```
### **$lte** operator
Return documents that matches each field value less than or equal to numeric value.
```js
const query = { salary: { $lte: 10000 } };
```
### **$exists** operator
Return documents that matches each field with a value.
```js
const query = { field: { $exists: true } };
```
### **$exists (sub array)** operator
Return documents that matches each sub field with any value.
```js
const query = { 'field.0': { $exists: true } };
```
### **$elemMatch** operator
Return documents that matches at least one of the elements in an array field.
```js
const query = { contact: { $elemMatch: { name: 'Anderson', age: 35 } } };
```
### **$date** operator
Querying based on dates are done using the `$date` operator combined with ISO date strings.
For example:
```js
// between two dates
const query = {
_changed: { $gt: { $date: '2016-08-01' }, $lt: { $date: '2016-08-05' } },
};
```
## SQL to NoSQL Query Examples
Converting SQL queries to NoSQL? Here are common [SQL](https://en.wikipedia.org/wiki/SQL) statements expressed as NoSQL query examples:
#### `SELECT * FROM users`
```js
/*
* SQL statement:
* SELECT * FROM users
* expressed as a nosql database query
*/
const db = await Datastore.open();
db.find('users');
```
#### `SELECT user_id, status FROM users`
```js
const query = {};
const opt = {
hints: { $fields: { user_id: 1, status: 1 } },
};
db.find('users', query, opt);
```
#### `SELECT * FROM users WHERE status = "A"`
```js
const query = { status: 'A' };
db.find('users', query);
```
#### `SELECT * FROM users WHERE status != "A"`
```js
const query = { status: { $not: 'A' } };
db.find('users', query);
```
#### `SELECT * FROM users WHERE status = "A" AND age = 50`
```js
const query = { status: 'A', age: 50 };
db.find('users', query);
```
#### `SELECT * FROM users WHERE status = "A" OR age = 50`
```js
const query = { $or: [{ status: 'A' }, { age: 50 }] };
db.find('users', query);
```
#### `SELECT * FROM users WHERE age > 25`
```js
const query = { age: { $gt: 25 } };
db.find('users', query);
```
#### `SELECT * FROM users WHERE user_id like "bc%"`
```js
const query = { user_id: /^bc/ };
db.find('users', query);
```
#### `SELECT * FROM users WHERE status = "A" ORDER BY name ASC`
```js {5}
// Use the CLI to create a sorted index
// $ codehooks createindex --collection users --index name
const query = { status: 'A' };
const opt = {
sort: { name: 1 },
};
db.find('users', query, opt);
```
#### `SELECT * FROM users WHERE status = "A" ORDER BY name DESC`
```js {6}
// Use the CLI to create a sorted index
// $ codehooks createindex --collection users --index name
const query = { status: 'A' };
const opt = {
sort: { name: -1 },
};
db.find('users', query, opt);
```
#### `SELECT COUNT(*) FROM users`
```js
const query = {};
const opt = {
hints: { $onlycount: true },
};
db.find('users', query, opt);
```
#### `SELECT COUNT(*) FROM users WHERE age > 30`
```js
const query = { age: { $gt: 30 } };
const opt = {
hints: { $onlycount: true },
};
db.find('users', query, opt);
```
#### `SELECT * FROM users LIMIT 1`
```js
const query = {};
const opt = { limit: 1 };
db.find('users', query, opt);
```
#### `SELECT * FROM users LIMIT 5 SKIP 10`
```js
const query = {};
const opt = {
limit: 5,
offset: 10,
};
db.find('users', query, opt);
```
---
## Queue API
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
Process jobs with async message queues and worker functions.
This API is automatically available from the inside of any Codehook function.
**API quick overview**
## Datastore.open()
Datastore.open() opens a connection to a datastore in the active project space.
```js
import {Datastore} from 'codehooks-js'
async function myfunc() {
const conn = await Datastore.open();
// use conn to call API functions
...
}
```
**Returns** Promise / Datastore connection
## app.worker(topic, handler, options)
Define a worker function that processes jobs from a queue topic. Each time a job is enqueued with a matching topic, this worker function will be invoked to process it.
**Parameters**
- **topic**: queue topic string to listen for
- **handler**: function `(req, res) => {}` that processes the job
- `req.body.payload` contains the job data
- Call `res.end()` when processing is complete
- **options** (optional): worker configuration
- **workers**: number of parallel workers (on paid plans more than 1 parallel worker can be applied)
- **timeout**: number in milliseconds (on paid plans the execution time can be up to 10 minutes)
**Code example**
```js title="index.js"
import { app, Datastore } from 'codehooks-js';
// Define a worker with 3 parallel workers
app.worker('sendEmail', (req, res) => {
const { email, subject, body } = req.body.payload;
console.log('Sending email to', email);
// Process the job...
res.end(); // Signal completion
}, { workers: 3 });
export default app.init();
```
:::tip
For more details on worker queues, parallel processing, and architecture, see the [Worker Queues documentation](/docs/queuehooks).
:::
## enqueue(topic, payload, options)
Add a queued job in a datastore.
Jobs in the queue are processed by your worker function.
**Parameters**
- **topic**: queue topic string
- **payload**: any json data
- **options**: NOT IN USE
Returns **Promise / JobId**
**Code example**
```js {12} title="index.js"
import { app, Datastore } from 'codehooks-js';
// your worker function
app.worker('sendEmail', (req, res) => {
console.debug('Sending email to', req.body.payload.email);
// send email with your favorite service, e.g. Mailgun or Sendgrid
res.end(); // done processing queue item
});
app.post('/welcome', async (req, res) => {
const conn = await Datastore.open();
const jobId = await conn.enqueue('sendEmail', req.body);
res.status(200).json({
message: 'Thanks for the signup, you will receive an email soon :)',
});
});
export default app.init(); // Bind functions to the serverless runtime
```
**Running the example with curl**
```bash title="Shell command"
curl --location --request POST 'https://.api.codehooks.io/dev/welcome' \
--header 'x-apikey: ' \
--header 'Content-Type: application/json' \
--data-raw '{"email": "jones@codehooks.io"}'
```
```bash title="Output"
{"message":"Thanks for the signup, you will receive an email soon :)"}
```
:::tip
Your project name and token(s) can be retrieved by running the command 'coho info' when you are inside the project folder.
:::
## enqueueFromQuery(collection, query, topic, options)
Add multiple queued jobs from the result of a database query in a datastore.
Each object from the collection in the query result will be processed by your worker function.
If your collection has 1000 items, a `{}` query will add 1000 items to the queue and call your worker function 1000 times.
**Parameters**
- **collection**: datastore collection name
- **query**: [JSON query object](nosql-database-query-language)
- **topic**: queue topic string
- **options** (advanced optional settings for indexes and filtering)
- **useIndex**: indexed field name
- Indexes are created with CLI command `coho createindex`
- **startIndex**: jump into the index
- **endIndex**: where to end in the index
- **limit**: how many items to return
- **offset**: how many items to skip before returning any
- **reverse**: reverese scan of index, i.e. descending sort
Returns **Promise / \{JobId, count\}**
**Code example**
```js {12} title="index.js"
import { app, Datastore } from 'codehooks-js';
// Codehooks route hooks
app.post('/add', add);
app.worker('mailworker', mailWorker);
// add all items in a collection to the queue worker function
async function add(req, res) {
const conn = await Datastore.open();
const query = { emailConsent: true }; // all objects that matches query
const topic = 'mailworker';
const job = await conn.enqueueFromQuery('emaildata', query, topic);
console.log('Queue job', job);
res.end(job);
}
// worker function
async function mailWorker(req, res) {
let { payload } = req.body;
console.log('Mail worker', payload.email);
// fake that the job takes 100 ms
setTimeout(res.end, 100);
}
export default app.init(); // Bind functions to the serverless runtime
```
**Running the example with curl**
```bash title="Shell command"
curl --location --request POST 'https://.api.codehooks.io/dev/add' \
--header 'x-apikey: '
```
```bash title="Output"
{"count":2018,"jobId":"7c29faa2-d075-4fe6-af54-60dce6024f04"}
```
## Monitoring Queue Health
Use the [queue-status](/docs/cli#queue-status) CLI command to monitor queue health, item counts, and worker activity in real-time:
```bash
# Monitor queue status in real-time
coho queue-status --follow
# View queue status in compact format
coho queue-status --format compact
# Export queue status as JSON
coho queue-status --format json
```
This is particularly useful for:
- Monitoring queue depths and processing rates
- Identifying stuck or slow workers
- Debugging queue-based workflows
- Verifying worker configuration
## Related Topics
- [Worker Queues](/docs/queuehooks) - Detailed guide on worker queue architecture and configuration
- [Workflow API](/docs/workflow-api) - Build complex stateful workflows with queues
- [CLI Tools](/docs/cli#queue-status) - Command-line tools for monitoring queues
---
## Worker queues
*(Note: This content contains MDX/JSX code)*
## What is a Worker Queue?
A **worker queue** is a system for handling background tasks asynchronously by distributing jobs to queue workers (processes). In a serverless environment, worker queues enable **persistent and scalable job execution**, allowing you to decouple long-running or compute-heavy tasks from user-facing code.
With **worker queues**, you can process jobs like sending emails, generating PDFs, or running workflows reliably — even under load.
## How Worker Queues Work
Objects (tasks) are added to worker queues using the built-in [JavaScript Queue API](queueapi). Serverless worker queue functions process each object in a queue asynchronously. The illustration below shows how three independent worker queues (named topic-1, topic-2, topic-3) with their payload objects (x) are being processed by workers. Note how topic-1 has multiple parallel workers dequeuing items from the same queue.
```mermaid
flowchart LR
enqueue["`REST API
Function`"] --> q0
q0["enqueue(topic, x)"] --> mailQueue & pdfQueue & smsQueue
mailQueue[Worker Queue: topic-1 ] -.->|dequeue| worker1a[["worker1({payload: x})"]]
mailQueue -.->|dequeue| worker1b[["worker1.2({payload: x})"]]
mailQueue -.->|dequeue| worker1c[["worker1.3({payload: x})"]]
pdfQueue[Worker Queue: topic-2] --> q3[x] --> q4[x] -.->|dequeue| function2[["worker2({payload: x})"]]
smsQueue[Worker Queue: topic-3] --> q6[x] --> q7[x] -.->|dequeue| function3[["worker3({payload: x})"]]
```
## Worker Queue Example (Serverless Function)
The following example worker queue function prints to the log each time a `request.body` is added to the queue as a `POST` to the example route `/addQueue`.
```js title="index.js"
import { app, Datastore } from 'codehooks-js';
// set the number of parallel workers
const workerOptions = {workers: 1}
// Serverless worker queue function for topic 'myqueue'
app.worker('myqueue', (req, res) => {
console.log('The queue item is', req.body.payload);
res.end(); // done with one item
}, workerOptions);
// API route that adds to the worker queue
app.post('/addQueue', async (req, res) => {
const conn = await Datastore.open();
const jobId = await conn.enqueue('myqueue', { data: req.body });
res.status(201).json({ jobId });
});
export default app.init();
```
## Worker queue configuration
- Topic names are unique strings. E.g. `"sendEmails"`
- Queue data are accessed through the `request.body.payload` object
- Each topic is processed as a separate queue
- `workerOptions` controls the number of parallel workers. For example: `{ workers: 5 }`
- Each queue is stored in separate system collections prefixed with `_queue_{TOPIC}`. E.g. if the topic is `sendEmail` the queue would be named `_queue_sendEmails`
- Read more about the [JavaScript Queue API](queueapi)
:::tip
If you need more complex functionality, which uses worker queues, make sure to check out the **[Workflow API](workflow-api)**.
:::
---
## Quickstart using the CLI
*(Note: This content contains MDX/JSX code)*
This short tutorial will show you how to create codehooks projects and spaces using the command line interface (CLI). Check out the [concepts](concepts) page for a more in-depth introduction.
:::tip Using the Studio to create projects
If you want to deploy your first project without using the CLI, you can also use the web-based Studio to create projects directly in your browser. All new projects automatically deploy a space with a database CRUD API using app.crudlify().
:::
## Install the CLI
Install the Codehooks command line interface (CLI), this lets you fully manage your projects from the command line.
```bash
npm i -g codehooks
```
> You need [Node.js](https://nodejs.org) to install the CLI.
> **Note:** The CLI command is `coho` (short for **co**de**ho**oks). You can also use the full `codehooks` command - both work the same way.
## Sign up and Log in
Next you need to sign up / log in to your account (it's totally free), in this example we use a Github account. Your browser will open and direct you a secure login page, and there you can return to the CLI - logged in.
```bash
coho login
```
Then choose login method, e.g. Github.
```bash
Select option below
❯ Use Github
Use Google
Exit
```
## Deploy a Webhook Handler in 30 Seconds
The fastest way to get started is to use one of our production-ready webhook templates. These templates include everything you need: signature verification, database storage, and proper error handling.
```bash
# Deploy a Stripe webhook handler
coho create mystripehandler --template webhook-stripe-minimal
# Deploy a Shopify webhook handler
coho create myshopifyhandler --template webhook-shopify-minimal
# Deploy a GitHub webhook handler
coho create mygithubhandler --template webhook-github-minimal
```
After running one of these commands, the CLI will create a project folder with the webhook handler code. Change into the project directory and install dependencies:
```bash
cd mystripehandler # or your chosen project name
npm install
```
Now deploy the webhook handler to your space:
```bash
coho deploy
```
**That's it!** Your webhook endpoint is immediately ready at:
```
https://.api.codehooks.io/dev/webhooks/
```
Add your webhook secrets (Stripe API key, etc.) via the CLI (`coho set-env `) or web UI, then configure your webhook URL in Stripe/Shopify/GitHub's dashboard and start receiving events.
:::info Install Templates in Existing Projects
You can also install a template into an existing project using:
```bash
coho install
```
The CLI will display a list of all available templates. You can also visit the [templates repository](https://github.com/RestDB/codehooks-io-templates) for the complete list including Discord, Twilio, Clerk, and Slack webhooks.This is useful when you want to add webhook handlers to a project you've already created.
:::
Continue reading below if you want to create a custom project instead.
## Create a Custom Project: Deploy a CRUD API
If you prefer to create a custom project instead of using a webhook template, follow these steps to deploy a CRUD API project.
### Create project
Let's create a new project on your account. A project contains the source code for your serverless functions.
```bash
coho create myproject
```
Follow the guide to create a personal or team project type.
Change directory to the new project and install dependencies:
```bash
cd myproject
npm install
```
The `coho create` command has created a new project space and generated a unique name for the API, in this example it's named `myproject-2b39` and `dev`.
Your account will be the owner of the project. This can be changed later.
:::note Deploy code to an existing project
If you have an existing project you'd like to use, for example created with the Studio application, you can connect and deploy the code to that project using the [CLI command](/docs/cli#init) `coho init` instead of `coho create`.
:::
### Inspect the CRUD API code
Next, start your favorite code editor and open the auto generated `index.js` in your project directory.
```js title="index.js"
/*
* Auto generated Codehooks (c) example
*/
import { app } from 'codehooks-js';
// test route for https://.api.codehooks.io/dev/
app.get('/', (req, res) => {
res.send('CRUD server ready');
});
// Use Crudlify to create a REST API for any database collection
app.crudlify();
// bind to serverless runtime
export default app.init();
```
`app.crudlify` enables a complete database CRUD REST API for any collection. POSTing to a route, e.g. `/users`, will create a new user in the database collection `users`. GETing to the same route will return all users in the database collection `users`. Check out the [database CRUD REST API](/docs/database-rest-api) for more details.
Save the JavaScript file if you made any changes.
:::tip TypeScript Support
Codehooks supports TypeScript (version 5) with strong typing. Just rename the index.js file to index.ts, update the package.json main property, and you're ready to go.
```ts title="index.ts"
/* TypeScript */
import { app, httpRequest, httpResponse } from 'codehooks-js';
// strong typing for request and response
app.get('/', (req: httpRequest, res: httpResponse) => {
res.send('CRUD server ready');
});
// Use Crudlify to create a REST API for any database collection
app.crudlify();
// bind to serverless runtime
export default app.init();
```
```js title="package.json"
{
// rest of your package.json file
"main": "index.ts"
}
```
:::
### Deploy the code
```bash
coho deploy
```
Example output from the deploy command.
```bash
Deploying to Project: myproject-2b39 Space: dev
Deployed Codehook successfully! 🙌
```
You can now test the [database CRUD REST API](/docs/databaserestapi.md) with a sample collection, for example `users`.
:::tip
Use the `coho info --examples` command to find your project name, API tokens and working curl examples.
:::
```bash title="curl shell command - POST a new user"
curl --location 'https://.api.codehooks.io/dev/users' \
--header 'x-apikey: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"name": "Bill",
"email": "william@example.com",
"active": true
}'
```
Example output from the curl test.
```js
{
"name": "Bill",
"email": "william@example.com",
"active": true,
"_id": "6421b3e6a3c762051688edf7"
}
```
Lets also test a query for the same data we've added.
```bash title="curl shell command - query users"
curl --location 'https://.api.codehooks.io/dev/users?name=Bill' \
--header 'x-apikey: ' \
--header 'Content-Type: application/json' \
```
Which returns an array of 1 object from the database.
```js
[
{
name: 'Bill',
email: 'william@example.com',
active: true,
_id: '6421b3e6a3c762051688edf7',
},
];
```
Our test shows that the automatic REST API is accessible on the `/dev/users` route, and we can successfully add and retrieve data from the database to the client.
:clap::clap:
---
## Real-time API
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
The Codehooks.io real-time API enables applications and clients to publish and subscribe to data events.
**API quick overview**
## State diagram
The following diagram shows the necessary dataflow between a client and a server.
```mermaid
sequenceDiagram
participant A as Client (listener)
participant B as Server (publisher)
A->>B: POST {metadata} to a '/channel'
B-->>A: Create a new ListenerID
A-->>B: Create a new EventSource(`/channel/${ListenerID}`)
B-->>A: Real-time Events
```
## realtime.createChannel(channel)
Create a real-time channel for clients listeners and and server publishers.
**Parameters**
- **channel**: string - e.g. '/goals'
**Returns**: void.
**Code example for creating a channel**
```js title="index.js"
import { app, realtime } from 'codehooks-js';
// create a real-time channel
realtime.createChannel('/goals');
/* more code here ... */
// bind to serverless runtime
export default app.init();
```
## realtime.publishEvent(channel,data, [query])
Publish a data event on a channel to all or some (query) listeners.
**Parameters**
- **channel**: string - e.g. '/goals'
- **data**: object - Event object to publish
- **query**: object - optional matching criteria for listeners to get the event
**Returns**: Promise: object
**Code example for publish events**
```js title="index.js"
import { app, realtime } from 'codehooks-js';
// create a real-time channel
realtime.createChannel('/goals');
// broadcast event
async function someFunction() {
const listeners = { topic: 'score' }; // send to all listeners subscribing to topic
const event = { 'team A': 0, 'team B': 1, player: 'Messi' };
const data = await realtime.publishEvent('/goals', event, listeners);
}
/* more logic here ...*/
// bind to serverless runtime
export default app.init();
```
## realtime.createListener(channel, data)
Create a new listener (ID) that clients can use to subscribe to an EventSource channel.
**Parameters**
- **channel**: string - Channel name, e.g. '/goals'
- **data**: object - Listener topics of interest, e.g. `{topic: "score"}`
**Returns**: Promise: listenerData with a unique `_id`
**Code example for creating a new listener**
```js title="index.js"
import { app, realtime } from 'codehooks-js';
// create a real-time channel
realtime.createChannel('/clock');
// client rest api to connect to the real time channel
app.post('/connect', async (req, res) => {
const listenerData = await realtime.createListener('/clock', req.body);
// return listener ID to client
res.json({ listenerID: listenerData._id });
});
app.job('* * * * *', async (req, res) => {
const listeners = { topic: 'tick' }; // send to all listeners subscribing to topic
const event = { 'The time is': new Date() };
const data = await realtime.publishEvent('/clock', event, listeners);
});
// bind to serverless runtime
export default app.init();
```
## realtime.getListener(channel, listenerID)
Get a listener object for a specific listener (ID) on a channel.
**Parameters**
- **channel**: string - Channel name, e.g. '/goals'
- **listenerID**: string - A valid \_id for an existing listener
**Returns**: Promise: listenerData
## realtime.getListeners(channel)
Get all listeners on a channel (channel).
**Parameters**
- **channel**: string - Channel name, e.g. '/goals'
**Returns**: Promise: array of listenerData
## realtime.removeListener(channel, listenerID)
Remove a listener (listenerID) from a channel (channel).
**Parameters**
- **channel**: string - Channel name, e.g. '/goals'
- **listenerID**: string - A valid \_id for an existing listener
**Returns**: Promise: listenerData
## Complete code example - Real-time chat
To better understand how the Codehooks.io real-time API works, check out the following code example. The complete source code, including install instructions, is avaliable at [Github](https://github.com/RestDB/codehooks-io-examples/tree/main/realtime-chat).
The code example is a simple web (plain vanilla ugly HTML) chat app for POST'ing messages to a REST API endpoint. The messages are then received on the client in real-time from the server. The server (Codehooks.io) uses the real-time API to subscribe and publish data to/from client requests.

[You can test out a live example here](https://vivacious-kingdom-f532.codehooks.io/public/index.html)
### HTML client page
```html title="/public/index.html"
Codehooks.io - Chat App
Codehooks.io - Chat example
Real-time status:
Initializing
```
### HTML client JavaScript
```js title="/public/chat.js"
const API_TOKEN = '0a2249d4-5f10-489c-8229-1f060ad1e0f6';
let listenerID = null;
document.addEventListener('DOMContentLoaded', function () {
const aliasInput = document.getElementById('aliasInput');
const messageInput = document.getElementById('messageInput');
const sendButton = document.getElementById('sendButton');
// Function to send a message to the server
function sendMessage(message) {
fetch('/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-apikey': API_TOKEN,
},
body: JSON.stringify({
message: message,
listenerID,
alias: aliasInput.value.trim(),
}),
})
.then((response) => response.json())
.then((data) => {
console.log('Message sent:', data);
messageInput.value = ''; // Clear input after sending
messageInput.focus(); // Keep focus on input field
})
.catch((error) => {
console.error('Error:', error);
});
}
sendButton.addEventListener('click', function () {
if (messageInput.value) {
sendMessage(messageInput.value);
}
});
messageInput.addEventListener('keydown', function (event) {
// Check if Enter key is pressed without Shift key
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault(); // Prevent default to avoid line breaks or form submission
if (messageInput.value) {
sendMessage(messageInput.value);
}
}
});
// init real-time connection
startListener();
});
function addMessage(message) {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.textContent = message;
messagesDiv.appendChild(messageElement); // Adds the message at the bottom
// Auto-scroll to the bottom
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
// connect to realtime SSE
async function startListener() {
// setup the real time stuff
const statusIndicator = document.getElementById('statusIndicator');
const interests = {}; // everything
var requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-apikey': API_TOKEN,
},
body: JSON.stringify(interests),
};
// get a real time listenerID
console.log('Connect request', requestOptions);
const response = await fetch('/connect', requestOptions);
const result = await response.json();
listenerID = result.listenerID;
console.log('GOT clientID', result);
// connect to reatime channel
let eventSource = new EventSourcePolyfill(`/chat/${result.listenerID}`, {
headers: {
'x-apikey': API_TOKEN,
},
});
statusIndicator.textContent = 'Connected';
statusIndicator.style.color = 'green';
// incoming message event
eventSource.onmessage = function (event) {
console.log('Event', event.data);
const result = JSON.parse(event.data);
addMessage(result.message);
};
// here we go
eventSource.onopen = function (event) {
// Connection is open
statusIndicator.textContent = 'Live data ready';
statusIndicator.style.color = 'green';
};
// oops, reconnecting if possible
eventSource.onerror = function (event) {
console.log('Error', event);
// An error occurred or the connection was closed
if (eventSource.readyState == EventSource.CLOSED) {
console.log('Connection was closed.');
}
statusIndicator.textContent = 'No connection';
statusIndicator.style.color = 'red';
};
return result;
}
```
### Codehooks.io Server
See install and deploy docs at [Github](https://github.com/RestDB/codehooks-io-examples/tree/main/realtime-chat).
```js title="index.js"
// Codehooks.io - Chat example
import { app, realtime } from 'codehooks-js';
// create a real-time channel
realtime.createChannel('/chat');
// client connects for a new real-time listener ID
app.post('/connect', async (req, res) => {
const listenerData = await realtime.createListener('/chat');
// return listener ID to client
res.json({ listenerID: listenerData._id });
});
// client post a new message
app.post('/messages', async (req, res) => {
console.log('Message in', req.body);
const { message, listenerID, alias } = req.body;
const data = await realtime.publishEvent('/chat', {
message: `${alias || 'Anonymous'}: ${message}`,
});
res.end(data);
});
// annoying message from a cron job every second minute ;)
app.job('*/2 * * * *', async (_, job) => {
const message = `Hello from cron job at ${new Date().toISOString()}`;
const data = await realtime.publishEvent('/chat', {
message: `cron: ${message}`,
});
job.end();
});
app.static({ route: '/public', directory: '/public' });
// bind to serverless runtime
export default app.init();
```
---
## REST API Routing
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
App routes lets you create secure public [(REST) API](https://en.wikipedia.org/wiki/Representational_state_transfer) endpoints to serverless JavaScript functions in your application.
:::info REST API Routing Fundamentals
REST API routes, also known as endpoints, are specific URLs that clients use to interact with a RESTful API using standard HTTP methods:
**Key Concepts:**
- **Resource-centric**: Routes represent resources (e.g., `/users`, `/products`, `/orders`) rather than actions
- **HTTP Methods**: [GET, POST, PUT, PATCH, DELETE](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) for different operations
- **Hierarchical Structure**: Routes follow patterns like `/users/{user_id}/orders` to represent relationships
- **Stateless**: Each request contains all necessary information for the server to process it
- **Consistent & Predictable**: Well-designed routes make APIs easier to understand and use
**Example REST API Routes:**
```
GET /api/products // Get all products
GET /api/products/{id} // Get specific product
POST /api/products // Create new product
PUT /api/products/{id} // Update existing product
DELETE /api/products/{id} // Delete a product
```
**Learn more:** [REST Architecture](https://en.wikipedia.org/wiki/Representational_state_transfer) • [HTTP Methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods) • [HTTP Status Codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) • [API Design Best Practices](https://restfulapi.net/)
:::
## Example REST API route
The example app routes below creates two REST API endpoint routes that triggers a serverless function. This simple example function `echo` does nothing useful, other than echoing back the request data to the client as JSON.
```js title="index.js"
import app from 'codehooks-js';
// Codehooks API routes
app.post('/myroute', echo);
app.get('/myroute/:var1/:var2', echo);
// Serverless function
function echo(req, res) {
res.json(req);
}
// bind to Codehooks runtime
export default app.init();
```
After deploying the REST API with the [`coho deploy` CLI command](/docs/cli#deploy) we can run a quick test from the command line using `curl`.
:::tip
You can use [Postman](https://www.postman.com/api-platform/api-testing/) together with Codehooks for testing your APIs.
:::
```bash title="Example command line"
curl https://myproject-abcd.api.codehooks.io/dev/myroute/123/321
-H 'x-apikey: 6e111ea6-6b3a-412d-8f6b-061c816c67c9'
```
The example shows that our function echoes back the input as a structured (and very handy) JSON object.
```js title="Example output"
{
"hostname": "myproject-abcd.api.codehooks.io",
"headers": {
"host": "myproject-abcd.api.codehooks.io",
"user-agent": "curl/7.77.0",
"accept": "*/*",
"x-apikey": "6e111ea6-6b3a-412d-8f6b-061c816c67c8"
},
"query": {},
"path": "/dev/myroute/123/321",
"apiPath": "/myroute/123/321",
"originalUrl": "/dev/myroute/123/321",
"params": {
"var1": "123",
"var2": "321"
},
"body": {},
"method": "GET"
}
```
After deployment the specified routes will be available as HTTPS endpoints.
## REST API endpoint URL spec
A REST API app route is available at an HTTPS endpoint based on the HTTP verb, tenant ID, datastore name and API route specification.
```shell
┌─── Project Id, e.g. booking-2b39
│ ┌─── Project space name (environment) , e.g. prod
│ │ ┌─── API route, e.g. /cars/:model/:make
│ │ │ ┌─── Parameters, e.g. color=black
│ │ │ │
│ │ │ │
│ │ │ │
│ │ │ │
https://.api.codehooks.io//[?QUERY]
Example URL: /cars/:model/:make
https://booking-2b39.api.codehooks.io/prod/cars/Tesla/model-x?color=black&campaign=A
```
## REST API route matching
Any request agaist a space REST API, e.g. `/dev/*`, are matched against the deployed and declared app route functions.
### String path
Route declaration must match exact to trigger function.
```js
// matches: https://apollo-ff00.api.codehooks.io/dev/admin
app.get('/admin', myFunc);
```
### Path parameters
Route declaration can match variable named segments of route to trigger function.
```js
// matches: https://apollo-ff00.api.codehooks.io/dev/admin/abc-123
app.get('/admin/:messageId', myFunc);
```
### Wildcard
Route declaration can match wildcard segments of route to trigger function.
```js
// matches: https://apollo-ff00.api.codehooks.io/dev/user/abc-123/inbox
// matches: https://apollo-ff00.api.codehooks.io/dev/user/turbo-xyz/inbox
app.get('/user/*/inbox', myFunc);
```
### Regular expression
Route declaration matches a regular expression to trigger function.
```js
// matches: https://apollo-ff00.api.codehooks.io/dev/foo/bar
// matches: https://apollo-ff00.api.codehooks.io/dev/user/bar/foo/y/x
app.get(/^\/(foo|bar)/, myFunc);
```
## The REST API request object
The request object contains these following properties for a client (HTTPS) request.
### Overview
node.value.startsWith('request.'))}/>
### request.headers
Get the HTTP header values as key-value pairs.
```js
console.log(req.headers['user-agent']); // -> PostmanRuntime/7.29.0
```
### request.query
Get the URL query parameters as key-value pairs.
```js
// route: /product
// url : /dev/product?price=10
console.log(req.query.price); // -> 10
```
### request.params
Get the URL route variables as key-value pairs.
```js
// route: /order/:orderID/:type
// url : /dev/order/12345/small
console.log(req.params.orderID, req.params.type); // -> 12345, small
```
### request.body
Get the parsed request body. Codehooks automatically parses the request body based on the `Content-Type` header:
- **`application/json`** - Parsed as JSON object
- **`application/x-www-form-urlencoded`** - Parsed as key-value object (used by HTML forms, Slack slash commands, etc.)
```js
// JSON request: {"foo": "bar"}
console.log(req.body); // -> {"foo": "bar"}
// Form-urlencoded request: name=John&age=30
console.log(req.body); // -> {"name": "John", "age": "30"}
```
:::tip Webhook Integrations
Some webhook providers send `application/x-www-form-urlencoded` instead of JSON. For example, Slack slash commands POST form data with fields like `command`, `text`, `user_id`, etc. Codehooks parses these automatically into `req.body`.
:::
### request.rawBody
Get the raw, unparsed request body as a string. This is particularly useful for webhook integrations that require signature verification using the original payload.
:::tip Webhook Signature Verification
Many webhook providers (like Stripe, GitHub, Shopify) require you to verify the webhook signature using the raw request body. Use `request.rawBody` instead of `request.body` for this purpose.
:::
```js
// Example: Verify webhook signature
const signature = req.headers['x-webhook-signature'];
const rawPayload = req.rawBody;
const expectedSignature = computeHMAC(rawPayload, webhookSecret);
if (signature === expectedSignature) {
// Signature is valid, process the webhook
const data = req.body; // Use parsed body for processing
console.log(data);
}
```
### request.path
Get the URL full path.
```js
// route: /order/:orderID
// url : /dev/order/12345
console.log(req.path); // -> /dev/order/12345
```
### request.apipath
Get the URL api path.
```js
// route: /order/:orderID
// url : /dev/order/12345
console.log(req.apipath); // -> /order/12345
```
### request.originalUrl
Get the URL full path and query parameter string.
```js
// route: /order/:orderID
// url : /dev/product?size=small
console.log(req.originalUrl); // -> /dev/product?size=small
```
### request.method
Get the HTTP request verb.
```js
// url: /dev/product
console.log(req.method); // -> GET
```
### request.hostname
Get the project URL domain name.
```js
// url: https://myproject-ff00.api.codehooks.io/dev/product
console.log(req.hostname); // -> myproject-ff00.api.codehooks.io
```
### request.pipe(Stream)
Process a request data stream by piping it to another stream. Useful to process other data formats than JSON, e.g. binary or raw data content types - `content-type: application/octet-stream`.
```js
const mystream = new PassThrough();
// process mystream
req.pipe(mystream);
```
### request.on(event, data)
Process a request data stream by listening to the request data emitter. Useful to process other data formats than JSON, e.g. binary or raw data content types - `content-type: application/octet-stream`.
```js
req.on('data', (buf) => {
console.log(buf.length); // process buf data
});
req.on('end', (bytes) => {
console.log('Got binary data', bytes);
res.end('Got binary ' + bytes);
});
```
## The REST API response object
The response object sends data back to the client and finalizes the request.
### Overview
node.value.startsWith('response.'))}/>
### response.set(header, value)
Set a response header value.
**Parameters**
- `header` **string**, [read more about HTTP header values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
- `value` **string**
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.set('Content-Type', 'text/html; charset=UTF-8'); // set a response header
res.end('
Hello world!
'); // send html back to client
}
```
### response.headers(Object)
Set multiple response header values.
**Parameters**
- `Object` **JSON**, [read more about HTTP header values](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers)
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.headers({
'Content-Type': 'text/html; charset=UTF-8',
'X-myheader': '123456890',
});
res.send('
Hello world!
'); // send html back to client
}
```
### response.status(HttpStatusCode)
Return a HTTP response status code for a client request.
**Parameters**
- `HttpStatusCode` **integer**, [read more about HTTP status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status)
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.status(401); // Unauthorized
res.end();
}
```
### response.json(Object)
Return a JSON response for a client request.
:::caution
Ends the client request and terminates the Codehook execution.
:::
**Parameters**
- `Object` **JSON**
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.json({ message: 'Hello' });
}
```
### response.write(data, [encoding])
Stream data back to the client as text or binary data buffers.
NB! `content-type` must be set before any `write` operations.
**Parameters**
- `data` **string** or any **data** that the Content-Type allows
- `encoding` **optional string** use `res.write(mybuf, 'buffer')` to write binary data back to client
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.set('content-type', 'text/plain');
res.write('Hello');
res.write('world!');
res.write('\n');
res.end();
}
```
### response.send(data)
End response for a client request.
:::caution
Ends client request and terminates the Codehook execution immediately.
If not called, the Codehook will not return and finally time out.
:::
**Parameters**
- `data` **string**
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.send('Thanks for visiting!');
res.end();
}
```
### response.redirect(statusCode, URL)
Redirect a client request.
**Parameters**
- `statusCode` **number** 301 or 302
- `URL` **string** A valid relative or absolute URL
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.redirect(301, 'https://example.com');
}
```
### response.end()
End response for a client request.
:::caution
Ends client request and terminates the Codehook execution immediately.
If not called, the Codehook will not return and finally time out.
:::
**Parameters**
- none
**Code example**
```js
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
res.send('Thanks for visiting!');
res.end();
}
```
### response.writable
Get the Writable object that can be used in node.js Stream operations.
```js
mystream.pipe(res.writable);
```
## Middleware functions
You can create middleware logic by chaining functions in an array or globally with the `app.use` function.
Each function have a `next` method parameter that is used to "chain" the next function or to terminate the chain. The example below shows how a middleware function is called before the main function. The middelware function typically check some state or logic before proceeding to the next.
```js title="index.js"
import app from 'codehooks-js';
// Global middleware on all routes
app.use((req, res, next) => {
console.log("I'm global");
res.next();
});
// Global middleware on route matches
app.use('/my*', (req, res, next) => {
console.log("I'm global match for /my*");
res.next();
});
// Example route with middleware
app.post('/myroute', middleWare, myFunc);
// REST route
app.get('/myroute', myFunc);
function myFunc(req, res) {
// All is OK, got here finally
res.end();
}
function middleWare(req, res, next) {
let OK = true;
// do something here
if (OK) {
next();
} else {
res.status(401); // Unauthorized
res.end();
}
}
export default app.init();
```
## Wildcard routes
Wildcards lets you match all `methods`and all `routes` that are not matched by previous `routes`.
See example code below.
```js {7} title="index.js"
import app from 'codehooks-js'
app.get('/myroute': getFunc);
app.put('/myroute/:id': putFunc);
// wildcard route
app.all('/*': (req, res) => {
res.end("I catch all methods, on all routes not matched before");
}
)
export default app.init();
```
---
## Application secrets
Application secrets are stored as environment variables in your application space.
To protect your private secrets, environment variables can be encrypted and then dynamically added to your application serverless runtime.
A Secret is typically an encrypted environment variable that contains a small amount of sensitive data such as a password, a token, or a key.
## Adding secret environment variables
The following Codehooks [CLI command](/docs/cli#set-env) will create an encrypted enviroment variable that you can use in your code as `process.env.MY_SECRET_VARIABLE`:
```bash
coho set-env MY_SECRET_VARIABLE 'some secret content here' --encrypted
```
## Adding regular non-secret environment variables
Running the `set-env` command without the `--encrypted` parameter will create a plain text variable, which you can use in your code as `process.env.MY_VARIABLE`.
The follwing Codehooks CLI command will create an encrypted environment variable that you can use in your code as `process.env.VARIABLENAME`:
```bash
coho set-env MY_VARIABLE 'some regular content here'
```
## Inspecting secrets and environment variables
You can use the `coho info` CLI command to inspect your system variables. You will not be able to inspect the actual content of encrypted variables. Example output of the `info` command is shown below:
```bash
coho info
...
Spaces:
┌──────────────┬─────────┬───────────────┬──────┬───────────────────────────────────────┐
│ Name │ Tokens │ Allowed Hosts │ Jwks │ Env │
├──────────────┼─────────┼───────────────┼──────┼───────────────────────────────────────┤
│ dev (active) │ │ │ │ MY_SECRET_VARIABLE=(encrypted) │
│ │ │ │ │ MY_VARIABLE=some regular content here │
└──────────────┴─────────┴───────────────┴──────┴───────────────────────────────────────┘
...
```
## Removing environment variables
The follwing Codehooks CLI command will remove any enviroment variable:
```bash
coho remove-env MY_VARIABLE
```
:::tip
You can also set these environment variables using the account UI.
:::
---
## Workflow API - Build Reliable Stateful Workflows with durable functions
*(Note: This content contains MDX/JSX code)*
Create and deploy robust, scalable workflows using queued functions and state management. Build reliable backend systems with automatic retry, state persistence, and distributed processing.
## Overview
Codehooks workflows combines persistent queues with state management to create reliable, scalable workflows. Each step in a workflow can modify the process state and control the flow using the `goto` function, enabling complex workflows with branching, loops, and conditional execution. The system automatically handles state persistence, distributed processing, and error recovery.
### Key Features
- **State Management**
- Persistent state storage
- Atomic state updates
- Complete execution history
- State inspection and debugging
- **Queue-Based Processing**
- Queued (durable) functions
- Automatic retry on failures
- Load balancing across workers
- No lost messages or duplicate processing
- Parallel execution of workflow steps
- **Scalability & Reliability**
- Distributed processing
- Automatic failover
- State recovery
- Error handling and monitoring
### Why Use Workflows?
Workflows help you build reliable backend systems and integration logic. Using queued functions they handle system failures and network issues by automatically resuming from the last known state after crashes or connection losses. This makes them ideal for critical processes like order processing and payment flows. The system distributes work across multiple workers and processes long-running tasks asynchronously. As a developer, you can focus on implementing business logic while the framework handles state persistence, retries, and scaling.
**Perfect for Webhook Processing:** Workflows are especially powerful for webhook handlers. When you receive a webhook from Stripe, Shopify, or GitHub, you often need multi-step processing: verify signature → store event → trigger fulfillment → update inventory → send notifications → schedule follow-ups. With workflows, you can acknowledge the webhook immediately (preventing retries), then reliably process each step with automatic retries and state tracking. If any step fails, the workflow resumes from that point—ensuring no webhook event is lost or processed twice.
## A simple workflow example
Let's create a simple workflow app that demonstrates branching based on a random number. The workflow will process even and odd numbers differently.
```mermaid
graph TD
A[begin] -->|initialize| B[decide]
B -->|even| C[evenPath]
B -->|odd| D[oddPath]
C -->|complete| E[end]
D -->|complete| E
E -->|finish| F[Exit]
```
The full source code for the workflow steps is shown below.
```javascript title="index.js"
import { app } from 'codehooks-js';
// Create a workflow definition
const workflow = app.createWorkflow('simpleTask', 'Basic workflow example', {
// Step 1: Initialize the workflow
begin: async function (state, goto) {
state = {
message: 'Starting workflow',
// Add a random number to demonstrate branching
value: Math.floor(Math.random() * 10),
};
goto('decide', state);
},
// Step 2: Decide which path to take
decide: async function (state, goto) {
// Branch based on whether the value is even or odd
if (state.value % 2 === 0) {
goto('evenPath', state);
} else {
goto('oddPath', state);
}
},
// Step 3a: Handle even numbers
evenPath: async function (state, goto) {
state = {
message: 'Processing even number',
path: 'even',
processed: true,
};
goto('end', state);
},
// Step 3b: Handle odd numbers
oddPath: async function (state, goto) {
state = {
message: 'Processing odd number',
path: 'odd',
processed: true,
};
goto('end', state);
},
// Step 4: End the workflow
end: function (state, goto) {
state = {
final_message: `Workflow completed! Processed ${state.path} number: ${state.value}`,
};
goto(null, state); // workflow complete
},
});
// emitted event when a workflow completes
workflow.on('completed', (data) => {
console.log('Workflow completed:', data);
});
// REST API to start a new workflow instance
app.post('/start', async (req, res) => {
const result = await workflow.start({ foo: 'bar' });
res.json(result);
});
// export app interface to serverless execution
export default app.init();
```
You can [deploy](/docs/quickstart-cli#deploy-the-code-to-the-serverless-cloud) the workflow in the [Studio](/docs/studio) app directly or with the [CLI command `coho deploy`](/docs/cli#deploy).
See more [examples](#example-workflows) at end of this page.
## Workflow State Storage and Inspection
All workflow instances and their states are automatically persisted in a collection (default name: `workflowdata`). This collection stores the complete state of every workflow instance, making it easy to inspect and debug workflows at any time.
Each document in the collection contains:
- `_id`: Unique identifier for the workflow instance
- `nextStep`: The next step to be executed (null when workflow is complete)
- `createdAt`: Timestamp when the workflow was created
- `updatedAt`: Timestamp of the last state update
- `workflowName`: Name of the workflow definition
- `previousStep`: Name of the last executed step
- `stepCount`: Object containing execution statistics for each step
- `visits`: Number of times the step was executed
- `startTime`: When the step started execution
- `finishTime`: When the step finished execution
- `totalTime`: Total execution time in milliseconds
- `totalTime`: Total workflow execution time in milliseconds
- `...state`: Your custom state properties for the workflow
**An example state object** for a completed `simpleTask` workflow is shown below:
```js
{
"foo": "bar",
"nextStep": null,
"createdAt": "2025-06-09T15:35:41.318Z",
"workflowName": "simpleTask",
"stepCount": {
"begin": {
"visits": 1,
"startTime": "2025-06-09T15:35:41.529Z",
"totalTime": 5,
"finishTime": "2025-06-09T15:35:41.534Z"
},
"decide": {
"visits": 1,
"startTime": "2025-06-09T15:35:41.700Z",
"totalTime": 7,
"finishTime": "2025-06-09T15:35:41.707Z"
},
"evenPath": {
"visits": 1,
"startTime": "2025-06-09T15:35:41.757Z",
"totalTime": 5,
"finishTime": "2025-06-09T15:35:41.762Z"
},
"end": {
"visits": 1,
"startTime": "2025-06-09T15:35:42.578Z",
"totalTime": 8,
"finishTime": "2025-06-09T15:35:42.586Z"
}
},
"_id": "6846ff4d5a5945501a11d691",
"updatedAt": "2025-06-09T15:35:42.586Z",
"message": "Processing even number",
"value": 6,
"previousStep": "end",
"path": "even",
"processed": true,
"final_message": "Workflow completed! Processed even number: 6",
"totalTime": 1260
}
```
You can query this collection directly to inspect all workflow states:
```js
// Example REST API to get all completed instances of a workflow
app.get('/some', async (req, res) => {
// Example: Find all running instances of a workflow
const db = await Datastore.open();
const runningInstances = await db
.getMany('workflowdata', {
workflowName: 'simpleTask',
nextStep: null,
})
.toArray();
res.json(runningInstances);
});
```
Or you can query the collection for a specific workflow instance:
```js
// Example REST API to get a specific workflow instance
app.get('/one/:id', async (req, res) => {
const db = await Datastore.open();
// Example: Get state of a specific workflow instance
const instance = await db.getOne('workflowdata', {
_id: req.params.id,
});
res.json(instance);
});
```
To use a different collection name, pass a configuration object as the last argument when creating the workflow:
```js
const workflow = app.createWorkflow('myWorkflow', 'description', steps, {
collectionName: 'my_custom_collection'
});
```
Here's how the workflow data appears in Codehooks Studio for an example workflow:

The screenshot shows the state of a workflow instance that:
1. Started at 17:21:00
2. Branched trough the `even` path
3. Is completed because nextStep is `null`
4. Contains the state data with the message properties
## Core API Overview
**Methods**
The workflow API provides these core methods for managing persistent workflows:
### createWorkflow(name, description, steps, config?)
Creates a new workflow of persistent steps.
```js
const workflow = app.createWorkflow('workflow', 'description', {
START: async (state, goto) => {
// step implementation
},
});
```
**Parameters:**
- `name` (string): Unique identifier for the workflow
- `description` (string): Human-readable description of the workflow
- `steps` (WorkflowDefinition): Object containing step definitions
- `config` (object, optional): Configuration options
- `collectionName` (string): Collection name for storing workflow data (default: `'workflowdata'`)
- `queuePrefix` (string): Queue prefix for workflow jobs (default: `'workflowqueue'`)
- `timeout` (number): Global timeout in milliseconds for workflow steps (default: `30000`)
- `maxStepCount` (number): Maximum number of times a step can be executed (default: `3`)
- `workers` (number): Number of parallel workers to run per queue (default: `1`)
- `steps` (object): Step-specific configuration options
- `[stepName].timeout` (number): Timeout in milliseconds for this specific step
- `[stepName].maxRetries` (number): Maximum number of retries for this step
- `[stepName].workers` (number): Number of parallel workers for this specific step
**Returns:** Workflow instance for managing the workflow
### start(initialState)
Starts a new Workflow instance.
```js
const result = await workflow.start({ data: 'value' });
// Returns state, e.g. {_id: '682ec2b3dab9887c1da726e9', ...state}
```
**Parameters:**
- `initialState` (any): Initial state for the workflow instance
**Returns:** Promise with the workflow instance
### updateState(instanceId, state, options?)
Updates the state of a workflow instance.
```js
await workflow.updateState(
'682ec2b3dab9887c1da726e9',
{ count: 1 },
{ continue: true }
);
```
**Parameters:**
- `instanceId` (string): ID of the workflow instance
- `state` (any): New state to update with
- `options` (UpdateOptions, optional): Options for the update, `{ continue: false }` to avoid continuing the step
**Returns:** Promise with the updated state
### setState(instanceId, stateData)
Set the complete state of a workflow instance.
```js
await workflow.setState('682ec2b3dab9887c1da726e9', {
_id: '682ec2b3dab9887c1da726e9',
state: { count: 1, status: 'active' },
});
```
**Parameters:**
- `instanceId` (string): ID of the workflow instance
- `stateData` (object): Object containing `_id` and `state`
**Returns:** Promise that resolves to void
### continue(instanceId, reset?)
Continue a paused workflow instance.
```js
const result = await workflow.continue('682ec2b3dab9887c1da726e9', false);
// Returns: { instanceId: string }
```
**Parameters:**
- `instanceId` (string): ID of the workflow instance
- `reset` (boolean, optional): Whether to reset all step counts (true) or just the current step (false)
**Returns:** Promise with the queue ID for the continued step
### getWorkflowStatus(id)
Get the status of a workflow instance.
```js
const status = await workflow.getWorkflowStatus('682ec2b3dab9887c1da726e9');
```
**Parameters:**
- `id` (string): ID of the workflow instance
**Returns:** Promise with the workflow status
:::tip CLI Tool
Use the [workflow-status](/docs/cli#workflow-status) CLI command to visualize and monitor all workflow instances in real-time:
```bash
coho workflow-status --follow --active
```
:::
### getInstances(filter)
Lists workflow instances matching a filter.
```js
const instances = await workflow.getInstances({ status: 'running' });
```
**Parameters:**
- `filter` (any): Filter criteria for workflows
**Returns:** Promise with list of workflow instances
### cancelWorkflow(id)
Cancel a workflow instance.
```js
const result = await workflow.cancelWorkflow('682ec2b3dab9887c1da726e9');
```
**Parameters:**
- `id` (string): ID of the workflow instance to cancel
**Returns:** Promise with the cancellation result
### continueAllTimedOut()
Continues all timed out step instances in the workflow. This is useful for recovering workflows that have timed out due to system issues or long-running operations.
**Returns:** Promise with array of results containing instance IDs for continued workflows
**Example:**
```js title="Continue timed out workflows"
// Create workflow with 5 minute timeout
const workflow = app.createWorkflow('simpleTask', 'Basic workflow example', {
/* workflow steps */
}, {
timeout: 1000 * 60 * 5, // 5 minute timeout threshold
});
// Schedule job to continue timed out workflows every 5 minutes
app.job('*/5 * * * *', async (req, res) => {
const result = await workflow.continueAllTimedOut();
if (result.length > 0) {
console.log('Continued these dangling workflows:', result);
}
res.end();
});
```
### findTimedOutSteps(filter?)
Find all workflow instances with timed out steps.
```js
const timedOutWorkflows = await workflow.findTimedOutSteps();
// Returns array of workflows with timeout details:
// [{
// workflowId: string,
// workflowName: string,
// isTimedOut: true,
// executionTime?: number,
// runningTime?: number,
// timeout: number,
// step: string,
// startTime: string,
// finishTime?: string,
// currentTime?: string,
// timeoutSource: string
// }]
```
**Parameters:**
- `filter` (any, optional): Optional filter criteria for workflows
**Returns:** Promise with array of workflow instances that have timed out steps
### on(event, listener)
Registers an event listener.
```js
workflow.on('stepStarted', ({ workflowName, step, state, instanceId }) => {
console.log(`Step ${step} started in workflow ${workflowName}`);
});
```
**Parameters:**
- `event` (WorkflowEvent): Name of the event to listen for
- `listener` (function): Callback function to handle the event
**Returns:** workflow instance
### once(event, listener)
Register a one-time event listener.
```js
workflow.once('completed', (data) =>
console.log('Workflow completed once:', data)
);
```
**Parameters:**
- `event` (WorkflowEvent): Name of the event to listen for
- `listener` (function): Callback function to handle the event
**Returns:** workflow instance
### off(event, listener)
Remove an event listener.
```js
workflow.off('stepStarted', myListener);
```
**Parameters:**
- `event` (WorkflowEvent): Name of the event
- `listener` (function): Callback function to remove
**Returns:** workflow instance
### emit(event, data)
Emit an event.
```js
workflow.emit('customEvent', { message: 'Custom event data' });
```
**Parameters:**
- `event` (WorkflowEvent): Name of the event to emit
- `data` (WorkflowEventData): Event data
**Returns:** boolean indicating if the event was handled
### Workflow Configuration
Configure the workflow engine by passing a configuration object as the last argument to `createWorkflow`.
```js
import { app } from 'codehooks-js';
const workflow = app.createWorkflow('myWorkflow', 'My workflow description', {
// ... workflow steps ...
}, {
collectionName: 'workflows', // Set storage collection name
queuePrefix: 'workflow', // Set queue prefix name
timeout: 30000, // Global timeout in milliseconds
maxStepCount: 3, // Maximum step execution count
workers: 5, // Number of parallel workers per queue
steps: {
// Step-specific configuration
stepName: {
timeout: 3000, // Step-specific timeout
maxRetries: 3, // Step-specific retry count
workers: 2, // Step-specific worker count
},
},
});
```
The configuration object supports these options:
- `collectionName` (string, optional): Collection name for storing workflow data. Default: `'workflowdata'`
- `queuePrefix` (string, optional): Queue prefix for workflow jobs. Default: `'workflowqueue'`
- `timeout` (number, optional): Global timeout in milliseconds for workflow steps. Default: `30000`
- `maxStepCount` (number, optional): Maximum number of times a step can be executed. Default: `3`
- `workers` (number, optional): Number of parallel workers to run per queue. Default: `1`
- `steps` (object, optional): Step-specific configuration options
- `timeout` (number, optional): Timeout in milliseconds for this specific step
- `maxRetries` (number, optional): Maximum number of retries for this step
- `workers` (number, optional): Number of parallel workers for this specific step. Overrides global `workers` setting.
## Events
The workflow API emits these events:
### workflowCreated
Emitted when a new workflow is registered.
```js
workflow.on('workflowCreated', ({ name, description }) => {
console.log(`Workflow "${name}" registered: ${description}`);
});
```
**Event Data:**
- `name` (string): Workflow name
- `description` (string): Workflow description
### workflowStarted
Emitted when a new workflow instance is started.
```js
workflow.on('workflowStarted', ({ name, initialState }) => {
console.log(`Workflow "${name}" started with initial state:`, initialState);
});
```
**Event Data:**
- `name` (string): Workflow name
- `initialState` (any): Initial state data
### stepStarted
Emitted when a step begins execution.
```js
workflow.on('stepStarted', ({ workflowName, step, state, instanceId }) => {
console.log(`Step "${step}" started in workflow "${workflowName}"`);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `step` (string): Name of the step
- `state` (any): Current workflow state
- `instanceId` (string): Workflow instance ID
### stateUpdated
Emitted when a step's state is updated.
```js
workflow.on('stateUpdated', ({ workflowName, state, instanceId }) => {
console.log(`State updated in workflow "${workflowName}":`, state);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `state` (any): Updated state data
- `instanceId` (string): Workflow instance ID
### stepWaiting
Emitted when a step is waiting for input.
```js
workflow.on('stepWaiting', ({ workflowName, step, instanceId }) => {
console.log(`Step "${step}" waiting for input in workflow "${workflowName}"`);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `step` (string): Name of the step
- `instanceId` (string): Workflow instance ID
### workflowContinued
Emitted when a workflow instance is continued after waiting.
```js
workflow.on('workflowContinued', ({ workflowName, step, instanceId }) => {
console.log(`Workflow "${workflowName}" continued from step "${step}"`);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `step` (string): Name of the step
- `instanceId` (string): Workflow instance ID
### stepEnqueued
Emitted when a step function is added to queue.
```js
workflow.on('stepEnqueued', ({ workflowName, step, instanceId }) => {
console.log(`Step "${step}" enqueued for workflow "${workflowName}"`);
});
```
**Event Data:**
- `workflowName` (string): Name of the workflow
- `step` (string): Name of the step
- `instanceId` (string): Workflow instance ID
### completed
Emitted when a workflow instance is completed.
```js
workflow.on('completed', ({ message, state }) => {
console.log('Workflow completed:', message, state);
});
```
**Event Data:**
- `message` (string): Completion message
- `state` (any): Final workflow state
### cancelled
Emitted when a workflow instance is cancelled.
```js
workflow.on('cancelled', ({ id }) => {
console.log(`Workflow instance "${id}" was cancelled`);
});
```
**Event Data:**
- `id` (string): Workflow instance ID
### error
Emitted when an error occurs.
```js
workflow.on('error', ({ error }) => {
console.error('Workflow error:', error);
});
```
**Event Data:**
- `error` (Error): Error object with details
## Step Function
Each step function receives three parameters:
### Parameters
- `state` (any): Current workflow state object that can be modified and passed to next step
- `goto` (function): Function to transition between steps, supports:
- Name of next step to transfer to: `goto('stepName', state)`
- Parallel execution of multiple steps: `goto(['p1', 'p2'], state)`.
- All parallel steps must join in the same next step, i.e. `goto('samestep', state)`.
- Process will wait for all parallel steps to finish before continuing to `samestep`.
- If name of next step is `null` the workflow is completed: `goto(null, state)`
- Optional third parameter for additional options: `goto('nextStep', state, options)`
- `wait` (function, optional): Function to wait for an external state change
- optional state update before going into a wait state, e.g. `wait()` or `wait({val: 42})`
- Call [updateState](#updatestateinstanceid-state-options) to continue workflow
### Examples
Basic step with single transition:
```js
async function step(state, goto) {
// Update state
state = { count: state.count + 1 };
// Transition to next step
goto('nextStep', state);
}
```
Step with conditional transitions:
```js
async function conditionalStep(state, goto) {
if (state.count >= 3) {
// End workflow when condition met
goto(null, state);
} else if (state.needsValidation) {
// Go to validation step
goto('validate', state);
} else {
// Continue to next step
goto('process', state);
}
}
```
Step with parallel execution:
```js
async function parallelStep(state, goto) {
// Execute multiple steps in parallel
goto(['processA', 'processB'], state);
}
// Both processA and processB must eventually call:
// goto('joinStep', state);
```
Step with a wait state:
```js {6}
async function stepWaiting(state, goto, wait) {
// goto bar or wait
if (state.foo === 'bar') {
goto('bar', state);
} else {
wait({ status: 'waiting for bar' });
}
}
```
## Error handling
Workflow errors are automatically captured and logged. You can monitor errors in several ways:
### Logging and Monitoring
- **Studio UI**: View workflow logs in real-time using the Studio interface
- **CLI Logs**: Stream logs with `coho log -f` to see workflow execution and errors
- **CLI Workflow Status**: Monitor workflow instances with `coho workflow-status --follow` ([details](/docs/cli#workflow-status))
- **CLI Queue Status**: Monitor queue health and worker activity with `coho queue-status --follow` ([details](/docs/cli#queue-status))
- **Database**: Errors are stored in the workflow instance as `lastError` property
The CLI monitoring tools are particularly useful during development and debugging:
```bash
# Monitor all active workflows in real-time
coho workflow-status --active --follow
# View detailed workflow instance status
coho workflow-status --detailed
# Monitor queue health and workers
coho queue-status --follow
# Export workflow status as JSON for analysis
coho workflow-status --format json > workflow-status.json
```
### Error Inspection
To find workflow instances with errors, use the Studio collection (`workflowdata`) query tool with these example queries:
```js
// Find all workflow instances with errors
{ lastError: { $exists: true } }
// Find all errors in a specific workflow
{ workflowName: 'simpleTask', lastError: { $exists: true } }
```
### Error Properties
The `lastError` object contains:
- `message`: Error message
- `updatedAt`: When the error occurred
### Example Error Object
```js
{
lastError: {
message: "Failed to process payment",
stack: "Error: Payment service unavailable\n at processPayment...",
step: "paymentStep",
timestamp: "2024-03-14T12:34:56.789Z"
}
}
```
### Best Practices
1. **State Management**
- Keep state objects simple and serializable
- Use atomic updates for state changes
- Validate state at each step
- Monitor state size and complexity
2. **Error Handling**
- Monitor errors in logs and database
- Use try/catch in steps that might fail
- Set appropriate timeouts
- Implement retry logic for transient failures
3. **Scalability**
- Design stateless step functions
- Use appropriate queue configurations
- Monitor worker performance
- Implement proper error recovery
4. **Monitoring**
- Track workflow completion rates
- Monitor error frequencies
- Watch queue lengths
- Set up alerts for critical failures
## Example Workflows
Here are some example workflows demonstrating common patterns and use cases. Each example includes a visualization, explanation of the steps, and complete source code.
### Minimal workflow example
This minimal example demonstrates the basic structure of a workflow with just two steps. It shows how to:
- Create a simple workflow definition
- Set and modify workflow state
- Use `goto` to control flow
- Expose a REST endpoint to start the workflow
```mermaid
flowchart TD
A[POST /start] --> |Initial state|B[start step]
B --> C[end step]
C --> |End state|D[Workflow completed]
```
The code below implements this workflow, handling the state and transitions:
```js
import { app } from 'codehooks-js';
// The workflow definition
const workflow = app.createWorkflow('minimal', 'Minimal workflow example', {
start: (state, goto) => {
goto('end', { ...state, message: 'Hello World' });
},
end: (state, goto) => goto(null, state), // null complete the workflow
});
// A REST API to start a new workflow instance
app.post('/start', async (req, res) =>
res.json(await workflow.start({ prop: 'some value' }))
);
export default app.init();
```
### Customer onboarding
Here's a visualization of the customer onboarding workflow:
```mermaid
graph TD
A[START] -->|send welcome email| B[waitForValidation]
B -->|validated: false| B
B -->|validated: true| C[complete]
C -->|goto null = finish| D[End]
style A fill:#2d3748,stroke:#4a5568,stroke-width:2px,color:#e2e8f0
style B fill:#374151,stroke:#4b5563,stroke-width:2px,color:#e2e8f0
style C fill:#1e3d2e,stroke:#2d4d3e,stroke-width:2px,color:#e2e8f0
style D fill:#22543d,stroke:#276749,stroke-width:2px,color:#e2e8f0
classDef waiting fill:#374151,stroke:#4b5563,stroke-width:2px,color:#e2e8f0
class B waiting
```
The workflow shows a process that:
1. Starts with customer email
2. Sends welcome email and enters validation state
3. Stays in validation state (no goto) until email is confirmed
4. Completes when email is validated (goto null = finish)
Here's a practical example of a customer onboarding workflow:
```javascript title="index.js"
import { app } from 'codehooks-js';
const workflow = app.createWorkflow(
'customerOnboarding',
'Customer signup and email validation',
{
START: async function (state, goto) {
// Initialize onboarding state
state = {
...state,
status: 'started',
email: state.email,
validated: false,
startedAt: new Date().toISOString(),
};
// Send welcome email
await sendWelcomeEmail(state.email);
// Move to validation step
goto('waitForValidation', state);
},
waitForValidation: async function (state, goto, wait) {
// Check if email was validated
if (state.validated) {
state.status = 'email_validated';
goto('complete', state);
} else {
// wait for validated = true
wait({ status: 'waiting_for_validation' });
}
},
complete: function (state, goto) {
// Finalize onboarding
state.status = 'completed';
state.completedAt = new Date().toISOString();
goto(null, state); // null === finish
},
}
);
// Event listeners for monitoring
workflow.on('completed', (data) => {
console.log('Onboarding completed for:', data);
});
workflow.on('stepStarted', (data) => {
console.log('Step started:', data.step, 'for customer:', data);
});
// API endpoints
app.post('/start-onboarding', async (req, res) => {
const { email } = req.body;
const result = await workflow.start({ email });
res.send(result);
});
// Endpoint to validate email (called by email webhook)
app.post('/validate-email', async (req, res) => {
const { email, token } = req.body;
// Find the onboarding instance for this email
const instances = await workflow.getInstances({
email: email,
validated: false,
});
if (instances.length > 0) {
const instance = instances[0];
// Update state with validation
const result = await workflow.updateState(
instance._id,
{
validated: true,
validatedAt: new Date().toISOString(),
validationToken: token,
},
{ continue: true } // this Step is complete
);
res.send(result);
} else {
res.status(404).send({ error: 'No pending onboarding found' });
}
});
// Helper function to send welcome email
async function sendWelcomeEmail(email) {
// Implementation of email sending
console.log('Sending welcome email to:', email);
// In a real app, you would use your email service here
}
export default app.init();
```
### Batch Example
This example demonstrates a batch process for managing long-running data operations. It's useful for scenarios like:
- Processing large datasets with multiple validation steps
- Orchestrating complex data transformations across systems
- Managing stateful operations that require error recovery
- Coordinating multi-step data synchronization tasks
- Implementing audit trails for data operations
- Handling distributed transactions with rollback capabilities
- Automating periodic data maintenance and cleanup tasks
- Building resilient ETL (Extract, Transform, Load) pipelines
The flow diagram below shows a three-step process that validates data against an external API, updates a database record, and transfers the result to an archive system. Each step includes error handling and state tracking.
```mermaid
graph TD
A[START] -->|init state| B[checkCustomerStatus]
B -->|check API| C[updateStatus]
C -->|update DB| D[transferData]
D -->|archive data| E[End]
B -->|error| F[error]
C -->|error| F
D -->|error| F
F -->|log error| E
style A fill:#2d3748,stroke:#4a5568,stroke-width:2px,color:#e2e8f0
style B fill:#1e3a3a,stroke:#2d4a4a,stroke-width:2px,color:#e2e8f0
style C fill:#1e3a3a,stroke:#2d4a4a,stroke-width:2px,color:#e2e8f0
style D fill:#1e3a3a,stroke:#2d4a4a,stroke-width:2px,color:#e2e8f0
style F fill:#4a2d2d,stroke:#5a3d3d,stroke-width:2px,color:#e2e8f0
style E fill:#22543d,stroke:#276749,stroke-width:2px,color:#e2e8f0
```
```javascript title="batch.js"
import { app } from 'codehooks-js';
const steps = app.createWorkflow(
'customerCleanup',
'Batch process customer data cleanup',
{
START: async function (state, goto) {
// Initialize batch state
state = {
...state,
customerId: state.customerId,
status: 'started',
startedAt: new Date().toISOString(),
results: {
checkStatus: null,
dataTransfer: null,
},
};
// Move to first step
goto('checkCustomerStatus', state);
},
checkCustomerStatus: async function (state, goto) {
try {
// Call external API to check customer status
const response = await fetch(
'https://api.example.com/customer-status',
{
method: 'POST',
body: JSON.stringify({ customerId: state.customerId }),
}
);
const data = await response.json();
// Update state with check results
state.results.checkStatus = {
status: data.status,
lastActive: data.lastActive,
checkedAt: new Date().toISOString(),
};
// Move to next step
goto('updateStatus', state);
} catch (error) {
state.error = error.message;
goto('error', state);
}
},
updateStatus: async function (state, goto) {
try {
// Update customer status in database
const newStatus =
state.results.checkStatus.lastActive < '2024-01-01'
? 'inactive'
: 'active';
await app.datastore.updateOne(
'customers',
{ customerId: state.customerId },
{ $set: { status: newStatus, updatedAt: new Date() } }
);
state.results.statusUpdated = {
newStatus,
updatedAt: new Date().toISOString(),
};
// Move to final step
goto('transferData', state);
} catch (error) {
state.error = error.message;
goto('error', state);
}
},
transferData: async function (state, goto) {
try {
// Transfer customer data to archive system
const response = await fetch('https://archive.example.com/transfer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
customerId: state.customerId,
status: state.results.statusUpdated.newStatus,
lastActive: state.results.checkStatus.lastActive,
}),
});
state.results.dataTransfer = {
success: response.ok,
transferredAt: new Date().toISOString(),
};
// Complete the process
goto(null, state);
} catch (error) {
state.error = error.message;
goto('error', state);
}
},
error: function (state, goto) {
// Log error and end process
console.error('Batch process failed:', state.error);
state.status = 'failed';
goto(null, state);
},
}
);
// API endpoint to start batch process
app.post('/cleanup-customer', async (req, res) => {
const { customerId } = req.body;
const result = await steps.start({ customerId });
res.send(result);
});
// Helper endpoint to check batch status
app.get('/cleanup-status/:customerId', async (req, res) => {
const instances = await steps.getInstances({
customerId: req.params.customerId,
status: { $ne: 'failed' },
});
if (instances.length > 0) {
res.send(instances[0]);
} else {
res.status(404).send({ error: 'No cleanup process found' });
}
});
export default app.init();
```
This example demonstrates a straight-through batch process that:
1. **Checks Customer Status**
- Calls external API to verify customer status
- Records last active date
- Handles API errors
2. **Updates Database**
- Sets new status based on activity
- Updates customer record
- Tracks update timestamp
3. **Transfers Data**
- Sends data to archive system
- Records transfer success
- Handles transfer errors
### Sub-workflows
Sub-workflows allow you to compose complex workflows by breaking them into smaller, reusable components. A main workflow can start a sub-workflow and wait for it to complete before continuing. This pattern is useful for:
- **Modular Design**: Break complex workflows into smaller, manageable pieces
- **Reusability**: Create sub-workflows that can be used by multiple parent workflows
- **Separation of Concerns**: Keep different business logic in separate workflow definitions
- **Parallel Processing**: Run multiple sub-workflows simultaneously
- **Error Isolation**: Contain errors within sub-workflows without affecting the parent
The example below shows a main workflow that processes a number and then waits for a sub-workflow to complete before finishing. The sub-workflow updates the parent's state to signal completion.
```mermaid
graph TD
%% Main Workflow
Begin[begin] --> Decide[decide]
Decide -->|even| EvenPath[evenPath]
Decide -->|odd| OddPath[oddPath]
EvenPath --> End[end]
OddPath --> End
%% Subflow
SubBegin[subflow.begin] --> SubEnd[subflow.end]
%% Connection between workflows
End -->|wait for subflow| SubBegin
SubEnd -->|update parent state| End
%% Styling
style Begin fill:#2d3748,stroke:#4a5568,stroke-width:2px,color:#e2e8f0
style End fill:#374151,stroke:#4b5563,stroke-width:2px,color:#e2e8f0
style Decide fill:#1e3a3a,stroke:#2d4a4a,stroke-width:2px,color:#e2e8f0
style EvenPath fill:#22543d,stroke:#276749,stroke-width:2px,color:#e2e8f0
style OddPath fill:#22543d,stroke:#276749,stroke-width:2px,color:#e2e8f0
style SubBegin fill:#4a2d3a,stroke:#5a3d4a,stroke-width:2px,color:#e2e8f0
style SubEnd fill:#4a2d3a,stroke:#5a3d4a,stroke-width:2px,color:#e2e8f0
```
The diagram above shows the flow where the main workflow processes a number (even/odd branching), then **waits** for a sub-workflow to complete. The sub-workflow runs independently but communicates back to the parent through state updates. In the implementation below, you'll see how:
- The main workflow starts a sub-workflow and passes its own ID as `parentId`
- The sub-workflow completes its task and updates the parent's state
- The parent workflow continues once it detects the sub-workflow has finished
- Both workflows use the same state management system for coordination
- The main workflow uses the `wait()` function to pause execution until the sub-workflow signals completion
```javascript {54,55,68} title="subflow.js"
import { app } from 'codehooks-js';
// Create a main workflow definition
const workflow = app.createWorkflow('simpleTask', 'Basic workflow example', {
// Step 1: Initialize the workflow
begin: async function (state, goto) {
state = {
message: 'Starting workflow',
// Add a random number to demonstrate branching
value: Math.floor(Math.random() * 10),
};
goto('decide', state);
},
// Step 2: Decide which path to take
decide: async function (state, goto) {
// Branch based on whether the value is even or odd
if (state.value % 2 === 0) {
goto('evenPath', state);
} else {
goto('oddPath', state);
}
},
// Step 3a: Handle even numbers
evenPath: async function (state, goto) {
state = {
message: 'Processing even number',
path: 'even',
processed: true,
};
goto('end', state);
},
// Step 3b: Handle odd numbers
oddPath: async function (state, goto) {
state = {
message: 'Processing odd number',
path: 'odd',
processed: true,
};
goto('end', state);
},
// Step 4: End the workflow
end: async function (state, goto, wait) {
if (state.subflowCompleted) {
state = {
final_message: `Workflow completed! Processed ${state.path} number: ${state.value}`,
};
goto(null, state); // workflow complete
} else {
// Start the subflow and wait for it to complete, set the parentId in the subflow state
await subflow.start({ parentId: state._id });
wait({ wait_message: 'Waiting for subflow to complete' });
}
},
});
// Create a subflow definition
const subflow = app.createWorkflow('subflow', 'Subflow', {
begin: async (state, goto) => {
state.message = 'Hello from subflow';
goto('end', state);
},
end: async (state, goto) => {
// Update the main workflow state to indicate that the subflow has completed
workflow.updateState(
state.parentId,
{
subflowCompleted: true,
},
{ continue: true }
);
goto(null, state);
},
});
// emitted event when a workflow completes
workflow.on('completed', (data) => {
console.log('Workflow completed:', data);
});
// REST API to start a new workflow instance
app.post('/start', async (req, res) => {
const result = await workflow.start({ foo: 'bar' });
res.json(result);
});
// export app interface to serverless execution
export default app.init();
```
## Best Practices
1. **State Management**
- Keep state objects simple and serializable
- Use atomic updates for state changes
- Validate state at each step
- Monitor state size and complexity
2. **Error Handling**
- Monitor errors in logs and database
- Use try/catch in steps that might fail
- Set appropriate timeouts
- Implement retry logic for transient failures
3. **Scalability**
- Design stateless step functions
- Use appropriate queue configurations
- Monitor worker performance
- Implement proper error recovery
4. **Monitoring**
- Track workflow completion rates
- Monitor error frequencies
- Watch queue lengths
- Set up alerts for critical failures
## Related Topics
- [Queue API](/docs/queueapi) - Learn about message queues
- [Job Hooks](/docs/jobhooks) - Schedule recurring tasks
- [Data Operators](/docs/dataoperators) - Work with data in your steps
- [CLI Tools](/docs/cli) - Command-line tools for monitoring workflows and queues
- [workflow-status](/docs/cli#workflow-status) - Visualize and monitor workflow instances
- [queue-status](/docs/cli#queue-status) - Monitor queue health and workers
### Other popular Workflow Engines
- [Temporal](https://temporal.io/) - Open source workflow engine
- [Zeebe](https://camunda.com/platform/zeebe/) - Cloud-native workflow engine
- [AWS Step Functions](https://aws.amazon.com/step-functions/) - Serverless workflow service
- [Zapier](https://zapier.com/) - No-code workflow automation platform
- [Pipedream](https://pipedream.com/) - Developer-first workflow automation
- [n8n](https://n8n.io/) - Open source workflow automation
- [Airflow](https://airflow.apache.org/) - Platform for data pipelines
- [Prefect](https://www.prefect.io/) - Modern workflow orchestration
- [Dagster](https://dagster.io/) - Data orchestration platform
- [Make (Integromat)](https://www.make.com/) - Visual workflow automation
---
## What is a Key-Value Store / Database and how do you use it? Tutorial with examples
*(Note: This content contains MDX/JSX code)*
# What is a Key-Value Store / Database and how do you use it? Tutorial with examples
Key-value stores power some of the world's most popular applications, handling millions of operations per second. This tutorial series shows you how to implement fast, scalable data storage using Codehooks' built-in key-value database. Whether you're building a high-performance cache, managing user sessions, or creating real-time counters, you'll learn how to leverage key-value storage for optimal results.
## What is a Key-Value Store (Database)?
A key-value store is one of the simplest yet most powerful types of NoSQL databases. Think of it like a giant dictionary where you can store any piece of data (the value) under a unique name (the key). Companies like Instagram use key-value stores to handle billions of operations per day, while Discord relies on them to manage real-time state for millions of concurrent users.
For example (keys on the left, value on the right):
```js
// Storing user data
"user:123" -> { name: "John", email: "john@example.com" }
// Storing a simple counter
"pageviews" -> 42
// Storing a session
"session:abc" -> { userId: 123, lastActive: "2024-03-20" }
```
Popular examples of key-value databases include:
- Redis (used by GitHub, Twitter, and Stack Overflow)
- Amazon DynamoDB (powers Amazon's shopping cart)
- Apache Cassandra (handles data at Meta/Facebook)
- etcd (manages Kubernetes clusters)
## Why Use a Key-Value Store?
Key-value stores offer several advantages:
- **Lightning-fast performance**: Data retrieval happens in near-constant time (typically milliseconds)
- **Simple to implement**: No complex schemas or relationships needed - just store and retrieve data by its key
- **Highly scalable**: Perfect for distributed systems and cloud applications
- **Flexible data storage**: Store any type of data as values - strings, numbers, JSON objects, or even binary data
## Common Use Cases
Key-value stores are ideal for:
- Session management (widely used in web applications)
- Caching (significantly reduces database load)
- Real-time counters
- User preferences
- Feature flags
- Shopping carts
- Game states
Lets dive into the simple but powerful world of key-value stores and how you can learn to use them.
## Getting Started with Codehooks Key-Value Store
Codehooks.io has a built-in key-value store (in addition to a NoSQL document database) that can be accessed from serverless JavaScript functions via the `import` statement. The simple code example below shows how the fundamental `get` and `set` functions operates on the [key-value `Datastore` API](/docs/key-value-database-api).
```js {9,10} title="index.js"
import { app, Datastore } from 'codehooks-js';
// Create an API endpoint that manages a counter
app.post('/counter', async (req, res) => {
// Connect to the key-value database
const kv = await Datastore.open();
// Increment a page view counter
const viewCount = (await kv.get('viewCount')) || 0;
await kv.set('viewCount', viewCount + 1);
// note that the two lines above could have been replaced by:
// const viewCount = await kv.incr('viewCount', 1)
res.json({ viewCount });
});
export default app.init();
```
The code example above is a complete serverless backend application that can be be deployed instantly with codehooks.io. Deployment and test with curl is shown in the example commands below.
```sh title="Deploy and test the serverless app from the project directory"
$ coho deploy
Project: keyval-xxxx Space: dev
Deployed Codehook successfully! 🙌
$ curl -X POST \
'https://keyval-xxxx.api.codehooks.io/dev/counter' \
--header 'Accept: */*' \
--header 'x-api-key: 07ae0bba-aaaa-437c-bbbbb-dfd0b991fa0b'
{"viewCount": 1}
```
### Keys are unique and ordered
In the key-value store, each key is a unique `string`.
> A key can only point to one value, and using "set" will always overwrite any existing value.
The example below shows three unique keys with their associated values.
```bash
'My first Key' => 'My first value'
'My second Key' => 'My second value'
'Another key' => 'Another value'
```
Another important feature of the key-value store is how the keys are inserted into the key-value store. Each key string is inserted in a ascending sorted order. When retrieving a single key-value pair the sort order is not relevant. However, as we will cover in another part of this tutorial, when streaming multiple key-value pairs, the sorted stream order can be used to solve many important problems.
### Values are strings
Just as with keys, values are also plain strings.
The content of the value-string can be anything, but is must be encoded to a string before inserted into the key-value store, and (if needed) decoded when retrieving. The example below shows a variation of string values and encoded string values.
```bash
'my_url_key' => 'https://codehooks.io/docs/'
'my_json_key' => '{"foo": "bar"}'
'my_num_key' => '42'
'my_buf_key' => '7b 22 66 6f 6f 22 3a 20 22 62 61 72 22 7d'
'my_user_key' => '4f90d13a42'
```
## Tutorial overview
- **Part 1 - Introduction to key-value stores** (you are here)
- [Part 2 - Basic operations get, set and delete key-values](part-2-basic-operations)
- [Part 3 - Increment and decrement operations](part-3-increment-and-decrement-operations)
- [Part 4 - Working with multiple values and streams](part-4-working-with-multiple-values-and-streams)
- [Part 5 - Managing data with TTL options](part-5-managing-data-with-ttl-options)
- [Part 6 - Multiple key spaces](part-6-multiple-key-spaces)
- [Part 7 - Interaction with the key-val store from the CLI](part-7-key-value-store-interaction-from-cli)
## Summary
Key-value stores are a powerful yet simple way to manage data efficiently, making them ideal for high-performance applications. In this introduction, we've covered the fundamentals of key-value databases, their advantages, and common use cases. We've also demonstrated how easy it is to get started with Codehooks.io's built-in key-value store, showing a practical example of a serverless API that tracks views.
As you progress through this tutorial series, you'll learn how to perform essential operations, work with advanced features like TTL and multiple key spaces, and integrate the key-value store into real-world applications.
This concludes our brief introduction to key-value stores in codehooks.io.
Continue to [Part-2](part-2-basic-operations) to learn how to master the basic operations of the key-value store.
---
import FAQSection from '@site/src/components/FAQSection';
` (create/update), `coho get ` (retrieve), and `coho del ` (delete). You can use wildcards for pattern matching (`coho get 'user-*'` to get all user keys) and set TTL values (`coho set 'key' 'value' --ttl 60000`). The CLI is perfect for database administration and debugging."
},
{
q: "What's the difference between Codehooks key-value store and Redis?",
a: "Codehooks provides a built-in key-value store alongside a NoSQL document database in a serverless environment, with no infrastructure to manage. It supports essential operations like get, set, delete, atomic counters (incr/decr), TTL, sorted keys, and namespaces. However, it offers only a small subset of Redis's extensive features - Redis has advanced data structures (lists, sets, hashes, streams) and hundreds of commands that Codehooks doesn't support. Codehooks is best for straightforward key-value needs within a fully managed backend platform."
}
]}
/>
---
:::info Key-Value Store API documentation
You can find the documentation for the key-value store API [here](/docs/key-value-database-api).
:::
---
## Key-Value Store Basic Operations: Get, Set, Delete | Part 2
*(Note: This content contains MDX/JSX code)*
## Tutorial overview
- [Part 1 - Introduction to key-value store](part-1)
- **[Part 2 - Basic operation get, set and delete key-values](part-2-basic-operations)**
- [Part 3 - Increment and decrement operations](part-3-increment-and-decrement-operations)
- [Part 4 - Working with multiple values and streams](part-4-working-with-multiple-values-and-streams)
- [Part 5 - Managing data with TTL options](part-5-managing-data-with-ttl-options)
- [Part 6 - Multiple key spaces](part-6-multiple-key-spaces)
- [Part 7 - Interaction with the key-val store from the CLI](part-7-key-value-store-interaction-from-cli)
A [key-value store](https://en.wikipedia.org/wiki/Key%E2%80%93value_database) has three basic operations: **get**, **set** and **delete**.
This makes it easy and efficient to insert, look up, update and remove data, as you only need to know the key to access the corresponding value.
In this (part-2) tutorial we'll show how to use the simple [key-value store API](/docs/key-value-database-api) operations in serverless node.js functions.
## Setting and getting values in the key-value store
Inserting (set) a new value in the key-value store requires a unique key string and an arbitrary string value.
`'key' => 'one 2 three'`
The API for setting and getting a value in the key-value store is shown in the code example below:
```js
// Connect to the key-value store
const db = await Datastore.open();
// set key-value
await db.set('mykey', 'My value');
// get value
const result = await db.get('mykey');
console.log(result);
```
Output from the `console.log` statement:
```bash
My value
```
:::tip
The `db.set(key, value, options={})` API has an options part which lets you add a TTL value and a keyspace parameter. This is covered more in the part-5/6 of this tutorial.
:::
### Set a JSON value
Setting a JSON value in the key-value store is easy. Use the `JSON.stringify` function to convert your JavaScript object to a string value before inserting the value to the store. And use the `JSON.parse` function to convert it back to an Object when retrieving the value.
```js
const myObj = {
foo: 'bar',
price: 42,
};
const db = await Datastore.open();
// convert JSON to string
await db.set('myjsonkey', JSON.stringify(myObj));
// get value from key
const result = await db.get('myjsonkey');
// convert back to Object
console.log(JSON.parse(result));
```
Output from the `console.log` statement:
```bash
{ foo: 'bar', price: 42 }
```
### Set a binary value
Using the Node.js `Buffer` library we can set any string or binary value into the key-value store. Converting a buffer value to a `hex` string is a perfect solution for this use case.
```js
const db = await Datastore.open();
// create a hex value from a Buffer
const myBufVal = Buffer.from('Hello world!😎😎😎', 'utf-8').toString('hex');
// set value
await db.set('mybufkey', myBufVal);
// get the value for key
const hexString = await db.get('mybufkey');
// convert hex string value back to a normal string
const stringVal = Buffer.from(hexString, 'hex').toString();
console.log(hexString, stringVal);
```
Output from the `console.log` statement:
```bash
48656c6c6f20776f726c6421f09f988ef09f988ef09f988e Hello world!😎😎😎
```
### Set a number value
Numbers are converted to its string representation.
```js
const db = await Datastore.open();
// set value as Integer
await db.set('mynumkey', 42);
// get value for key
const result = await db.get('mynumkey');
console.log(result, 'is of type', typeof result);
```
Output from the `console.log` statement:
```bash
42 is of type string
```
### Setting multiple values
In some use cases you'll need to insert or set multiple values to the key-value store.
JavaScript solves this task for us with the `Promise.all` function. The key-value store `set` function returns a `Promise`, hence we can create an array of `set` functions that we pass to the `Promise.all` function. The code example below shows you how we can create multiple key-value inserts in one operation.
```js
const db = await Datastore.open();
const array = [
db.set('uno', 'one value'),
db.set('dos', 'two value'),
db.set('tres', 'three value'),
];
const result = await Promise.all(array);
console.log(result);
```
Output from the `console.log` statement:
```bash
[ 'one value', 'two value', 'three value' ]
```
### Getting multiple values
Similar to the technique we use for setting multiple values in the key-value store, we can also get multiple values with the `Promise.all` function.
```js
const db = await Datastore.open();
const array = [db.get('uno'), db.get('dos'), db.get('tres')];
const result = await Promise.all(array);
console.log(result);
```
Output from the `console.log` statement:
```bash
[ 'one value', 'two value', 'three value' ]
```
## Deleting values
Deleting values from the key-value store follows the same pattern as the `get` operation.
When deleleting a key-value pair, the value is returned if the key exists, otherwise a `null` value is returned. You can also use the `Promise.all` technique shown above for multiple delete operations. The code example below shows the basic delete operation.
```js
const db = await Datastore.open();
// delete key-value pair
const result1 = await db.del('uno');
// try to get the deleted key, should be null
const result2 = await db.get('uno');
console.log(result1, result2);
```
Output from the `console.log` statement:
```bash
uno null
```
This part-2 of the key-value store tutorial has shown how you can use the basic operations in serverless JavaScript functions with codehooks.io.
:::info Key-Value Store API documentation
You can find the documentation for the key-value store API [here](/docs/key-value-database-api).
:::
---
## Key-Value Store Counters: Increment & Decrement Operations | Part 3
*(Note: This content contains MDX/JSX code)*
## Tutorial overview
- [Part 1 - Introduction to key-value store](part-1)
- [Part 2 - Basic operation get, set and delete key-values](part-2-basic-operations)
- **[Part 3 - Increment and decrement operations](part-3-increment-and-decrement-operations)**
- [Part 4 - Working with multiple values and streams](part-4-working-with-multiple-values-and-streams)
- [Part 5 - Managing data with TTL options](part-5-managing-data-with-ttl-options)
- [Part 6 - Multiple key spaces](part-6-multiple-key-spaces)
- [Part 7 - Interaction with the key-val store from the CLI](part-7-key-value-store-interaction-from-cli)
This is part-3 of the [key-value database tutorial](part-1) for [codehooks.io](https://codehooks.io) serverless JavaScript. In this part we'll focus on how to increment and decrement values in the built-in key-value database.
Increment and decrement operations are commonly supported in [key-value databases](https://en.wikipedia.org/wiki/Key%E2%80%93value_database), and they can be useful for a variety of applications.
One benefit of increment and decrement operations is that they allow you to update a value in the database without having to read the current value, update it, and then write the updated value back to the database. This can be more efficient than performing these steps separately, especially if the value is being updated frequently.
Increment and decrement operations can also be useful for maintaining counters or other types of numerical data in the database. For example, you could use an increment operation to count the number of times a user has visited a webpage, or you could use a decrement operation to reduce a stock quantity in an online store when an item is purchased.
Overall, increment and decrement operations can provide a simple and efficient way to update numerical values in a key-value database.
## Key-value store API for incrementing and decrementing values
The initial value of an incremented or decremented key-value is always 0. You can increment or decrement a value with any positive `integer`. The minimal code example below shows usage of the two functions `incr` and `decr` with the Datastore API.
```js
// connect to key-value store
const db = await Datastore.open();
// increment value
const val1 = await db.incr('myValue', 1);
// decrement value
const val2 = await db.decr('myValue', 1);
console.log('key-val', val1, val2);
// output: key-val, 1, 1
```
Lets continue with some more realistic and practical code examples.
## Code example: count user API traffic
The code example below shows a simple REST API endpoint that increments a counter in the key-value store each time it's called from a client app.
```js title="index.js"
import app from 'codehooks-js'; // codehooks lib for express style code
// serverless function
async function myFunc(req, res) {
// get userID from the URL, e.g. /myendpoint/9x00fbb
const { userID } = req.params;
// connect to key-value store
const db = await Datastore.open();
// increment api count by userID
const count = await db.incr(`API-count-${userID}`, 1);
res.json({ count }); // returns {"count": 1}, {"count": 2}, etc.
}
// Create a GET endpoint route with the serverless function
app.get('/myendpoint/:userID', myFunc);
export default app.init(); // Bind functions to the serverless cloud
```
Some example values in the key-value store, after some usage of the endpoint, might look like the key-values shown below:
```js
{ key: 'API-count-9x00fbb', value: 4 }
{ key: 'API-count-9x00fea', value: 1 }
{ key: 'API-count-out32xz', value: 5 }
{ key: 'API-count-qar870f', value: 3 }
```
## Code example: reduce stock quantity in an e-commerce online store
Lets say you fill and re-stock your warehouse by calling the key-value store API.
E.g. `db.set('in-stock-TeslaModelZ', 100)`, `db.set('in-stock-TeslaModelX', 45)`
Having an `in-stock` count of our products will enable us to create an endpoint for purchasing and reducing the in-stock count like shown in the code example below.
```js title="index.js"
import app from 'codehooks-js'; // codehooks lib for express style code
app.post('/checkout/:basketID', async (req, res) => {
// get basketID from the URL, e.g. /checkout/xyz123
const { basketID } = req.params;
// get quantity and product from POST body
const { quantity, product } = req.body;
// connect to key-value store
const db = await Datastore.open();
// decrement value
const stock = await db.decr(`in-stock-${product}`, quantity);
// handle case if stock === 0
res.json({ stock, product });
});
export default app.init(); // Bind functions to the serverless cloud
```
The code example below shows how a JavaScript client can `POST` a purchase and a quantity by calling this REST API endpoint:
```js title="client-app.js"
const MY_API_KEY = 'XXXXX'; // NB! Not for production. Use JWT and JWKS for secure API endpoints.
const MY_BASKET_ID = 'xyz123'; // example id
const headersList = {
Accept: '*/*',
'x-api-key': MY_API_KEY,
'Content-Type': 'application/json',
};
const bodyContent = JSON.stringify({
product: 'TeslaModelZ',
quantity: 1,
});
const response = await fetch(
'https://keyvaldemo-f0fe.api.codehooks.io/prod/checkout/' + MY_BASKET_ID,
{
method: 'POST',
body: bodyContent,
headers: headersList,
}
);
const data = await response.json();
console.log(data);
```
Example response:
```js
{
"stock": 97,
"product": "TeslaModelZ"
}
```
This part-3 of the key-value store tutorial has shown how you can use the increment and decrement operations in serverless JavaScript functions with [codehooks.io](https://codehooks.io).
:::info Key-Value Store API documentation
You can find the documentation for the key-value store API [here](/docs/key-value-database-api).
:::
---
## Working with multiple values and streams | Part 4
*(Note: This content contains MDX/JSX code)*
## Tutorial overview
- [Part 1 - Introduction to key-value store](part-1)
- [Part 2 - Basic operation get, set and delete key-values](part-2-basic-operations)
- [Part 3 - Increment and decrement operations](part-3-increment-and-decrement-operations)
- **[Part 4 - Working with multiple values and streams](part-4-working-with-multiple-values-and-streams)**
- [Part 5 - Managing data with TTL options](part-5-managing-data-with-ttl-options)
- [Part 6 - Multiple key spaces](part-6-multiple-key-spaces)
- [Part 7 - Interaction with the key-val store from the CLI](part-7-key-value-store-interaction-from-cli)
Welcome to this **part-4** of the [key-value database tutorial](part-1) for [codehooks.io](https://codehooks.io) serverless [Node.js](https://nodejs.org) backend. In this part we'll focus on how to work with multiple key-value pairs in a [key-value database](https://en.wikipedia.org/wiki/Key%E2%80%93value_database).
The Codehooks.io key-value database stores all keys as a sorted string index. This is a very useful feature if you need to store and retrieve multiple values together, as a group or as a segment of key-values.
## IoT device - time series data example
To illustrate this feature we'll create an example use-case for capturing data from multiple [IoT devices](https://en.wikipedia.org/wiki/Internet_of_things). In this example, each IoT device sends its location, identification and observations to a specific [REST API](https://en.wikipedia.org/wiki/Representational_state_transfer) network endpoint.
For example, a temperature sensor device in the "Alpha region West" reports its value each minute. And a wind force sensor in the "Beta region West" reports its values hourly, and so on.
To ensure that we can access and process the observation data sets effectiviely, we'll create a key string combination that captures all necessary information in one unique key-value pair.
To illustrate this concept lets imagine a series of IoT device observations as shown in the example below.
```bash title="Example IoT device observation data points"
'/Alpha/West/density/2023-01-09T10:14:00.495Z' => '4.4191'
'/Alpha/West/humidity/2023-01-09T17:01:00.821Z' => '0.7539'
'/Alpha/West/temp/2023-01-09T21:15:00.450Z' => '87.100'
'/Beta/West/wind/2023-01-19T21:57:00.316Z' => '5.9477'
...
```
Now that we understand the concept for our IoT database, lets begin the development of an example backend API for IoT sensors.
We'll create a serverless REST API endpoint to receive observations from many IoT devices. The API will store each observation with a logic key that effectively creates a [time series database](https://en.wikipedia.org/wiki/Time_series_database) with data points that we can explore and use in our application.
Lets get started with creating the first API to receive data observations from a device.
## REST API endpoint to POST a single observation
The REST API route takes in 3 parameters for a given device, region, location and device.
And the POST body must contain the actual device observation data point.
For example:
```bash
POST /observation/North/Plaza/humid03
{"observation": "4.02"}
```
The route parameters are combined into a unique key with a time stamp which gives a correct time series for the data points. The following table shows some example device observations.
| IoT devices time series | Data points |
| ---------------------------------------------- | ----------- |
| /East/BuilingA/temp01/2023-02-22T18:30:14.441Z | 21.4 |
| /North/Plaza01/humid3/2023-02-23T05:23:11.306Z | 12.5 |
| /East/BuilingB/temp02/2023-02-22T18:32:38.991Z | 19.0 |
The serverless function below implements a REST API endpoint route that can receive data from a device and insert it to the key-value database.
```js {7}
app.post('/observation/:region/:location/:device', async (req, res) => {
const { region, location, device } = req.params;
const { observation } = req.body;
const key = `/${region}/${location}/${device}/${new Date().toISOString()}`;
// string key e.g. '/North/Plaza/humid03/2023-02-23T05:23:11.306Z'
const conn = await Datastore.open();
const result = await conn.set(key, observation);
console.log(result);
res.status(201).end('Data received ok');
});
```
```bash title="Curl example post of a single observation"
curl -X POST \
'https://.api.codehooks.io/dev/observation/East/BuilingA/temp01' \
--header 'x-apikey: ' \
--header 'Content-Type: application/json' \
--data-raw '{
"observation": "21.4"
}'
```
:::tip
Use the CLI command [`coho info --examples`](../../cli#info) to inspect your project API endpoint and Curl examples.
:::
## Inspecting the key-value database with the CLI
The [Codehooks CLI](../../cli.md#get) can be used to manage the key-value database basic operations, `get`, `set` and `del`. For example, to retrieve all observations for the `East` locations you can run the following command:
```bash
coho get '/East*'
```
This will list all matching keys in the key-value database as the example output shows below.
```bash title="command output"
{
key: '/East/BuilingA/temp01/2023-02-23T05:48:19.646Z',
value: '21.4'
}
{
key: '/East/BuilingA/temp01/2023-02-23T05:49:06.614Z',
value: '20.0'
}
2 keys
```
## Endpoint to GET observations subkey
Similiar to the CLI example above, we can use the [database API](../../nodekeyval.md) `getAll` method to retrieve all key-value pairs that match a start segment of a particular key. In this example we are producing [CSV data format](https://en.wikipedia.org/wiki/Comma-separated_values) for easy spreadsheet analysis etc.
```js {7}
app.get('/observations/:prefix', async (req, res) => {
const conn = await Datastore.open();
const { prefix } = req.params;
let count = 0;
res.set('content-type', 'text/csv');
res.write('Device, Data\n');
const stream = conn.getAll(`/${prefix}`);
stream
.on('data', (data) => {
const outString = `"${data.key}", ${data.val}\n`;
count++;
res.write(outString);
})
.on('end', () => {
res.end();
});
});
```
Testing the API with curl again.
```bash title="Curl example, list time series as CSV data"
curl -X GET \
'https://.api.codehooks.io/dev/observations/Alpha' \
--header 'x-apikey: ' \
```
```csv title="command output CSV data"
Device, Data
"/Alpha/Center/decibel/2023-01-09T11:11:31.218Z", 2.3428
"/Alpha/Center/decibel/2023-01-09T11:12:01.050Z", 6.0632
"/Alpha/Center/decibel/2023-01-09T13:13:30.541Z", 0.7196
"/Alpha/Center/decibel/2023-01-09T15:23:00.589Z", 9.7232
"/Alpha/Center/decibel/2023-01-09T15:34:00.520Z", 5.0089
"/Alpha/Center/decibel/2023-01-09T17:04:00.942Z", 9.1861
```
## Complete source code
The complete source code for our IoT backend application is shown below.
```js title="index.js"
import { app, Datastore } from 'codehooks-js'; // Standard JS lib for express style code
// list a serie of IoT device observations
app.get('/observations/:prefix', async (req, res) => {
const conn = await Datastore.open();
const { prefix } = req.params;
let count = 0;
res.set('content-type', 'text/csv');
res.write('Device, Data\n');
const stream = conn.getAll(`/${prefix}`);
stream
.on('data', (data) => {
const outString = `"${data.key}", ${data.val}\n`;
count++;
res.write(outString);
})
.on('end', () => {
res.write(`Result: ${count}`);
res.end();
});
});
// register a new IoT device observation
app.post('/observation/:region/:location/:device', async (req, res) => {
const { region, location, device } = req.params;
const { observation } = req.body;
// string key e.g. '/North/Plaza/humid03/2023-02-23T05:23:11.306Z'
const key = `/${region}/${location}/${device}/${new Date().toISOString()}`;
const conn = await Datastore.open();
const result = await conn.set(key, observation);
console.log(result);
res.status(201).end('Data received ok');
});
export default app.init(); // Bind functions to the serverless cloud
```
This part-4 of the key-value store tutorial has shown how you can combine smart keys with the database streaming capabilities to create kind of IoT time series database with [codehooks.io](https://codehooks.io).
:::info Key-Value Store API documentation
You can find the documentation for the key-value store API [here](/docs/key-value-database-api).
:::
---
## Key-Value TTL: Auto-Expiring Data & Cache Management | Part 5
*(Note: This content contains MDX/JSX code)*
## Tutorial overview
- [Part 1 - Introduction to key-value store](part-1)
- [Part 2 - Basic operation get, set and delete key-values](part-2-basic-operations)
- [Part 3 - Increment and decrement operations](part-3-increment-and-decrement-operations)
- [Part 4 - Working with multiple values and streams](part-4-working-with-multiple-values-and-streams)
- **[Part 5 - Managing data with TTL options](part-5-managing-data-with-ttl-options)**
- [Part 6 - Multiple key spaces](part-6-multiple-key-spaces)
- [Part 7 - Interaction with the key-val store from the CLI](part-7-key-value-store-interaction-from-cli)
Welcome to this **part-5** of the [key-value database tutorial](part-1) for [codehooks.io](https://codehooks.io) serverless [Node.js](https://nodejs.org) backend. In this part we'll cover the time-to-live (TTL) feature in the Codehooks key-value database.
The TTL database feature allows you to associate an expiration time with each key-value pair. Once the TTL for a particular key expires, the key-value pair is automatically removed from the database.
Some common use cases for the TTL features are listed below:
1. Cache Expiration: Implement TTL in your cache to automatically remove stale data and ensure fresh content is served to users, improving website performance.
2. Session Management: Set TTL for user sessions in your web application to automatically remove expired sessions, enhancing security and freeing up server resources.
3. Temporary Data Storage: Use TTL to store temporary data, such as shopping cart information or form submissions, and automatically remove it after a certain period to avoid cluttering the database.
4. Logging: Apply TTL to store logs or activity data for a limited duration, ensuring the database remains focused on recent and relevant information.
5. Rate Limiting: Utilize TTL to enforce rate limits for API calls or user interactions, automatically resetting counters or restrictions after a specific time interval.
## Example code using the TTL feature
The example code below shows how to create a REST API to POST a temporary state value to a particular key. The state will be kept for 24 hours by the TTL feature.
```js
// keep state for 24 hours
app.post('/state/:id', async (req, res) => {
const ONE_DAY = 86400000; // One day in millis
// get id from request parameter
const { id } = req.params;
// state is the raw JSON body
const state = req.body;
// connect to data store
const conn = await Datastore.open();
const opt = {
ttl: ONE_DAY,
};
// set state with TTL option
const result = await conn.set(`state-${id}`, JSON.stringify(state), opt);
res.json(result);
});
```
Furthermore, the next code example shows how we can create a REST API to retrieve the state value within the 24 hour TTL time frame. If the state exists is is returned, otherwise a 404 status code is returned.
```js
// get state or 404 if TTL expired
app.get('/state/:id', async (req, res) => {
// get id from request parameter
const { id } = req.params;
const conn = await Datastore.open();
// get state from database or NULL if key is deleted
const result = await conn.get(`state-${id}`);
if (result === null) {
res.status(404).send('No state');
} else {
res.set('content-type', 'application/json');
res.send(result);
}
});
```
These simple examples shows how easy it is to implement various use cases with the TTL feature.
Overall, the TTL feature in the key-value database provides flexibility and automation for managing data expiration, allowing you to optimize performance, storage, and overall system efficiency.
:::info Key-Value Store API documentation
You can find the documentation for the key-value store API [here](/docs/key-value-database-api).
:::
---
## Key-Value Namespaces: Isolated Key Spaces & Data Organization | Part 6
*(Note: This content contains MDX/JSX code)*
## Tutorial overview
- [Part 1 - Introduction to key-value store](part-1)
- [Part 2 - Basic operation get, set and delete key-values](part-2-basic-operations)
- [Part 3 - Increment and decrement operations](part-3-increment-and-decrement-operations)
- [Part 4 - Working with multiple values and streams](part-4-working-with-multiple-values-and-streams)
- [Part 5 - Managing data with TTL options](part-5-managing-data-with-ttl-options)
- **[Part 6 - Multiple key spaces](part-6-multiple-key-spaces)**
- [Part 7 - Interaction with the key-val store from the CLI](part-7-key-value-store-interaction-from-cli)
Welcome to this **part-6** of the [key-value database tutorial](part-1) for [codehooks.io](https://codehooks.io) serverless [Node.js](https://nodejs.org) backend.
## Introducing Key-Value with Isolated Key Spaces
Key-Value databases are powerful tools for storing and retrieving data using a simple key-based approach. They offer flexibility, performance, and scalability for various applications. With the concept of isolated key spaces, you can take the benefits of Key-Value databases a step further.
Isolated key spaces allow you to logically separate different sets of keys within the same key-value database. Each isolated key space acts as an independent namespace, providing data isolation and organization. This means you can have multiple sets of keys with the same names, and they won't interfere with each other.
The [key-value database API](/docs/key-value-database-api) supports isolated key spaces for all API methods: `set`, `get`, `getAll`, `incr`, `decr`, `del` and `delAll`.
To set a key-value in an isolated key space you use the `keyspace` option value in each API method.
```js
const conn = await Datastore.open();
const option = {
keyspace: 'foospace',
};
const result = await conn.set('somekey', 'some value', option);
console.log(result);
```
Similarily to get a key-value from the isolated key space the same option applies.
```js
const conn = await Datastore.open();
const option = {
keyspace: 'foospace',
};
const result = await conn.get('somekey', option);
console.log(result);
```
This feature is particularly useful in scenarios where you need to store data from different sources or applications within a single database. By leveraging isolated key spaces, you can maintain data separation, avoid key collisions, and manage your data more efficiently.
Isolated key spaces enable you to:
- Organize Data: Group related keys together within isolated key spaces, making it easier to manage and maintain your data.
- Prevent Key Collisions: With isolated key spaces, keys with the same name can coexist without conflicts, as they belong to separate namespaces.
- Achieve Data Isolation: Keep data from different applications or components separate, ensuring integrity and avoiding unintended data interactions.
- Simplify Access Control: Apply different access permissions and security measures to each isolated key space, providing granular control over data access.
- Enhance Scalability: Isolated key spaces allow for horizontal scaling by distributing data across multiple instances or clusters.
By utilizing key-value databases with isolated key spaces, you can improve data organization, scalability, and security while maintaining the simplicity and efficiency of key-value storage.
:::info Key-Value Store API documentation
You can find the documentation for the key-value store API [here](/docs/key-value-database-api).
:::
---
## Key-Value CLI Commands: Command Line Database Management | Part 7
*(Note: This content contains MDX/JSX code)*
## Tutorial overview
- [Part 1 - Introduction to key-value store](part-1)
- [Part 2 - Basic operation get, set and delete key-values](part-2-basic-operations)
- [Part 3 - Increment and decrement operations](part-3-increment-and-decrement-operations)
- [Part 4 - Working with multiple values and streams](part-4-working-with-multiple-values-and-streams)
- [Part 5 - Managing data with TTL options](part-5-managing-data-with-ttl-options)
- [Part 6 - Multiple key spaces](part-6-multiple-key-spaces)
- **[Part 7 - Interaction with the key-val store from the CLI](part-7-key-value-store-interaction-from-cli)**
Welcome to this **part-7** of the [key-value database tutorial](part-1) for [codehooks.io](https://codehooks.io) serverless [Node.js](https://nodejs.org) backend.
In this section we'll cover how you can work with the key-value database using the Command Line Interface (CLI).
The Codehooks.io [CLI](/docs/cli) is an essential component of many developers' workflows, this is also true when working with the key-value database.
The CLI har 3 basic operations for working with the key-value database:
1. Set a value: `coho set `
2. Get a value: `coho get `
3. Delete a value: `coho del `
Let's go through some simple CLI examples showing how to work with the database keys and values.
## Setting a new key-value
```bash
coho set 'my-first-value' 'This is the first value'
# CLI output
{ key: 'my-first-value', value: 'This is the first value' }
```
## Setting another key-value
```bash
coho set 'my-second-value' 'This is the second value'
# CLI output
{ key: 'my-second-value', value: 'This is the second value' }
```
## Getting a key-value
```bash
coho get 'my-second-value'
# CLI output
{ key: 'my-second-value', value: 'This is the second value' }
```
## Getting multiple key-value(s) by key pattern
```bash
coho get 'my-*'
# CLI output
{ key: 'my-first-value', val: 'This is the first value' }
{ key: 'my-second-value', val: 'This is the second value' }
2 keys
```
## Setting a key-value with Time To Live (TTL)
```bash
coho set 'my-short-lived-value' 'I will be gone in 60 seconds' --ttl 60000
# CLI output
{ key: 'my-short-lived-value', value: 'I will be gone in 60 seconds' }
```
## Deleting a key-value
```bash
coho del 'my-first-value'
# CLI output
{ key: 'my-first-value' }
```
This concludes our seven parts tutorial on how to develop with the key-value store in codehooks.io.
:::info Key-Value Store API documentation
You can find the documentation for the key-value store API [here](/docs/key-value-database-api).
:::
---
## Example Applications
*(Note: This content contains MDX/JSX code)*
import Admonition from '@theme/Admonition';
import GitHubIcon from '@mui/icons-material/GitHub';
Complete, production-ready applications you can fork and customize. These showcase full-stack implementations with frontend and backend code.
## Codehooks Analytics
Codehooks Analytics is an open-source web analytics tool for tracking website and app traffic, giving you full control over your data and dashboard. You can deploy it as-is, customize it, or build your own solution.
Unlike third-party services, this tool avoids costly per-event fees, offers real-time data access, and eliminates vendor lock-in. It does not use cookies like most other analytics solutions.
Built with [Codehooks.io](https://codehooks.io) for the backend and [Alpine.js](https://alpinejs.dev/), [TailwindCSS](https://tailwindcss.com/), and [DaisyUI](https://daisyui.com/) for the frontend, it's easy to use and modify. The Codehooks.io free Developer plan covers most use cases unless your site is very busy.
- **Codehooks.io**: Simplifies API and database management.
- **Alpine.js**: Adds interactivity with minimal complexity.
- **TailwindCSS**: Enables rapid UI development.
- **DaisyUI**: Provides pre-built components for faster design.
- **OpenAI** API: Provides daily AI insights from your data.
}
title="Get started with the Codehooks analytics template on Github"
>
All you need to host the whole solution and start tracking your custom
analytics data is a codehooks.io project/space and the free Development plan.
The source code and complete installation and setup instructions can be found
in [the Github repository](https://github.com/RestDB/webanalytics). Feel free
to fork it, submit pull requests or give it a star.
---
## Directory Template
This template contains everything you need to build your own directory service. Directory services are crucial for organizing and presenting information in a structured way. They're used for everything from product catalogs to service listings, and their success often depends on two key factors:
- SEO optimization for discoverability
- Developer-friendly implementation
Built-in features:
- Server-side rendering using HTML/Handlebars
- Built-in SEO optimization
- Modern UI using DaisyUI and Tailwind CSS
- Automatic screenshot generation
- Sitemap generation
- Full client-side text search with lunrjs
- Mock data support for rapid development
- Easy deployment to Codehooks.io
### Examples in the wild
- [AllToolz.dev](https://alltoolz.dev) - a directory of developer tools
- [DigiHub.no](https://bergen.digihub.no) - a directory of web agencies and consulting companies in Bergen, Norway
}
title="Get started with the Directory template on Github"
>
All you need is a codehooks.io project/space to start creating your own
directory service. The source code and complete installation and setup
instructions can be found in [the Github
repository](https://github.com/RestDB/directory-template).
---
## Other Integrations
*(Note: This content contains MDX/JSX code)*
import { styled } from '@mui/material/styles';
import alpineLogo from './images/alpine-logo.png';
import auth0Logo from './images/auth0-logo.png';
import chatgptLogo from './images/chatgpt-logo.png';
import graphQlLogo from './images/graphql-logo.png';
import Link from '@docusaurus/Link';
import mailgunLogo from './images/mailgun-logo.png';
import mongodbLogo from './images/mongodb-logo.png';
import reactLogo from './images/react-logo.png';
export const nextLogo = '/img/webhooks/next-logo.svg';
import svelteLogo from './images/svelte-logo.png';
import s3Logo from './images/s3-logo.png';
export const SmallFeature = ({
image,
link = '#',
title = 'No title',
content =
body text here
,
nextsteps = [],
}) => {
return (
{title}
{content}
{nextsteps.length > 0 && (
Examples
{nextsteps.map((step, i) => (
{step}
))}
)}
);
};
Step-by-step guides showing how to integrate Codehooks with popular frameworks and services. These examples demonstrate basic setups and best practices for specific technologies.
:::tip Build anything fast with AI coding agents
Need a custom integration? With coding agents like Claude, Cursor, or ChatGPT, you can build it in minutes. Check out our [AI agent setup](/docs/ai-agent-setup) to get started.
:::
## Frontend Frameworks
Learn how to use codehooks.io as an API backend for your React app
,
]}
/>
Next.js todo app with complete code example
,
More coming up soon ...,
]}
/>
Svelte todo app with complete code example
,
More coming up soon ...,
]}
/>
Examine how Alpine.js' lightweight approach complements Codehooks.io's
backend capabilities. We'll look at building interactive web components
with easy backend integration.
}
nextsteps={[
Tutorial: create an Alpine.js app with a NoSQL backend
,
DaisyUI + Alpine.js + Codehooks.io - the simple web app trio
,
]}
/>
## APIs & Data
See how Codehooks.io can act as a complete GraphQL backend. We'll
demonstrate handling GraphQL queries and mutations and storing data in a
codehooks database.
}
nextsteps={[
How to create a GraphQL-powered persistent backend with codehooks.io's
built-in NoSQL database
,
]}
/>
Discover how Codehooks.io integrates with MongoDB, giving you full
control of data ownership and management. Our examples will show you the
straightforward process of connecting Codehooks.io with your MongoDB
setup.
}
nextsteps={[
Connecting to an external MongoDB database
,
]}
/>
## Authentication & Security
Understand the integration of Auth0 with Codehooks.io for
authentication. We'll delve into secure authentication flow
implementations, emphasizing JWT and security best practices.
}
nextsteps={[
Learn about authentication with Codehooks.io
,
]}
/>
## Cloud Services
Learn about leveraging Codehooks.io with AWS S3 for data storage
solutions. We'll explore simple file retrieval and storage.
}
nextsteps={[
Build an API for file upload and download with AWS S3 (v3)
,
]}
/>
Dive into the integration of Mailgun for email functionality in your
applications with Codehooks.io.
}
nextsteps={[
Integrating with Mailgun REST API
,
]}
/>
## AI & Automation
Here we show you ways you can get ChatGPT to code the backend for you.
You can also explore how to add AI-powered chat functionalities with
ChatGPT. We'll cover integration techniques for incorporating
conversational AI into your applications.
}
nextsteps={[
Set up AI agents to build Backend APIs with codehooks.io
,
]}
/>
---
## Templates & Examples
*(Note: This content contains MDX/JSX code)*
import { useState } from 'react';
import Link from '@docusaurus/Link';
import Admonition from '@theme/Admonition';
import GitHubIcon from '@mui/icons-material/GitHub';
export const stripeLogo = '/img/webhooks/stripe-logo.svg';
export const paypalLogo = '/img/webhooks/paypal-logo.svg';
export const shopifyLogo = '/img/webhooks/shopify-logo.svg';
export const githubLogo = '/img/webhooks/github-logo.svg';
export const discordLogo = '/img/webhooks/discord-logo.svg';
export const twilioLogo = '/img/webhooks/twilio-logo.svg';
export const slackLogo = '/img/webhooks/slack-logo.svg';
export const clerkLogo = '/img/webhooks/clerk-logo.svg';
export const FilterTabs = ({ activeFilter, setActiveFilter }) => {
const filters = ['All', 'Backend', 'Payments', 'Communication', 'Developer Tools', 'Auth'];
return (
);
};
export const TemplatesGrid = () => {
const [activeFilter, setActiveFilter] = useState('All');
const templates = [
{
title: 'CRUD API Backend',
description: 'Simple CRUD API database backend using the Codehooks NoSQL database REST API. Perfect starting point for any application needing a database backend.',
command: 'coho create myapi --template crud-api-backend',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/crud-api-backend',
category: 'Backend',
},
{
title: 'React Backend-for-Frontend',
description: 'React frontend and backend deployed together following the BFF pattern. Vite-powered with lowest latency to your API and database.',
command: 'coho create myreactbff --template react-bff',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/react-bff',
category: 'Backend',
},
{
title: 'React Admin Dashboard',
description: 'Data-driven admin dashboard with CRUD REST API, OpenAPI/Swagger docs, JWT authentication, role-based access, visual datamodel editor, and AI-assisted schema design. Built with React, Vite, Tailwind CSS, and shadcn/ui.',
command: 'coho create myadmin --template react-admin-dashboard',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/react-admin-dashboard',
tutorialUrl: '/react-admin-dashboard',
category: 'Backend',
},
{
title: 'Static Website with Tailwind',
description: 'Deploy static websites instantly with Tailwind CSS included. Perfect for landing pages, documentation sites, or any static content.',
command: 'coho create mysite --template static-website-tailwindcss',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/static-website-tailwindcss',
category: 'Backend',
},
{
title: 'Drip Email Workflow',
description: 'Production-ready email automation system. Own your drip campaigns without vendor lock-in. JSON-configured steps with SendGrid, Mailgun, or Postmark.',
command: 'coho create mydrip --template drip-email-workflow',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/drip-email-workflow',
tutorialUrl: '/drip-email-campaign',
category: 'Communication',
},
{
title: 'SaaS Metering Webhook',
description: 'Usage-based billing infrastructure for SaaS applications. Track API calls, compute usage, or any metered resource with webhook-driven event collection.',
command: 'coho create mymetering --template saas-metering-webhook',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/saas-metering-webhook',
tutorialUrl: '/saas-metering-webhook',
category: 'Payments',
},
{
image: stripeLogo,
title: 'Stripe Webhook Handler',
description: 'Production-ready Stripe webhook with signature verification and automatic event storage. Handle payments, subscriptions, and customer events.',
command: 'coho create mystripe --template stripe-webhook-handler',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/stripe-webhook-handler',
tutorialUrl: '/docs/examples/webhooks/stripe',
category: 'Payments',
},
{
image: stripeLogo,
title: 'Stripe (Minimal)',
description: 'Minimal Stripe payment webhook with signature validation. Process payment events with just the essentials.',
command: 'coho create mystripe --template webhook-stripe-minimal',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-stripe-minimal',
tutorialUrl: '/docs/examples/webhooks/stripe',
category: 'Payments',
},
{
image: paypalLogo,
title: 'PayPal',
description: 'PayPal payment webhook with signature verification. Handle payments, refunds, and dispute events.',
command: 'coho create mypaypal --template webhook-paypal-minimal',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-paypal-minimal',
tutorialUrl: '/docs/examples/webhooks/paypal',
category: 'Payments',
},
{
image: shopifyLogo,
title: 'Shopify',
description: 'Shopify e-commerce event processor with HMAC verification. Handle orders, inventory, and customer events.',
command: 'coho create myshopify --template webhook-shopify-minimal',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-shopify-minimal',
tutorialUrl: '/docs/examples/webhooks/shopify',
category: 'Payments',
},
{
image: slackLogo,
title: 'Slack Memory Bot',
description: 'Advanced Slack bot with persistent memory and pluggable search adapters. Demonstrates webhook handling and modular architecture.',
command: 'coho create myslackbot --template slack-memory-bot',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/slack-memory-bot',
tutorialUrl: '/docs/examples/webhooks/slack',
category: 'Communication',
},
{
image: discordLogo,
title: 'Discord',
description: 'Discord bot interaction handler with Ed25519 verification. Build interactive Discord bots and slash commands.',
command: 'coho create mydiscord --template webhook-discord-minimal',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-discord-minimal',
tutorialUrl: '/docs/examples/webhooks/discord',
category: 'Communication',
},
{
image: twilioLogo,
title: 'Twilio',
description: 'Twilio SMS/voice event handler with TwiML response support. Process incoming messages and calls.',
command: 'coho create mytwilio --template webhook-twilio-minimal',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-twilio-minimal',
tutorialUrl: '/docs/examples/webhooks/twilio',
category: 'Communication',
},
{
image: githubLogo,
title: 'GitHub',
description: 'GitHub event handler with HMAC SHA-256 verification. Automate workflows based on repository events.',
command: 'coho create mygithub --template webhook-github-minimal',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-github-minimal',
tutorialUrl: '/docs/examples/webhooks/github',
category: 'Developer Tools',
},
{
image: '/img/webhooks/webhook-icon.svg',
title: 'Webhook Delivery System',
description: 'Complete webhook delivery infrastructure for sending webhooks to your customers. Queue-based with automatic retries and HMAC signing.',
command: 'coho create mywebhooks --template webhook-delivery',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-delivery',
tutorialUrl: '/webhook-delivery-system',
category: 'Developer Tools',
},
{
image: '/img/webhooks/webhook-icon.svg',
title: 'Webhook Inspector',
description: 'Catch, inspect, and replay incoming webhooks with a dark mode UI. Self-hosted RequestBin alternative with cURL export and raw body preservation.',
command: 'coho create myinspector --template webhook-inspector',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-inspector',
tutorialUrl: '/webhook-inspector',
category: 'Developer Tools',
},
{
image: clerkLogo,
title: 'Clerk',
description: 'Clerk authentication event handler using Svix verification. React to user signup, login, and profile updates.',
command: 'coho create myclerk --template webhook-clerk-minimal',
templateUrl: 'https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-clerk-minimal',
category: 'Auth',
},
];
return (
<>
{templates.map((template, index) => (
))}
>
);
};
Production-ready templates for backends, webhooks, APIs, workflows, and more. Pick a template, run the command, and you're live. You can also just copy/paste the code into your index.js or index.ts file and deploy.
:::tip Looking for webhook integration tutorials?
For comprehensive guides on implementing webhooks for each service, check out our [Webhook Integration Examples](/docs/examples/webhooks).
:::
All templates are open source and available in the [codehooks-io-templates](https://github.com/RestDB/codehooks-io-templates) GitHub repository. Feel free to fork, modify, or contribute!
---
## Codehooks Authentication
**[codehooks-auth](https://www.npmjs.com/package/codehooks-auth)** is our official authentication library - a complete, open-source authentication solution that gives you full control over your authentication process and user data.
Built with security and flexibility in mind, it supports multiple authentication methods:
- One-time password (OTP) authentication
- OAuth integration (Google and GitHub)
- JWT-based access and refresh tokens
- Email-based user verification
The authentication system integrates seamlessly with:
- **Codehooks.io**: For backend API and database management
- **Multiple email providers**: Including Mailgun, Postmark, and SendGrid
- **Custom templates**: Fully customizable HTML/CSS templates for all auth pages
- **OAuth providers**: Ready-to-use integrations with major providers
}
title="Get started with Codehooks Authentication"
>
Install via npm: `npm install codehooks-auth`. The library is available as an [npm package](https://www.npmjs.com/package/codehooks-auth) with complete documentation and examples in [the Github repository](https://github.com/RestDB/codehooks-auth).
---
## Example Applications
Complete, production-ready applications you can fork and customize. These showcase full-stack implementations with frontend and backend code.
Codehooks Analytics
Open-source web analytics with full control over your data. Cookie-free tracking, real-time access, no vendor lock-in. Built with Alpine.js, TailwindCSS, and DaisyUI.
View Examples →
Directory Template
Build SEO-optimized directory services with server-side rendering, automatic screenshots, sitemap generation, and full-text search.
View Examples →
---
## Discord Webhooks Integration Example: Slash Commands Without WebSocket
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# Discord Webhooks Integration Example: Slash Commands Without WebSocket
Discord bots can use webhooks to handle slash commands, buttons, and interactions - no persistent WebSocket connection required. This serverless approach is perfect for command-based bots that don't need to monitor messages in real-time.
**What are Discord Interactions?** When users trigger slash commands (`/command`), click buttons, or use select menus, Discord sends these events to your webhook endpoint instead of requiring a WebSocket gateway connection.
**The problem?** Traditional Discord bots require maintaining a persistent WebSocket connection, which means running a server 24/7 even when idle.
**The solution?** Use Discord's Interactions Endpoint URL to receive events via webhooks. Your endpoint only runs when users interact with your bot - perfect for serverless.
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
## Quick Deploy with Template
The fastest way to get started is using the Codehooks Discord bot template:
```bash
coho create mydiscordbot --template webhook-discord-minimal
```
You'll see output like:
```
Creating project: mydiscordbot
Project created successfully!
Your API endpoints:
https://mydiscordbot-a1b2.api.codehooks.io/dev
```
Then install, deploy, and configure:
```bash
cd mydiscordbot
npm install
coho deploy
coho set-env DISCORD_PUBLIC_KEY your-public-key-here --encrypted
```
```
Project: mydiscordbot-a1b2 Space: dev
Deployed Codehook successfully! 🙌
```
:::tip Finding your API endpoint
Run `coho info` to see your API endpoints. Your interactions URL will be:
`https://mydiscordbot-a1b2.api.codehooks.io/dev/interactions`
:::
## Custom Implementation
If you prefer to build from scratch, create a Discord interaction handler:
```javascript
import { app } from 'codehooks-js';
import crypto from 'crypto';
const DISCORD_PUBLIC_KEY = process.env.DISCORD_PUBLIC_KEY;
// Bypass API key auth for Discord endpoint (Discord uses Ed25519 signatures)
app.auth('/interactions', (req, res, next) => next());
// Verify Discord Ed25519 signature
function verifyDiscordSignature(req) {
const signature = req.headers['x-signature-ed25519'];
const timestamp = req.headers['x-signature-timestamp'];
if (!signature || !timestamp || !DISCORD_PUBLIC_KEY) {
return false;
}
try {
const message = timestamp + req.rawBody;
return crypto.verify(
null,
Buffer.from(message),
{
key: Buffer.from(DISCORD_PUBLIC_KEY, 'hex'),
format: 'der',
type: 'spki'
},
Buffer.from(signature, 'hex')
);
} catch (err) {
console.error('Signature verification failed:', err.message);
return false;
}
}
// Discord interactions endpoint
app.post('/interactions', async (req, res) => {
// Verify the request is from Discord
if (!verifyDiscordSignature(req)) {
return res.status(401).send('Invalid signature');
}
const interaction = req.body;
// Type 1: PING - Discord verifies your endpoint
if (interaction.type === 1) {
return res.json({ type: 1 }); // PONG
}
// Type 2: Application Command (slash commands)
if (interaction.type === 2) {
const commandName = interaction.data.name;
const userId = interaction.member?.user?.id || interaction.user?.id;
console.log(`Command /${commandName} from user ${userId}`);
switch (commandName) {
case 'ping':
return res.json({
type: 4, // CHANNEL_MESSAGE_WITH_SOURCE
data: {
content: '🏓 Pong! Bot is running on Codehooks.io'
}
});
case 'hello':
return res.json({
type: 4,
data: {
content: `Hello <@${userId}>! 👋`
}
});
case 'info':
return res.json({
type: 4,
data: {
embeds: [{
title: 'Bot Information',
color: 0x5865F2, // Discord blurple
fields: [
{ name: 'Platform', value: 'Codehooks.io', inline: true },
{ name: 'Type', value: 'HTTP Interactions', inline: true },
{ name: 'Latency', value: 'Serverless', inline: true }
],
footer: { text: 'Built with Codehooks.io' }
}]
}
});
default:
return res.json({
type: 4,
data: {
content: `Unknown command: /${commandName}`
}
});
}
}
// Type 3: Message Component (buttons, select menus)
if (interaction.type === 3) {
const customId = interaction.data.custom_id;
console.log(`Button clicked: ${customId}`);
return res.json({
type: 4,
data: {
content: `You clicked: ${customId}`,
flags: 64 // Ephemeral (only visible to user)
}
});
}
// Type 5: Modal Submit
if (interaction.type === 5) {
const modalId = interaction.data.custom_id;
const fields = interaction.data.components;
console.log(`Modal submitted: ${modalId}`);
return res.json({
type: 4,
data: {
content: 'Form submitted successfully!',
flags: 64
}
});
}
res.status(400).json({ error: 'Unknown interaction type' });
});
export default app.init();
```
## Set Up Your Discord Application
### 1. Create a Discord Application
1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
2. Click **New Application**
3. Enter a name and create
### 2. Get Your Public Key
1. Go to **General Information**
2. Copy the **Public Key** (you'll need this for signature verification)
3. Set it in Codehooks:
```bash
coho set-env DISCORD_PUBLIC_KEY your-public-key-here
```
### 3. Set the Interactions Endpoint URL
1. In the Developer Portal, go to **General Information**
2. Enter your **Interactions Endpoint URL**: `https://mydiscordbot-a1b2.api.codehooks.io/dev/interactions`
3. Click **Save Changes**
Discord will send a PING request to verify your endpoint. If signature verification is working, it will save successfully.
### 4. Register Slash Commands
Register your commands using the Discord API:
```javascript
// Run this once to register commands (not part of your bot)
const DISCORD_APP_ID = 'your-application-id';
const DISCORD_BOT_TOKEN = 'your-bot-token';
const commands = [
{
name: 'ping',
description: 'Check if the bot is running'
},
{
name: 'hello',
description: 'Get a friendly greeting'
},
{
name: 'info',
description: 'Get bot information'
}
];
// Register global commands
await fetch(`https://discord.com/api/v10/applications/${DISCORD_APP_ID}/commands`, {
method: 'PUT',
headers: {
'Authorization': `Bot ${DISCORD_BOT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(commands)
});
console.log('Commands registered!');
```
### 5. Invite the Bot to Your Server
1. Go to **OAuth2** → **URL Generator**
2. Select scopes: `applications.commands`
3. Copy the URL and open it to invite the bot
## Interaction Types
| Type | Name | Description |
|------|------|-------------|
| 1 | PING | Discord verifying your endpoint |
| 2 | APPLICATION_COMMAND | Slash commands |
| 3 | MESSAGE_COMPONENT | Buttons, select menus |
| 4 | APPLICATION_COMMAND_AUTOCOMPLETE | Autocomplete suggestions |
| 5 | MODAL_SUBMIT | Modal form submissions |
## Response Types
| Type | Name | Description |
|------|------|-------------|
| 1 | PONG | Response to PING |
| 4 | CHANNEL_MESSAGE_WITH_SOURCE | Send a message |
| 5 | DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE | Acknowledge, send message later |
| 6 | DEFERRED_UPDATE_MESSAGE | Acknowledge component, edit later |
| 7 | UPDATE_MESSAGE | Edit the original message |
| 8 | APPLICATION_COMMAND_AUTOCOMPLETE_RESULT | Autocomplete results |
| 9 | MODAL | Open a modal dialog |
## Handling Buttons and Select Menus
Send interactive components and handle clicks:
```javascript
// Command that sends a button
case 'button-demo':
return res.json({
type: 4,
data: {
content: 'Click a button:',
components: [{
type: 1, // Action Row
components: [
{
type: 2, // Button
style: 1, // Primary (blue)
label: 'Click Me',
custom_id: 'button_click'
},
{
type: 2,
style: 4, // Danger (red)
label: 'Delete',
custom_id: 'button_delete'
}
]
}]
}
});
// Handle button clicks (type 3)
if (interaction.type === 3) {
const customId = interaction.data.custom_id;
if (customId === 'button_click') {
return res.json({
type: 7, // UPDATE_MESSAGE
data: {
content: '✅ Button was clicked!',
components: [] // Remove buttons
}
});
}
if (customId === 'button_delete') {
return res.json({
type: 7,
data: {
content: '🗑️ Deleted!',
components: []
}
});
}
}
```
## Deferred Responses for Long Operations
For operations taking more than 3 seconds, defer the response:
```javascript
import { Datastore } from 'codehooks-js';
case 'slow-command':
// Acknowledge immediately (user sees "Bot is thinking...")
res.json({
type: 5 // DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
});
// Do slow work
const result = await doSlowOperation();
// Send follow-up message
const webhookUrl = `https://discord.com/api/v10/webhooks/${interaction.application_id}/${interaction.token}/messages/@original`;
await fetch(webhookUrl, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: `Operation complete: ${result}`
})
});
return;
```
:::warning 3-Second Timeout
Discord requires an initial response within 3 seconds. For longer operations, use deferred responses (type 5). You then have 15 minutes to send the actual response.
:::
## Ed25519 Signature Verification
Discord signs all interaction payloads with Ed25519. You **must** verify signatures:
```javascript
import crypto from 'crypto';
function verifyDiscordSignature(req) {
const signature = req.headers['x-signature-ed25519'];
const timestamp = req.headers['x-signature-timestamp'];
const publicKey = process.env.DISCORD_PUBLIC_KEY;
if (!signature || !timestamp || !publicKey) {
return false;
}
try {
// Concatenate timestamp + raw body
const message = timestamp + req.rawBody;
// Verify using Ed25519
return crypto.verify(
null,
Buffer.from(message),
{
key: Buffer.from(publicKey, 'hex'),
format: 'der',
type: 'spki'
},
Buffer.from(signature, 'hex')
);
} catch (err) {
return false;
}
}
```
**Important:** Discord periodically sends invalid signatures to test your verification. If you fail, Discord will disable your endpoint and notify you.
## HTTP Bots vs Gateway Bots
| Feature | HTTP (Interactions) | Gateway (WebSocket) |
|---------|---------------------|---------------------|
| Connection | On-demand | Always connected |
| Hosting | Serverless friendly | Requires persistent server |
| Slash commands | ✅ Yes | ✅ Yes |
| Buttons/Menus | ✅ Yes | ✅ Yes |
| Read messages | ❌ No | ✅ Yes |
| Presence updates | ❌ No | ✅ Yes |
| Voice | ❌ No | ✅ Yes |
| Reactions | ❌ No | ✅ Yes |
| Cost | Pay per use | 24/7 server costs |
**Use HTTP Interactions when:**
- Building command-based bots
- Using serverless platforms
- Bot doesn't need to monitor messages
**Use Gateway when:**
- Bot needs to read/react to messages
- Building moderation bots
- Need presence or voice features
## Why Use Codehooks.io for Discord Bots?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours | 5 minutes |
| Server management | Configure & maintain | Serverless |
| Signature verification | Implement Ed25519 | Built-in support |
| Database for bot data | Set up separately | Built-in |
| Scaling | Manual configuration | Automatic |
| Cost | 24/7 server costs | Pay per interaction |
## Related Resources
- [Discord Bot Template](https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-discord-minimal) - Ready-to-deploy template
- [Discord Developer Portal](https://discord.com/developers/applications)
- [Discord Interactions Documentation](https://discord.com/developers/docs/interactions/receiving-and-responding)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [Slack Bot Webhooks Example](/docs/examples/webhooks/slack) - Another chat platform integration
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Payment webhook handler
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## GitHub Webhooks Integration Example: Automate Repository & CI/CD Events
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# GitHub Webhooks Integration Example: Automate Repository & CI/CD Events
GitHub webhooks notify your application when events occur in your repositories — code is pushed, pull requests are opened, issues are created, releases are published, and more. They're essential for CI/CD pipelines, automated workflows, and integrations.
**The problem?** Setting up GitHub webhooks requires signature verification, handling various event types, and managing payload parsing.
**The solution?** Deploy a production-ready GitHub webhook handler with Codehooks.io in under 5 minutes using our ready-made template.
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
## Quick Deploy with Template
The fastest way to get started is using the Codehooks GitHub webhook template:
```bash
coho create mygithubhandler --template webhook-github-minimal
```
You'll see output like:
```
Creating project: mygithubhandler
Project created successfully!
Your API endpoints:
https://mygithubhandler-a1b2.api.codehooks.io/dev
```
Then install, deploy, and configure:
```bash
cd mygithubhandler
npm install
coho deploy
coho set-env GITHUB_WEBHOOK_SECRET your-webhook-secret --encrypted
```
That's it! The template includes signature verification, event handling, and common webhook patterns.
:::tip Use the Template
The `webhook-github-minimal` template is production-ready with proper signature verification and event handling. Browse all available templates in the [templates repository](https://github.com/RestDB/codehooks-io-templates).
:::
## Custom Implementation
If you prefer to build from scratch, create a new project and add your GitHub webhook handler code to `index.js`:
```javascript
import { app } from 'codehooks-js';
import { Datastore } from 'codehooks-js';
import crypto from 'crypto';
const GITHUB_WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
// Verify GitHub webhook signature (SHA-256)
function verifyGitHubWebhook(req) {
const signature = req.headers['x-hub-signature-256'];
if (!signature) return false;
const body = req.rawBody;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', GITHUB_WEBHOOK_SECRET)
.update(body)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (e) {
return false;
}
}
// GitHub webhook endpoint
app.post('/github/webhooks', async (req, res) => {
// Verify the webhook signature
if (!verifyGitHubWebhook(req)) {
console.error('Invalid GitHub webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.headers['x-github-event'];
const deliveryId = req.headers['x-github-delivery'];
const payload = req.body;
console.log(`Received ${event} event (${deliveryId})`);
// Handle different GitHub events
switch (event) {
case 'push':
await handlePush(payload);
break;
case 'pull_request':
await handlePullRequest(payload);
break;
case 'issues':
await handleIssue(payload);
break;
case 'release':
await handleRelease(payload);
break;
case 'workflow_run':
await handleWorkflowRun(payload);
break;
case 'ping':
console.log('Ping received! Webhook configured successfully.');
break;
default:
console.log(`Unhandled event: ${event}`);
}
res.json({ received: true });
});
async function handlePush(payload) {
const { repository, pusher, commits, ref } = payload;
const branch = ref.replace('refs/heads/', '');
console.log(`Push to ${repository.full_name}/${branch} by ${pusher.name}`);
console.log(`Commits: ${commits.length}`);
commits.forEach(commit => {
console.log(`- ${commit.id.slice(0, 7)}: ${commit.message.split('\n')[0]}`);
});
// Your business logic:
// - Trigger deployment
// - Run tests
// - Send notifications
// - Update documentation
}
async function handlePullRequest(payload) {
const { action, pull_request, repository } = payload;
console.log(`PR #${pull_request.number} ${action} in ${repository.full_name}`);
console.log(`Title: ${pull_request.title}`);
console.log(`Author: ${pull_request.user.login}`);
switch (action) {
case 'opened':
// New PR opened
// - Run CI checks
// - Add labels
// - Assign reviewers
break;
case 'closed':
if (pull_request.merged) {
// PR was merged
// - Trigger deployment
// - Update changelog
}
break;
case 'synchronize':
// New commits pushed to PR
// - Re-run CI checks
break;
}
}
async function handleIssue(payload) {
const { action, issue, repository } = payload;
console.log(`Issue #${issue.number} ${action} in ${repository.full_name}`);
console.log(`Title: ${issue.title}`);
if (action === 'opened') {
// New issue created
// - Auto-label based on content
// - Notify team on Slack
// - Create task in project management tool
}
}
async function handleRelease(payload) {
const { action, release, repository } = payload;
if (action === 'published') {
console.log(`New release ${release.tag_name} in ${repository.full_name}`);
// Your business logic:
// - Trigger production deployment
// - Update documentation
// - Send announcement
// - Update package registries
}
}
async function handleWorkflowRun(payload) {
const { action, workflow_run, repository } = payload;
if (action === 'completed') {
const status = workflow_run.conclusion;
console.log(`Workflow "${workflow_run.name}" ${status}`);
if (status === 'failure') {
// Notify team of failed workflow
}
}
}
export default app.init();
```
Deploy:
```bash
coho deploy
```
```
Project: mygithubhandler-a1b2 Space: dev
Deployed Codehook successfully! 🙌
```
:::tip Finding your API endpoint
Run `coho info` to see your API endpoints and tokens. Your webhook URL will be:
`https://mygithubhandler-a1b2.api.codehooks.io/dev/github/webhooks`
:::
## Configure GitHub Webhooks
### Via GitHub Repository Settings
1. Go to your repository → **Settings** → **Webhooks**
2. Click **Add webhook**
3. Enter:
- **Payload URL:** `https://your-app.api.codehooks.io/dev/github/webhooks`
- **Content type:** `application/json`
- **Secret:** Your webhook secret (save this!)
4. Select events:
- Choose **Let me select individual events**
- Select: Push, Pull requests, Issues, etc.
5. Click **Add webhook**
### Via GitHub API
```javascript
// Create a webhook programmatically
const response = await fetch(
`https://api.github.com/repos/${owner}/${repo}/hooks`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${githubToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'web',
active: true,
events: ['push', 'pull_request', 'issues'],
config: {
url: 'https://your-app.api.codehooks.io/dev/github/webhooks',
content_type: 'json',
secret: process.env.GITHUB_WEBHOOK_SECRET
}
})
}
);
```
### Set Environment Variables
```bash
coho set-env GITHUB_WEBHOOK_SECRET your-secret-here --encrypted
```
## Common GitHub Webhook Events
| Event | Trigger | Common Use Case |
|-------|---------|-----------------|
| `push` | Code pushed to branch | CI/CD, deployments |
| `pull_request` | PR opened/closed/merged | Code review automation |
| `pull_request_review` | Review submitted | Merge automation |
| `issues` | Issue created/updated | Ticket management |
| `issue_comment` | Comment on issue/PR | Bot responses |
| `release` | Release published | Production deployment |
| `workflow_run` | GitHub Action completed | Status notifications |
| `create` | Branch/tag created | Environment provisioning |
| `delete` | Branch/tag deleted | Cleanup resources |
| `deployment_status` | Deployment status change | Monitoring |
| `star` | Repository starred | Analytics |
| `fork` | Repository forked | Tracking |
## GitHub Signature Verification
GitHub signs all webhook payloads with HMAC-SHA256. Always verify signatures:
```javascript
import crypto from 'crypto';
function verifyGitHubWebhook(req) {
const signature = req.headers['x-hub-signature-256'];
const secret = process.env.GITHUB_WEBHOOK_SECRET;
if (!signature) {
return false;
}
// Must use raw body, not parsed JSON
const body = req.rawBody;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (e) {
return false;
}
}
```
**Important:**
- Use `req.rawBody` (the raw string), not `req.body` (parsed JSON)
- Use `x-hub-signature-256` (SHA-256), not the deprecated `x-hub-signature` (SHA-1)
- Use `crypto.timingSafeEqual()` to prevent timing attacks
## CI/CD Integration Example
Trigger deployments on push to main:
```javascript
import { app } from 'codehooks-js';
app.post('/github/webhooks', async (req, res) => {
if (!verifyGitHubWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.headers['x-github-event'];
const payload = req.body;
if (event === 'push') {
const branch = payload.ref.replace('refs/heads/', '');
if (branch === 'main') {
// Trigger production deployment
await triggerDeployment({
environment: 'production',
commit: payload.after,
repository: payload.repository.full_name
});
// Notify team
await sendSlackNotification(
`🚀 Deploying ${payload.repository.name} to production\n` +
`Commit: ${payload.after.slice(0, 7)}`
);
} else if (branch === 'develop') {
// Trigger staging deployment
await triggerDeployment({
environment: 'staging',
commit: payload.after,
repository: payload.repository.full_name
});
}
}
res.json({ received: true });
});
async function triggerDeployment({ environment, commit, repository }) {
// Call your deployment API
await fetch(process.env.DEPLOY_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ environment, commit, repository })
});
}
export default app.init();
```
## Slack Notifications for GitHub Events
```javascript
async function sendGitHubNotificationToSlack(event, payload) {
let message;
switch (event) {
case 'push':
const branch = payload.ref.replace('refs/heads/', '');
const commits = payload.commits.length;
message = `📦 *${payload.pusher.name}* pushed ${commits} commit(s) to \`${branch}\`\n` +
`Repository: ${payload.repository.full_name}`;
break;
case 'pull_request':
const pr = payload.pull_request;
const action = payload.action;
const emoji = action === 'opened' ? '🆕' : action === 'closed' ? '✅' : '📝';
message = `${emoji} PR #${pr.number} ${action}: *${pr.title}*\n` +
`Author: ${pr.user.login}\n` +
`<${pr.html_url}|View PR>`;
break;
case 'issues':
const issue = payload.issue;
message = `🎫 Issue #${issue.number} ${payload.action}: *${issue.title}*\n` +
`<${issue.html_url}|View Issue>`;
break;
}
if (message) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: message })
});
}
}
```
## Best Practices for GitHub Webhooks
### 1. Handle the Ping Event
GitHub sends a `ping` event when you create a webhook. Handle it to confirm setup:
```javascript
if (event === 'ping') {
console.log('Webhook configured! Zen:', payload.zen);
return res.json({ message: 'pong' });
}
```
### 2. Use Delivery ID for Idempotency
```javascript
const deliveryId = req.headers['x-github-delivery'];
// Check if already processed
const conn = await Datastore.open();
try {
await conn.getOne('github_deliveries', deliveryId);
return res.json({ received: true, duplicate: true });
} catch (e) {
// Not found, continue to process
}
// Process and store
await processWebhook(payload);
await conn.insertOne('github_deliveries', {
_id: deliveryId,
event,
processedAt: new Date().toISOString()
});
```
### 3. Respond Quickly
GitHub expects a response within 10 seconds. Store the event first, then return immediately:
```javascript
app.post('/github/webhooks', async (req, res) => {
// Verify signature first
if (!verifyGitHubWebhook(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const conn = await Datastore.open();
const deliveryId = req.headers['x-github-delivery'];
// Check if we already have this event (idempotency)
try {
await conn.getOne('github_events', deliveryId);
return res.json({ received: true }); // Already exists
} catch (e) {
// Not found, continue to store
}
// Store new event
await conn.insertOne('github_events', {
_id: deliveryId,
event: req.headers['x-github-event'],
rawBody: req.rawBody, // Store raw body for replay capability
processedAt: null
});
res.json({ received: true });
});
```
## Why Use Codehooks.io for GitHub Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours/Days | 5 minutes |
| Signature verification | Manual implementation | Built-in support |
| Scaling | Configure infrastructure | Automatic |
| Multiple repos | Complex routing | Simple |
| Templates | Build from scratch | Ready-to-use |
| Cost | Server costs | Free tier available |
## Related Resources
- [GitHub Webhook Template](https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-github-minimal) - Ready-to-deploy template
- [All Codehooks Templates](https://github.com/RestDB/codehooks-io-templates) - Browse all webhook templates
- [GitHub Webhooks Documentation](https://docs.github.com/en/webhooks)
- [GitHub Webhook Events Reference](https://docs.github.com/en/webhooks/webhook-events-and-payloads)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Secure Zapier/Make/n8n/IFTTT Webhooks](/blog/secure-zapier-make-n8n-webhooks-signature-verification) - Forward verified GitHub events to automation platforms
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [Jira Webhooks Example](/docs/examples/webhooks/jira) - Another developer tools integration
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Payment webhook handler
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## Webhook Integration Examples
*(Note: This content contains MDX/JSX code)*
import Link from '@docusaurus/Link';
export const stripeLogo = '/img/webhooks/stripe-logo.svg';
export const githubLogo = '/img/webhooks/github-logo.svg';
export const discordLogo = '/img/webhooks/discord-logo.svg';
export const shopifyLogo = '/img/webhooks/shopify-logo.svg';
export const paypalLogo = '/img/webhooks/paypal-logo.svg';
export const jiraLogo = '/img/webhooks/jira-logo.svg';
export const slackLogo = '/img/webhooks/slack-logo.svg';
export const twilioLogo = '/img/webhooks/twilio-logo.svg';
export const sendgridLogo = '/img/webhooks/sendgrid-logo.svg';
export const mailgunLogo = '/img/webhooks/mailgun-logo.png';
export const openaiLogo = '/img/webhooks/openai-logo.svg';
export const WebhookCard = ({
image,
title,
description,
link,
}) => {
return (
{title}
{description}
Read Tutorial →
);
};
Production-ready webhook handlers with signature verification. Each example includes complete code, security best practices, and deployment instructions.
:::tip Building for non-developers?
[**Snill.ai**](https://snill.ai/#waitlist) lets business users build data apps from a plain-language description. Every Snill app emits HMAC-signed outbound webhooks and exposes a REST API — so you can wire Codehooks handlers to Snill events or push processed data back into Snill collections. [Join the waitlist →](https://snill.ai/#waitlist)
:::
:::tip New to webhooks?
Start with our guide on [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) to understand the fundamentals before diving into specific integrations.
:::
## Payments & E-commerce
Handle payment events, order notifications, and e-commerce integrations securely.
## Developer Tools
Automate workflows based on repository events, issue tracking, and CI/CD pipelines.
## Communication
Build bots, handle messages, and integrate with messaging platforms.
## Email Services
Track email delivery, opens, clicks, and bounces in real-time.
## AI & Automation
Receive notifications from AI platforms when async tasks complete.
---
## Jira Webhooks Integration Example: Automate Issues & Sprint Events
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# Jira Webhooks Integration Example: Automate Issues & Sprint Events
Jira webhooks notify your application when events occur in your Jira projects — issues are created, updated, transitioned, sprints start, and more. They're essential for automating workflows, syncing with external tools, and building custom integrations.
**The problem?** Jira webhook payloads are complex, events fire frequently, and you need proper filtering to avoid overwhelming your systems.
**The solution?** Deploy a production-ready Jira webhook handler with Codehooks.io in under 5 minutes.
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
## Quick Start
Create a Jira webhook handler:
```bash
coho create myjirahandler
```
You'll see output like:
```
Creating project: myjirahandler
Project created successfully!
Your API endpoints:
https://myjirahandler-a1b2.api.codehooks.io/dev
```
Then set up the project:
```bash
cd myjirahandler
npm install
```
Add your Jira webhook handler code to `index.js`:
```javascript
import { app } from 'codehooks-js';
import { Datastore } from 'codehooks-js';
// Jira webhook endpoint
app.post('/jira/webhooks', async (req, res) => {
const { webhookEvent, issue, user, changelog, sprint } = req.body;
console.log(`Received Jira event: ${webhookEvent}`);
// Handle different Jira events
switch (webhookEvent) {
case 'jira:issue_created':
await handleIssueCreated(issue, user);
break;
case 'jira:issue_updated':
await handleIssueUpdated(issue, user, changelog);
break;
case 'jira:issue_deleted':
await handleIssueDeleted(issue, user);
break;
case 'sprint_started':
await handleSprintStarted(sprint);
break;
case 'sprint_closed':
await handleSprintClosed(sprint);
break;
case 'comment_created':
await handleCommentCreated(req.body);
break;
default:
console.log(`Unhandled event: ${webhookEvent}`);
}
res.json({ received: true });
});
async function handleIssueCreated(issue, user) {
console.log(`Issue created: ${issue.key} - ${issue.fields.summary}`);
console.log(`Created by: ${user.displayName}`);
console.log(`Project: ${issue.fields.project.name}`);
console.log(`Type: ${issue.fields.issuetype.name}`);
console.log(`Priority: ${issue.fields.priority?.name}`);
// Your business logic:
// - Send Slack notification
// - Create task in external system
// - Update dashboard
// - Auto-assign based on rules
}
async function handleIssueUpdated(issue, user, changelog) {
console.log(`Issue updated: ${issue.key}`);
console.log(`Updated by: ${user.displayName}`);
// Check what changed
if (changelog?.items) {
for (const change of changelog.items) {
console.log(`Field: ${change.field}`);
console.log(`From: ${change.fromString} → To: ${change.toString}`);
// Handle specific field changes
if (change.field === 'status') {
await handleStatusChange(issue, change);
} else if (change.field === 'assignee') {
await handleAssigneeChange(issue, change);
} else if (change.field === 'priority') {
await handlePriorityChange(issue, change);
}
}
}
}
async function handleStatusChange(issue, change) {
const { fromString, toString } = change;
console.log(`Status changed: ${fromString} → ${toString}`);
// Status-specific actions
if (toString === 'Done') {
// Issue completed
// - Update time tracking
// - Send completion notification
// - Trigger deployment if it's a deploy ticket
} else if (toString === 'In Progress') {
// Work started
// - Start time tracking
// - Notify team
} else if (toString === 'In Review') {
// Ready for review
// - Notify reviewers
// - Create PR reminder
}
}
async function handleAssigneeChange(issue, change) {
console.log(`Assignee changed: ${change.fromString || 'Unassigned'} → ${change.toString || 'Unassigned'}`);
// Notify new assignee
}
async function handlePriorityChange(issue, change) {
if (change.toString === 'Highest' || change.toString === 'Blocker') {
console.log(`High priority issue: ${issue.key}`);
// Send urgent notification
}
}
async function handleIssueDeleted(issue, user) {
console.log(`Issue deleted: ${issue.key} by ${user.displayName}`);
// Cleanup external references
}
async function handleSprintStarted(sprint) {
console.log(`Sprint started: ${sprint.name}`);
console.log(`Goal: ${sprint.goal}`);
console.log(`Start: ${sprint.startDate}`);
console.log(`End: ${sprint.endDate}`);
// Your business logic:
// - Send sprint kickoff notification
// - Update dashboards
// - Create sprint report template
}
async function handleSprintClosed(sprint) {
console.log(`Sprint closed: ${sprint.name}`);
// Your business logic:
// - Generate sprint report
// - Calculate velocity
// - Archive sprint data
}
async function handleCommentCreated(payload) {
const { comment, issue } = payload;
console.log(`New comment on ${issue.key} by ${comment.author.displayName}`);
console.log(`Comment: ${comment.body}`);
// Check for mentions or keywords
if (comment.body.includes('@devops')) {
// Notify DevOps team
}
}
export default app.init();
```
Deploy:
```bash
coho deploy
```
```
Project: myjirahandler-a1b2 Space: dev
Deployed Codehook successfully! 🙌
```
:::tip Finding your API endpoint
Run `coho info` to see your API endpoints and tokens. Your webhook URL will be:
`https://myjirahandler-a1b2.api.codehooks.io/dev/jira/webhooks`
:::
## Configure Jira Webhooks
### Jira Cloud
1. Go to **Settings** → **System** → **WebHooks** (under Advanced)
2. Click **Create a WebHook**
3. Enter:
- **Name:** Your webhook name
- **URL:** `https://myjirahandler-a1b2.api.codehooks.io/dev/jira/webhooks`
- **Events:** Select events to subscribe to
- **JQL Filter (optional):** Filter which issues trigger webhooks
4. Click **Create**
### Jira Server/Data Center
1. Go to **Administration** → **System** → **WebHooks**
2. Click **Create a WebHook**
3. Configure URL and events
4. Optionally add a JQL filter
### Set Environment Variables
```bash
# If using Jira API for responses
coho set-env JIRA_API_TOKEN your-api-token --encrypted
coho set-env JIRA_EMAIL your-email@company.com
coho set-env JIRA_BASE_URL https://your-domain.atlassian.net
```
## Common Jira Webhook Events
| Event | Trigger | Common Use Case |
|-------|---------|-----------------|
| `jira:issue_created` | New issue created | Notifications, auto-assign |
| `jira:issue_updated` | Issue modified | Sync external systems |
| `jira:issue_deleted` | Issue removed | Cleanup references |
| `comment_created` | Comment added | Mention notifications |
| `comment_updated` | Comment edited | Audit trail |
| `sprint_started` | Sprint begins | Team notifications |
| `sprint_closed` | Sprint ends | Reports, velocity |
| `worklog_created` | Time logged | Time tracking sync |
| `issuelink_created` | Issues linked | Dependency tracking |
| `project_created` | New project | Setup automation |
| `user_created` | New user added | Onboarding workflows |
## Jira Webhook Signature Verification (HMAC)
Jira Cloud supports webhook signature verification using HMAC-SHA256. When you register a webhook with a secret, Jira signs the payload:
```javascript
import { app } from 'codehooks-js';
import crypto from 'crypto';
const JIRA_WEBHOOK_SECRET = process.env.JIRA_WEBHOOK_SECRET;
// Verify Jira webhook signature
function verifyJiraWebhook(req) {
const signature = req.headers['x-hub-signature'];
if (!signature || !JIRA_WEBHOOK_SECRET) {
return false; // No signature or no secret configured
}
const body = req.rawBody;
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', JIRA_WEBHOOK_SECRET)
.update(body)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
} catch (e) {
return false;
}
}
app.post('/jira/webhooks', async (req, res) => {
// Verify signature if secret is configured
if (JIRA_WEBHOOK_SECRET && !verifyJiraWebhook(req)) {
console.error('Invalid Jira webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
res.json({ received: true });
});
export default app.init();
```
Set your webhook secret:
```bash
coho set-env JIRA_WEBHOOK_SECRET your-secret-here --encrypted
```
:::note Jira Cloud vs Server
Signature verification is available in Jira Cloud. For Jira Server/Data Center, consider IP whitelisting or using Connect apps for authentication.
:::
## JQL Filtering
Use JQL (Jira Query Language) to filter which issues trigger your webhook:
```
# Only issues in PROJECT
project = PROJECT
# Only bugs
issuetype = Bug
# Only high priority issues
priority in (Highest, High)
# Combination
project = PROJECT AND issuetype = Bug AND priority = Highest
# Issues assigned to specific user
assignee = currentUser()
# Issues in specific sprint
sprint in openSprints()
# Recently updated
updated >= -1h
```
**Pro tip:** Use JQL filtering to reduce webhook traffic and focus on relevant events.
## Slack Integration for Jira Events
Send Jira updates to Slack:
```javascript
import { app } from 'codehooks-js';
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
app.post('/jira/webhooks', async (req, res) => {
const { webhookEvent, issue, user, changelog } = req.body;
let slackMessage = null;
switch (webhookEvent) {
case 'jira:issue_created':
slackMessage = {
text: `🆕 New issue created`,
attachments: [{
color: getTypeColor(issue.fields.issuetype.name),
title: `${issue.key}: ${issue.fields.summary}`,
title_link: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
fields: [
{ title: 'Type', value: issue.fields.issuetype.name, short: true },
{ title: 'Priority', value: issue.fields.priority?.name || 'None', short: true },
{ title: 'Reporter', value: user.displayName, short: true },
{ title: 'Project', value: issue.fields.project.name, short: true }
]
}]
};
break;
case 'jira:issue_updated':
const statusChange = changelog?.items?.find(i => i.field === 'status');
if (statusChange) {
slackMessage = {
text: `📋 Issue status changed`,
attachments: [{
color: getStatusColor(statusChange.toString),
title: `${issue.key}: ${issue.fields.summary}`,
title_link: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
fields: [
{ title: 'Status', value: `${statusChange.fromString} → ${statusChange.toString}`, short: true },
{ title: 'Updated by', value: user.displayName, short: true }
]
}]
};
}
break;
case 'comment_created':
const { comment } = req.body;
slackMessage = {
text: `💬 New comment on ${issue.key}`,
attachments: [{
color: '#7289da',
author_name: comment.author.displayName,
title: issue.fields.summary,
title_link: `${process.env.JIRA_BASE_URL}/browse/${issue.key}`,
text: comment.body.slice(0, 200) + (comment.body.length > 200 ? '...' : '')
}]
};
break;
}
if (slackMessage) {
await fetch(SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(slackMessage)
});
}
res.json({ received: true });
});
function getTypeColor(type) {
const colors = {
'Bug': '#ff0000',
'Story': '#00ff00',
'Task': '#0099ff',
'Epic': '#6554c0',
'Sub-task': '#4fade6'
};
return colors[type] || '#7289da';
}
function getStatusColor(status) {
const colors = {
'Done': '#00ff00',
'In Progress': '#0099ff',
'To Do': '#cccccc',
'In Review': '#ffff00',
'Blocked': '#ff0000'
};
return colors[status] || '#7289da';
}
export default app.init();
```
## Auto-Assignment Based on Rules
Automatically assign issues based on criteria:
```javascript
import { app } from 'codehooks-js';
const JIRA_BASE_URL = process.env.JIRA_BASE_URL;
const JIRA_EMAIL = process.env.JIRA_EMAIL;
const JIRA_API_TOKEN = process.env.JIRA_API_TOKEN;
app.post('/jira/webhooks', async (req, res) => {
const { webhookEvent, issue } = req.body;
if (webhookEvent === 'jira:issue_created') {
// Auto-assign based on component or label
const assignee = determineAssignee(issue);
if (assignee && !issue.fields.assignee) {
await assignIssue(issue.key, assignee);
}
}
res.json({ received: true });
});
function determineAssignee(issue) {
const components = issue.fields.components?.map(c => c.name) || [];
const labels = issue.fields.labels || [];
// Assignment rules
const rules = {
'backend': 'backend-team-lead',
'frontend': 'frontend-team-lead',
'database': 'dba-team',
'security': 'security-team',
'urgent': 'on-call-engineer'
};
// Check components
for (const component of components) {
if (rules[component.toLowerCase()]) {
return rules[component.toLowerCase()];
}
}
// Check labels
for (const label of labels) {
if (rules[label.toLowerCase()]) {
return rules[label.toLowerCase()];
}
}
return null;
}
async function assignIssue(issueKey, assigneeId) {
const auth = Buffer.from(`${JIRA_EMAIL}:${JIRA_API_TOKEN}`).toString('base64');
await fetch(`${JIRA_BASE_URL}/rest/api/3/issue/${issueKey}/assignee`, {
method: 'PUT',
headers: {
'Authorization': `Basic ${auth}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
accountId: assigneeId
})
});
}
export default app.init();
```
## Handling High Volume Webhooks
For busy Jira instances, process webhooks asynchronously:
```javascript
import { app, Datastore } from 'codehooks-js';
// Receive webhook and queue for processing
app.post('/jira/webhooks', async (req, res) => {
const conn = await Datastore.open();
// Store the webhook payload
const webhookId = `jira-${Date.now()}-${Math.random().toString(36).slice(2)}`;
await conn.insertOne('jira_webhooks', {
_id: webhookId,
payload: req.body,
receivedAt: new Date().toISOString(),
processed: false
});
// Queue for processing
await conn.enqueue('process-jira-webhook', { webhookId });
// Return immediately
res.json({ received: true, queued: webhookId });
});
// Process webhooks from queue
app.worker('process-jira-webhook', async (req, res) => {
const { webhookId } = req.body.payload;
const conn = await Datastore.open();
let webhook;
try {
webhook = await conn.getOne('jira_webhooks', webhookId);
} catch (e) {
// Not found
res.end();
return;
}
// Process the webhook
await processJiraEvent(webhook.payload);
// Mark as processed
await conn.updateOne('jira_webhooks', webhookId, {
$set: {
processed: true,
processedAt: new Date().toISOString()
}
});
res.end();
});
async function processJiraEvent(payload) {
// Your processing logic here
}
export default app.init();
```
## Best Practices
### 1. Use JQL Filters
Reduce webhook volume by filtering to only relevant issues:
```
project = MYPROJECT AND issuetype in (Bug, Story)
```
### 2. Handle Retries
Jira may retry failed webhooks. Use idempotency:
```javascript
const eventKey = `${req.body.timestamp}-${req.body.issue?.key}`;
// Check if already processed before handling
```
### 3. Respond Quickly
Return 2xx within a few seconds. Queue heavy processing for async handling.
### 4. Log Everything
Jira webhook debugging can be tricky. Log payloads during development.
## Related Resources
- [Jira Webhooks Documentation](https://developer.atlassian.com/cloud/jira/platform/webhooks/)
- [Jira REST API Reference](https://developer.atlassian.com/cloud/jira/platform/rest/v3/)
- [JQL Syntax Reference](https://support.atlassian.com/jira-software-cloud/docs/use-advanced-search-with-jira-query-language-jql/)
- [All Codehooks Templates](https://github.com/RestDB/codehooks-io-templates) - Browse all templates
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [GitHub Webhooks Example](/docs/examples/webhooks/github) - Another developer tools integration
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Payment webhook handler
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## Mailgun Webhooks Integration Example: Track Email Delivery & Engagement
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# Mailgun Webhooks Integration Example: Track Email Delivery & Engagement
Mailgun webhooks notify your application when email events occur — messages are delivered, opened, clicked, bounced, or marked as spam. Instead of polling the Events API, webhooks push real-time event data to your endpoint.
**The problem?** Setting up Mailgun webhooks requires HMAC-SHA256 signature verification, handling different content types, and processing various event formats.
**The solution?** Deploy a production-ready Mailgun webhook handler with Codehooks.io in under 5 minutes.
:::info New to webhooks?
If you're new to webhooks, check out our guide on [What are webhooks?](/docs/examples/webhooks/what-are-webhooks) to understand the fundamentals before diving into this Mailgun-specific implementation.
:::
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
You'll also need:
- A [Mailgun account](https://signup.mailgun.com/) with a verified domain
- Your Webhook Signing Key from [Mailgun Dashboard → Settings → Webhooks](https://app.mailgun.com/settings/webhooks)
## Quick Start: Email Event Handler
Create a new Codehooks project:
```bash
coho create my-mailgun-handler
cd my-mailgun-handler
```
Replace `index.js` with:
```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
// Bypass JWT auth for Mailgun webhook endpoint
app.auth('/mailgun/*', (req, res, next) => {
next();
});
// Verify Mailgun webhook signature
function verifyMailgunSignature(signingKey, timestamp, token, signature) {
const encodedToken = crypto
.createHmac('sha256', signingKey)
.update(timestamp.concat(token))
.digest('hex');
return encodedToken === signature;
}
// Handle Mailgun Event Webhook
app.post('/mailgun/events', async (req, res) => {
const signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
// Extract signature data from request
const { timestamp, token, signature } = req.body.signature || req.body;
// Verify the webhook is from Mailgun
if (signingKey && timestamp && token && signature) {
const isValid = verifyMailgunSignature(signingKey, timestamp, token, signature);
if (!isValid) {
console.error('Invalid Mailgun signature');
return res.status(403).json({ error: 'Invalid signature' });
}
}
// Extract event data
const eventData = req.body['event-data'] || req.body;
const event = eventData.event;
const recipient = eventData.recipient;
const messageId = eventData.message?.headers?.['message-id'];
console.log(`Mailgun event: ${event} for ${recipient}`);
// Store the event
const conn = await Datastore.open();
await conn.insertOne('email_events', {
event,
recipient,
messageId,
timestamp: eventData.timestamp,
deliveryStatus: eventData['delivery-status'],
tags: eventData.tags || [],
receivedAt: new Date().toISOString(),
});
// Handle specific event types
switch (event) {
case 'delivered':
console.log(`Email delivered to ${recipient}`);
break;
case 'opened':
console.log(`Email opened by ${recipient}`);
break;
case 'clicked':
console.log(`Link clicked by ${recipient}: ${eventData.url}`);
break;
case 'failed':
console.log(`Email failed for ${recipient}: ${eventData.reason}`);
break;
case 'complained':
console.log(`Spam complaint from ${recipient}`);
break;
case 'unsubscribed':
console.log(`Unsubscribe from ${recipient}`);
break;
}
res.status(200).json({ received: true });
});
export default app.init();
```
Deploy and configure:
```bash
coho deploy
coho set-env MAILGUN_WEBHOOK_SIGNING_KEY "your-signing-key" --encrypted
```
Your webhook URL will be: `https://my-mailgun-handler-xxxx.api.codehooks.io/dev/mailgun/events`
## Configure Mailgun Dashboard
1. Go to [Mailgun Dashboard → Sending → Webhooks](https://app.mailgun.com/sending/webhooks)
2. Select your domain
3. Click **Add webhook** for each event type you want to track
4. Enter your Codehooks URL: `https://your-app.api.codehooks.io/dev/mailgun/events`
5. Select the event type (delivered, opened, clicked, etc.)
6. Click **Create webhook**
**To get your Webhook Signing Key:**
1. Go to [Settings → Webhooks](https://app.mailgun.com/settings/webhooks)
2. Click **Show** next to "HTTP webhook signing key"
3. Copy the key
```bash
coho set-env MAILGUN_WEBHOOK_SIGNING_KEY "key-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" --encrypted
```
## Mailgun Event Types
### Delivery Events
| Event | Description | Key Fields |
|-------|-------------|------------|
| `accepted` | Mailgun accepted the message for delivery | `recipient`, `message-id` |
| `delivered` | Message delivered to recipient's server | `recipient`, `delivery-status` |
| `failed` | Permanent delivery failure | `recipient`, `reason`, `severity` |
| `rejected` | Message rejected by Mailgun | `recipient`, `reason` |
### Engagement Events
| Event | Description | Key Fields |
|-------|-------------|------------|
| `opened` | Recipient opened the email | `recipient`, `ip`, `user-agent` |
| `clicked` | Recipient clicked a link | `recipient`, `url`, `ip` |
| `unsubscribed` | Recipient unsubscribed | `recipient` |
| `complained` | Marked as spam | `recipient` |
### Storage Events
| Event | Description | Key Fields |
|-------|-------------|------------|
| `stored` | Message stored for retrieval | `storage.url` |
## Event Payload Examples
### Delivered Event
```json
{
"signature": {
"timestamp": "1701388800",
"token": "abc123...",
"signature": "def456..."
},
"event-data": {
"event": "delivered",
"timestamp": 1701388800.123,
"recipient": "user@example.com",
"message": {
"headers": {
"message-id": "abc123@mailgun.org"
}
},
"delivery-status": {
"code": 250,
"message": "OK",
"description": "Message accepted"
},
"tags": ["newsletter", "december"]
}
}
```
### Clicked Event
```json
{
"signature": {
"timestamp": "1701388900",
"token": "xyz789...",
"signature": "ghi012..."
},
"event-data": {
"event": "clicked",
"timestamp": 1701388900.456,
"recipient": "user@example.com",
"url": "https://example.com/promo",
"ip": "192.168.1.1",
"client-info": {
"user-agent": "Mozilla/5.0...",
"device-type": "desktop"
}
}
}
```
### Failed Event
```json
{
"signature": {
"timestamp": "1701388800",
"token": "fail123...",
"signature": "sig456..."
},
"event-data": {
"event": "failed",
"timestamp": 1701388800.789,
"recipient": "invalid@example.com",
"reason": "bounce",
"severity": "permanent",
"delivery-status": {
"code": 550,
"message": "User not found"
}
}
}
```
## Advanced: Track Campaign Analytics
Store detailed analytics for email campaigns:
```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
app.auth('/mailgun/*', (req, res, next) => {
next();
});
function verifyMailgunSignature(signingKey, timestamp, token, signature) {
const encodedToken = crypto
.createHmac('sha256', signingKey)
.update(timestamp.concat(token))
.digest('hex');
return encodedToken === signature;
}
app.post('/mailgun/events', async (req, res) => {
const signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
const { timestamp, token, signature } = req.body.signature || {};
if (signingKey && timestamp && token && signature) {
if (!verifyMailgunSignature(signingKey, timestamp, token, signature)) {
return res.status(403).json({ error: 'Invalid signature' });
}
}
const eventData = req.body['event-data'] || req.body;
const conn = await Datastore.open();
// Store raw event
await conn.insertOne('email_events', {
...eventData,
receivedAt: new Date().toISOString(),
});
// Update campaign stats if tags are present
const tags = eventData.tags || [];
for (const tag of tags) {
const updateField = getUpdateField(eventData.event);
if (updateField) {
await conn.updateOne(
'campaign_stats',
{ tag },
{
$inc: { [updateField]: 1 },
$set: { lastEventAt: new Date().toISOString() }
},
{ upsert: true }
);
}
}
// Handle bounces and complaints for list hygiene
if (eventData.event === 'failed' && eventData.severity === 'permanent') {
await conn.updateOne(
'subscribers',
{ email: eventData.recipient },
{
$set: {
status: 'bounced',
bounceReason: eventData.reason,
bounceDate: new Date().toISOString(),
}
}
);
}
if (eventData.event === 'complained') {
await conn.updateOne(
'subscribers',
{ email: eventData.recipient },
{
$set: {
status: 'complained',
doNotEmail: true,
complaintDate: new Date().toISOString(),
}
}
);
}
res.status(200).json({ received: true });
});
function getUpdateField(eventType) {
const mapping = {
'accepted': 'accepted',
'delivered': 'delivered',
'opened': 'opens',
'clicked': 'clicks',
'failed': 'failures',
'complained': 'complaints',
'unsubscribed': 'unsubscribes',
};
return mapping[eventType];
}
export default app.init();
```
## Async Processing for High Volume
For high-volume email sending, process events asynchronously:
```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
// Worker for async event processing
app.worker('processMailgunEvent', async (req, res) => {
const eventData = req.body.payload;
const conn = await Datastore.open();
// Store event
await conn.insertOne('email_events', {
...eventData,
processedAt: new Date().toISOString(),
});
// Handle failures - update subscriber status
if (eventData.event === 'failed' && eventData.severity === 'permanent') {
await conn.updateOne(
'subscribers',
{ email: eventData.recipient },
{ $set: { status: 'bounced', bounceReason: eventData.reason } }
);
}
// Handle spam complaints
if (eventData.event === 'complained') {
await conn.updateOne(
'subscribers',
{ email: eventData.recipient },
{ $set: { status: 'complained', doNotEmail: true } }
);
}
res.end();
});
app.auth('/mailgun/*', (req, res, next) => {
next();
});
function verifyMailgunSignature(signingKey, timestamp, token, signature) {
const encodedToken = crypto
.createHmac('sha256', signingKey)
.update(timestamp.concat(token))
.digest('hex');
return encodedToken === signature;
}
app.post('/mailgun/events', async (req, res) => {
const signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
const { timestamp, token, signature } = req.body.signature || {};
if (signingKey && timestamp && token && signature) {
if (!verifyMailgunSignature(signingKey, timestamp, token, signature)) {
return res.status(403).json({ error: 'Invalid signature' });
}
}
const eventData = req.body['event-data'] || req.body;
const conn = await Datastore.open();
// Queue for async processing
await conn.enqueue('processMailgunEvent', eventData);
// Respond immediately
res.status(200).json({ queued: true });
});
export default app.init();
```
## Mailgun Signature Verification
Mailgun signs all webhooks using HMAC-SHA256. The signature is verified by:
1. Concatenating the `timestamp` and `token` values
2. Computing HMAC-SHA256 using your Webhook Signing Key
3. Comparing the result to the `signature` value
```javascript
import crypto from 'crypto';
function verifyMailgunSignature(signingKey, timestamp, token, signature) {
const encodedToken = crypto
.createHmac('sha256', signingKey)
.update(timestamp.concat(token))
.digest('hex');
return encodedToken === signature;
}
// Usage in webhook handler
const { timestamp, token, signature } = req.body.signature;
const isValid = verifyMailgunSignature(signingKey, timestamp, token, signature);
```
**Security tips:**
- Cache processed `token` values to prevent replay attacks
- Check that `timestamp` is recent (within 5-10 minutes) to reject old requests
## Preventing Replay Attacks
For additional security, cache tokens and reject duplicates:
```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
app.auth('/mailgun/*', (req, res, next) => {
next();
});
async function verifyAndCheckReplay(signingKey, timestamp, token, signature) {
// Verify signature
const encodedToken = crypto
.createHmac('sha256', signingKey)
.update(timestamp.concat(token))
.digest('hex');
if (encodedToken !== signature) {
return { valid: false, reason: 'invalid_signature' };
}
// Check timestamp is recent (within 5 minutes)
const timestampAge = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (timestampAge > 300) {
return { valid: false, reason: 'timestamp_expired' };
}
// Check for replay attack
const conn = await Datastore.open();
const existing = await conn.get(`mailgun-token-${token}`, { keyspace: 'webhooks' });
if (existing) {
return { valid: false, reason: 'replay_attack' };
}
// Store token with 10-minute TTL
await conn.set(`mailgun-token-${token}`, '1', {
keyspace: 'webhooks',
ttl: 600000 // 10 minutes
});
return { valid: true };
}
app.post('/mailgun/events', async (req, res) => {
const signingKey = process.env.MAILGUN_WEBHOOK_SIGNING_KEY;
const { timestamp, token, signature } = req.body.signature || {};
if (signingKey && timestamp && token && signature) {
const result = await verifyAndCheckReplay(signingKey, timestamp, token, signature);
if (!result.valid) {
console.error('Webhook verification failed:', result.reason);
return res.status(403).json({ error: result.reason });
}
}
// Process event...
const eventData = req.body['event-data'] || req.body;
console.log(`Processing ${eventData.event} for ${eventData.recipient}`);
res.status(200).json({ received: true });
});
export default app.init();
```
## Best Practices
### 1. Always Verify Signatures in Production
```javascript
if (!signingKey) {
console.warn('MAILGUN_WEBHOOK_SIGNING_KEY not set - skipping verification');
} else {
const isValid = verifyMailgunSignature(signingKey, timestamp, token, signature);
if (!isValid) {
return res.status(403).json({ error: 'Invalid signature' });
}
}
```
### 2. Handle Both Payload Formats
Mailgun can send webhooks in different formats. Handle both:
```javascript
// New format
const eventData = req.body['event-data'];
const signatureData = req.body.signature;
// Legacy format (fallback)
if (!eventData) {
const { event, recipient, timestamp, token, signature } = req.body;
// Process legacy format...
}
```
### 3. Clean Bounce Lists
Automatically remove bounced addresses:
```javascript
if (eventData.event === 'failed' && eventData.severity === 'permanent') {
await conn.updateOne(
'subscribers',
{ email: eventData.recipient },
{ $set: { active: false, bounced: true } }
);
}
```
### 4. Respond Quickly
Return 200 immediately to prevent retries:
```javascript
// Queue for processing and respond immediately
await conn.enqueue('processEvent', eventData);
res.status(200).json({ queued: true });
```
## Why Use Codehooks.io for Mailgun Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours | 5 minutes |
| SSL/HTTPS | Configure certificates | Built-in |
| Signature verification | Manual crypto implementation | Built-in crypto module |
| Database for events | Set up separately | Built-in |
| Key-value store (replay prevention) | Set up Redis/similar | Built-in with TTL |
| Async processing | Configure queues | Built-in workers |
| Monitoring | Set up logging | Built-in logs |
| Cost | Server costs | Free tier available |
## Related Resources
- [Mailgun Webhooks Documentation](https://documentation.mailgun.com/docs/mailgun/user-manual/events/webhooks)
- [Mailgun Webhook Guide](https://www.mailgun.com/blog/product/a-guide-to-using-mailguns-webhooks/)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [SendGrid Webhooks Example](/docs/examples/webhooks/sendgrid) - Another email service integration
- [Twilio Webhooks Example](/docs/examples/webhooks/twilio) - SMS and voice webhooks
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Payment webhook handler
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## OpenAI Webhooks Integration Example: Handle Deep Research & Batch Jobs
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# OpenAI Webhooks Integration Example: Handle Deep Research & Batch Jobs
OpenAI webhooks notify your application when long-running AI tasks complete — Deep Research results, batch processing jobs, fine-tuning completions, and more. Instead of polling the OpenAI API, webhooks push real-time notifications when operations finish.
**The problem?** OpenAI's Deep Research and batch APIs can take minutes or hours. Polling wastes resources and introduces latency. You need instant notifications when tasks complete.
**The solution?** Deploy a production-ready OpenAI webhook handler with Codehooks.io in under 5 minutes.
:::info New to webhooks?
If you're new to webhooks, check out our guide on [What are webhooks?](/docs/examples/webhooks/what-are-webhooks) to understand the fundamentals before diving into this OpenAI-specific implementation.
:::
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
You'll also need:
- An [OpenAI Platform account](https://platform.openai.com/)
- A webhook signing secret from the [OpenAI Dashboard](https://platform.openai.com/settings/project/webhooks)
## Quick Start: OpenAI Webhook Handler
Create a new Codehooks project:
```bash
coho create my-openai-handler
cd my-openai-handler
npm install openai
```
Replace `index.js` with:
```javascript
import { app, Datastore } from 'codehooks-js';
import OpenAI from 'openai';
const openai = new OpenAI();
// Bypass JWT auth for OpenAI webhook endpoints
app.auth('/openai/*', (req, res, next) => {
next();
});
// Handle OpenAI webhook events
app.post('/openai/webhook', async (req, res) => {
const webhookSecret = process.env.OPENAI_WEBHOOK_SECRET;
try {
// Verify signature and parse the event
const event = openai.webhooks.unwrap(req.rawBody, req.headers, webhookSecret);
console.log(`Received OpenAI event: ${event.type}`);
// Store the event
const conn = await Datastore.open();
await conn.insertOne('openai_events', {
eventId: event.id,
type: event.type,
data: event.data,
receivedAt: new Date().toISOString(),
});
// Handle specific event types
switch (event.type) {
case 'response.completed':
await handleResponseCompleted(event.data);
break;
case 'batch.completed':
await handleBatchCompleted(event.data);
break;
case 'fine_tuning.job.succeeded':
await handleFineTuningSucceeded(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook verification failed:', err.message);
res.status(400).json({ error: 'Invalid signature' });
}
});
async function handleResponseCompleted(data) {
console.log('Deep Research completed:', data.id);
// Process the completed research results
}
async function handleBatchCompleted(data) {
console.log('Batch job completed:', data.id);
// Fetch and process batch results
}
async function handleFineTuningSucceeded(data) {
console.log('Fine-tuning succeeded:', data.id);
// Store the new model ID for future use
}
export default app.init();
```
Deploy and configure:
```bash
coho deploy
coho set-env OPENAI_WEBHOOK_SECRET whsec_xxxxx --encrypted
coho set-env OPENAI_API_KEY sk-xxxxx --encrypted
```
Your webhook URL will be: `https://my-openai-handler-xxxx.api.codehooks.io/dev/openai/webhook`
## Configure OpenAI Dashboard
1. Go to [OpenAI Dashboard → Webhooks](https://platform.openai.com/settings/project/webhooks)
2. Click **Create webhook**
3. Enter your webhook URL: `https://your-app.api.codehooks.io/dev/openai/webhook`
4. Select the events you want to receive
5. Copy the **signing secret** — you'll need it for verification
6. Click **Save**
:::warning Save Your Signing Secret
The signing secret is only shown once when you create the webhook. Store it securely in your environment variables.
:::
## OpenAI Webhook Event Types
### Background Response Events
For Deep Research and other long-running response operations:
| Event Type | Description |
|------------|-------------|
| `response.completed` | Background response finished successfully |
| `response.failed` | Background response encountered an error |
| `response.cancelled` | Response was cancelled |
| `response.incomplete` | Response hit limits or timed out |
### Batch Processing Events
For batch API operations:
| Event Type | Description |
|------------|-------------|
| `batch.completed` | All batch items processed successfully |
| `batch.failed` | Batch processing failed |
| `batch.expired` | Batch expired before completion |
| `batch.cancelled` | Batch was cancelled |
### Fine-Tuning Events
For model fine-tuning jobs:
| Event Type | Description |
|------------|-------------|
| `fine_tuning.job.succeeded` | Fine-tuning completed successfully |
| `fine_tuning.job.failed` | Fine-tuning encountered an error |
| `fine_tuning.job.cancelled` | Fine-tuning was cancelled |
### Eval Run Events
For evaluation runs:
| Event Type | Description |
|------------|-------------|
| `eval.run.succeeded` | Evaluation completed successfully |
| `eval.run.failed` | Evaluation encountered an error |
| `eval.run.canceled` | Evaluation was cancelled |
## Webhook Payload Format
OpenAI webhook payloads follow this structure:
```json
{
"id": "evt_1234567890abcdef",
"object": "event",
"type": "response.completed",
"created_at": 1735123456,
"data": {
"id": "resp_abc123",
"object": "response",
"status": "completed",
"output": [
{
"type": "message",
"message": {
"role": "assistant",
"content": "..."
}
}
],
"completed_at": 1735123456
},
"api_version": "2025-01-01"
}
```
### Webhook Headers
Each request includes these headers:
| Header | Description |
|--------|-------------|
| `webhook-id` | Unique identifier for this delivery (use for idempotency) |
| `webhook-timestamp` | Unix timestamp when the webhook was sent |
| `webhook-signature` | Signature for verification (format: `v1,base64_signature`) |
| `content-type` | Always `application/json` |
## Deep Research Webhook Handler
Handle completed Deep Research results:
```javascript
import { app, Datastore } from 'codehooks-js';
import OpenAI from 'openai';
const openai = new OpenAI();
app.auth('/openai/*', (req, res, next) => {
next();
});
app.post('/openai/webhook', async (req, res) => {
const webhookSecret = process.env.OPENAI_WEBHOOK_SECRET;
try {
const event = openai.webhooks.unwrap(req.rawBody, req.headers, webhookSecret);
if (event.type === 'response.completed') {
const response = event.data;
// Extract the research results
const output = response.output || [];
const messageContent = output
.filter(item => item.type === 'message')
.map(item => item.message?.content)
.join('\n');
// Store the completed research
const conn = await Datastore.open();
await conn.insertOne('research_results', {
responseId: response.id,
status: 'completed',
content: messageContent,
completedAt: new Date(response.completed_at * 1000).toISOString(),
receivedAt: new Date().toISOString(),
});
console.log('Deep Research completed:', response.id);
// Optionally notify your users
await conn.enqueue('notifyUser', {
type: 'research_complete',
responseId: response.id,
});
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).json({ error: 'Invalid signature' });
}
});
// Worker to notify users asynchronously
app.worker('notifyUser', async (req, res) => {
const { type, responseId } = req.body.payload;
// Send email, push notification, etc.
console.log(`Notifying user about ${type}: ${responseId}`);
res.end();
});
export default app.init();
```
## Batch Processing Webhook Handler
Handle batch job completion:
```javascript
import { app, Datastore } from 'codehooks-js';
import OpenAI from 'openai';
const openai = new OpenAI();
app.auth('/openai/*', (req, res, next) => {
next();
});
app.post('/openai/webhook', async (req, res) => {
const webhookSecret = process.env.OPENAI_WEBHOOK_SECRET;
try {
const event = openai.webhooks.unwrap(req.rawBody, req.headers, webhookSecret);
if (event.type === 'batch.completed') {
const batch = event.data;
console.log(`Batch ${batch.id} completed`);
// Fetch the batch results
const completedBatch = await openai.batches.retrieve(batch.id);
// Download and process the output file
if (completedBatch.output_file_id) {
const fileContent = await openai.files.content(completedBatch.output_file_id);
const results = await fileContent.text();
// Store batch results
const conn = await Datastore.open();
await conn.insertOne('batch_results', {
batchId: batch.id,
status: 'completed',
outputFileId: completedBatch.output_file_id,
results: results,
requestCounts: completedBatch.request_counts,
completedAt: new Date().toISOString(),
});
}
// Handle any errors
if (completedBatch.error_file_id) {
const errorContent = await openai.files.content(completedBatch.error_file_id);
console.error('Batch errors:', await errorContent.text());
}
}
if (event.type === 'batch.failed') {
const batch = event.data;
console.error(`Batch ${batch.id} failed`);
const conn = await Datastore.open();
await conn.insertOne('batch_results', {
batchId: batch.id,
status: 'failed',
failedAt: new Date().toISOString(),
});
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).json({ error: 'Invalid signature' });
}
});
export default app.init();
```
## Fine-Tuning Webhook Handler
Handle fine-tuning job completion:
```javascript
import { app, Datastore } from 'codehooks-js';
import OpenAI from 'openai';
const openai = new OpenAI();
app.auth('/openai/*', (req, res, next) => {
next();
});
app.post('/openai/webhook', async (req, res) => {
const webhookSecret = process.env.OPENAI_WEBHOOK_SECRET;
try {
const event = openai.webhooks.unwrap(req.rawBody, req.headers, webhookSecret);
if (event.type === 'fine_tuning.job.succeeded') {
const job = event.data;
console.log(`Fine-tuning job ${job.id} succeeded`);
console.log(`New model: ${job.fine_tuned_model}`);
// Store the fine-tuned model info
const conn = await Datastore.open();
await conn.insertOne('fine_tuned_models', {
jobId: job.id,
modelId: job.fine_tuned_model,
baseModel: job.model,
status: 'succeeded',
trainedTokens: job.trained_tokens,
createdAt: new Date().toISOString(),
});
// You can now use job.fine_tuned_model in your API calls
}
if (event.type === 'fine_tuning.job.failed') {
const job = event.data;
console.error(`Fine-tuning job ${job.id} failed:`, job.error);
const conn = await Datastore.open();
await conn.insertOne('fine_tuned_models', {
jobId: job.id,
status: 'failed',
error: job.error,
failedAt: new Date().toISOString(),
});
}
res.status(200).json({ received: true });
} catch (err) {
console.error('Webhook error:', err.message);
res.status(400).json({ error: 'Invalid signature' });
}
});
export default app.init();
```
## Complete Multi-Event Handler
Handle all OpenAI webhook types in one deployment:
```javascript
import { app, Datastore } from 'codehooks-js';
import OpenAI from 'openai';
const openai = new OpenAI();
// Bypass JWT for OpenAI webhook endpoint
app.auth('/openai/*', (req, res, next) => {
next();
});
// Main webhook handler
app.post('/openai/webhook', async (req, res) => {
const webhookSecret = process.env.OPENAI_WEBHOOK_SECRET;
let event;
try {
event = openai.webhooks.unwrap(req.rawBody, req.headers, webhookSecret);
} catch (err) {
console.error('Signature verification failed:', err.message);
return res.status(400).json({ error: 'Invalid signature' });
}
const conn = await Datastore.open();
const webhookId = req.headers['webhook-id'];
// Idempotency check
try {
await conn.getOne('openai_events', { webhookId });
console.log('Already processed:', webhookId);
return res.status(200).json({ received: true });
} catch (e) {
// Not found, continue to process
}
// Store the event
await conn.insertOne('openai_events', {
webhookId,
eventId: event.id,
type: event.type,
data: event.data,
receivedAt: new Date().toISOString(),
});
console.log(`Received event: ${event.type}`);
// Route to appropriate handler
switch (event.type) {
// Background Response events
case 'response.completed':
await handleResponseCompleted(conn, event.data);
break;
case 'response.failed':
await handleResponseFailed(conn, event.data);
break;
case 'response.cancelled':
case 'response.incomplete':
await handleResponseIncomplete(conn, event.data, event.type);
break;
// Batch events
case 'batch.completed':
await handleBatchCompleted(conn, event.data);
break;
case 'batch.failed':
case 'batch.expired':
case 'batch.cancelled':
await handleBatchFailed(conn, event.data, event.type);
break;
// Fine-tuning events
case 'fine_tuning.job.succeeded':
await handleFineTuningSucceeded(conn, event.data);
break;
case 'fine_tuning.job.failed':
case 'fine_tuning.job.cancelled':
await handleFineTuningFailed(conn, event.data, event.type);
break;
// Eval events
case 'eval.run.succeeded':
await handleEvalSucceeded(conn, event.data);
break;
case 'eval.run.failed':
case 'eval.run.canceled':
await handleEvalFailed(conn, event.data, event.type);
break;
default:
console.log('Unhandled event type:', event.type);
}
res.status(200).json({ received: true });
});
// Handler functions
async function handleResponseCompleted(conn, data) {
console.log('Response completed:', data.id);
await conn.updateOne('responses', { responseId: data.id }, {
$set: { status: 'completed', completedAt: new Date().toISOString() }
});
}
async function handleResponseFailed(conn, data) {
console.error('Response failed:', data.id, data.error);
await conn.updateOne('responses', { responseId: data.id }, {
$set: { status: 'failed', error: data.error, failedAt: new Date().toISOString() }
});
}
async function handleResponseIncomplete(conn, data, eventType) {
console.log(`Response ${eventType}:`, data.id);
await conn.updateOne('responses', { responseId: data.id }, {
$set: { status: eventType.split('.')[1], updatedAt: new Date().toISOString() }
});
}
async function handleBatchCompleted(conn, data) {
console.log('Batch completed:', data.id);
await conn.updateOne('batches', { batchId: data.id }, {
$set: { status: 'completed', completedAt: new Date().toISOString() }
});
}
async function handleBatchFailed(conn, data, eventType) {
const status = eventType.split('.')[1];
console.error(`Batch ${status}:`, data.id);
await conn.updateOne('batches', { batchId: data.id }, {
$set: { status, failedAt: new Date().toISOString() }
});
}
async function handleFineTuningSucceeded(conn, data) {
console.log('Fine-tuning succeeded:', data.id, data.fine_tuned_model);
await conn.updateOne('fine_tuning_jobs', { jobId: data.id }, {
$set: {
status: 'succeeded',
fineTunedModel: data.fine_tuned_model,
completedAt: new Date().toISOString()
}
});
}
async function handleFineTuningFailed(conn, data, eventType) {
const status = eventType.split('.')[2];
console.error(`Fine-tuning ${status}:`, data.id, data.error);
await conn.updateOne('fine_tuning_jobs', { jobId: data.id }, {
$set: { status, error: data.error, failedAt: new Date().toISOString() }
});
}
async function handleEvalSucceeded(conn, data) {
console.log('Eval run succeeded:', data.id);
await conn.insertOne('eval_results', {
evalId: data.id,
status: 'succeeded',
completedAt: new Date().toISOString(),
});
}
async function handleEvalFailed(conn, data, eventType) {
const status = eventType.split('.')[2];
console.error(`Eval ${status}:`, data.id);
await conn.insertOne('eval_results', {
evalId: data.id,
status,
failedAt: new Date().toISOString(),
});
}
export default app.init();
```
## Signature Verification
OpenAI uses the [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) specification for signature verification. The signature is computed using HMAC-SHA256.
### Using the OpenAI SDK (Recommended)
```javascript
import OpenAI from 'openai';
const openai = new OpenAI();
function verifyOpenAIWebhook(rawBody, headers, secret) {
try {
// unwrap() verifies the signature and parses the payload
const event = openai.webhooks.unwrap(rawBody, headers, secret);
return event;
} catch (err) {
console.error('Verification failed:', err.message);
throw err;
}
}
```
**Important:** Use `req.rawBody` (the raw JSON string) for verification, not `req.body` (the parsed object).
### Using Standard Webhooks Library
Alternatively, use the `standardwebhooks` package directly:
```javascript
import { Webhook } from 'standardwebhooks';
function verifyWithStandardWebhooks(rawBody, headers, secret) {
const wh = new Webhook(secret);
// Extract required headers
const webhookHeaders = {
'webhook-id': headers['webhook-id'],
'webhook-timestamp': headers['webhook-timestamp'],
'webhook-signature': headers['webhook-signature'],
};
try {
const payload = wh.verify(rawBody, webhookHeaders);
return JSON.parse(payload);
} catch (err) {
console.error('Verification failed:', err.message);
throw err;
}
}
```
### Verify Only (Without Parsing)
If you need to verify the signature separately:
```javascript
import OpenAI from 'openai';
const openai = new OpenAI();
// Just verify, don't parse
openai.webhooks.verifySignature(rawBody, headers, secret);
// Then parse separately
const event = JSON.parse(rawBody);
```
## Response Requirements
OpenAI webhooks require a quick response:
- **Respond with 2xx** within a few seconds to indicate success
- **Non-2xx responses** trigger retries with exponential backoff (up to 72 hours)
- **3xx redirects** are not followed and treated as failures
```javascript
app.post('/openai/webhook', async (req, res) => {
try {
const event = openai.webhooks.unwrap(req.rawBody, req.headers, secret);
// Quick acknowledge, process later
res.status(200).json({ received: true });
// Async processing after response
const conn = await Datastore.open();
await conn.enqueue('processEvent', { event });
} catch (err) {
res.status(400).json({ error: 'Invalid signature' });
}
});
```
## Best Practices for OpenAI Webhooks
### 1. Always Verify Signatures
Never skip signature verification in production:
```javascript
const event = openai.webhooks.unwrap(req.rawBody, req.headers, secret);
```
### 2. Use Environment Variables
```bash
coho set-env OPENAI_WEBHOOK_SECRET whsec_xxxxx --encrypted
coho set-env OPENAI_API_KEY sk-xxxxx --encrypted
```
### 3. Handle Idempotency
OpenAI typically doesn't send duplicates, but use `webhook-id` as an idempotency key:
```javascript
const webhookId = req.headers['webhook-id'];
try {
await conn.getOne('openai_events', { webhookId });
console.log('Already processed:', webhookId);
return res.status(200).json({ received: true });
} catch (e) {
// Not found, continue to process
}
```
### 4. Respond Quickly
Acknowledge within seconds, process asynchronously:
```javascript
// Store immediately
await conn.insertOne('events', { eventId: event.id, type: event.type });
// Queue for async processing
await conn.enqueue('processOpenAIEvent', { event });
// Respond immediately
res.status(200).json({ received: true });
```
### 5. Log Everything
```javascript
console.log('OpenAI webhook received:', {
eventId: event.id,
type: event.type,
timestamp: new Date().toISOString(),
});
```
Check logs with: `coho logs --follow`
## Why Use Codehooks.io for OpenAI Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours | 5 minutes |
| SSL/HTTPS | Configure certificates | Built-in |
| Signature validation | Manual implementation | Use OpenAI SDK |
| Database for results | Set up separately | Built-in |
| Async processing | Configure queues | Built-in workers |
| Monitoring | Set up logging | Built-in logs |
| Cost | Server costs | Free tier available |
## Related Resources
- [OpenAI Webhooks Documentation](https://platform.openai.com/docs/guides/webhooks)
- [OpenAI Batch API](https://platform.openai.com/docs/guides/batch)
- [Standard Webhooks Specification](https://github.com/standard-webhooks/standard-webhooks)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Building LLM Workflows with Codehooks.io](/blog/building-llm-workflows-javascript)
- [Secure Zapier/Make/n8n/IFTTT Webhooks](/blog/secure-zapier-make-n8n-webhooks-signature-verification) - Forward verified AI results to automation platforms
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Payment webhook handler with similar patterns
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## PayPal Webhooks Integration Example: Handle Payments, Refunds & Disputes
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# PayPal Webhooks Integration Example: Handle Payments, Refunds & Disputes
PayPal webhooks notify your application when payment events occur — orders are approved, payments captured, refunds processed, or disputes opened. Instead of polling the PayPal API, webhooks push real-time event data to your endpoint.
**The problem?** Setting up PayPal webhooks requires OAuth2 authentication, postback signature verification, handling exact payload formatting, and processing various event types.
**The solution?** Deploy a production-ready PayPal webhook handler with Codehooks.io in under 5 minutes.
:::info New to webhooks?
If you're new to webhooks, check out our guide on [What are webhooks?](/docs/examples/webhooks/what-are-webhooks) to understand the fundamentals before diving into this PayPal-specific implementation.
:::
## Quick Deploy with Template
The fastest way to get started is using our ready-made PayPal webhook template:
```bash
coho create mypaypal --template webhook-paypal-minimal
cd mypaypal
coho set-env PAYPAL_CLIENT_ID "your_client_id" --encrypted
coho set-env PAYPAL_CLIENT_SECRET "your_client_secret" --encrypted
coho set-env PAYPAL_WEBHOOK_ID "your_webhook_id" --encrypted
coho set-env PAYPAL_MODE "sandbox"
coho deploy
```
The template includes:
- **Token caching** — OAuth tokens cached for 8 hours to minimize API calls
- **Header validation** — All required PayPal headers validated upfront
- **Sandbox/Live switching** — Simple `PAYPAL_MODE` environment variable
- **Health check endpoint** — GET `/` returns configuration status
Your webhook URL will be: `https://mypaypal-xxxx.api.codehooks.io/dev/webhook`
Want more control? Continue reading for a step-by-step implementation guide.
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
You'll also need:
- A [PayPal Developer account](https://developer.paypal.com/)
- A PayPal app with REST API credentials (Client ID and Secret)
- Your Webhook ID from the [PayPal Developer Dashboard](https://developer.paypal.com/dashboard/applications/)
## Quick Start: Payment Capture Handler
Create a new Codehooks project:
```bash
coho create my-paypal-handler
cd my-paypal-handler
```
Replace `index.js` with:
```javascript
import { app, Datastore } from 'codehooks-js';
// Bypass JWT auth for PayPal webhook endpoint
app.auth('/paypal/*', (req, res, next) => {
next();
});
// Get PayPal OAuth access token
async function getPayPalAccessToken() {
const clientId = process.env.PAYPAL_CLIENT_ID;
const clientSecret = process.env.PAYPAL_CLIENT_SECRET;
const baseUrl = process.env.PAYPAL_API_URL || 'https://api-m.sandbox.paypal.com';
const response = await fetch(`${baseUrl}/v1/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
},
body: 'grant_type=client_credentials',
});
const data = await response.json();
return data.access_token;
}
// Verify PayPal webhook signature using postback verification
async function verifyPayPalWebhook(req) {
const webhookId = process.env.PAYPAL_WEBHOOK_ID;
const baseUrl = process.env.PAYPAL_API_URL || 'https://api-m.sandbox.paypal.com';
const accessToken = await getPayPalAccessToken();
// Parse the raw body to ensure exact payload preservation
const webhookEvent = JSON.parse(req.rawBody);
const response = await fetch(`${baseUrl}/v1/notifications/verify-webhook-signature`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({
auth_algo: req.headers['paypal-auth-algo'],
cert_url: req.headers['paypal-cert-url'],
transmission_id: req.headers['paypal-transmission-id'],
transmission_sig: req.headers['paypal-transmission-sig'],
transmission_time: req.headers['paypal-transmission-time'],
webhook_id: webhookId,
webhook_event: webhookEvent,
}),
});
const result = await response.json();
return result.verification_status === 'SUCCESS';
}
// Handle PayPal webhook events
app.post('/paypal/webhooks', async (req, res) => {
// Verify the webhook is from PayPal
try {
const isValid = await verifyPayPalWebhook(req);
if (!isValid) {
console.error('Invalid PayPal webhook signature');
return res.status(403).json({ error: 'Invalid signature' });
}
} catch (error) {
console.error('Webhook verification failed:', error.message);
return res.status(500).json({ error: 'Verification failed' });
}
const event = req.body;
const eventType = event.event_type;
const resource = event.resource;
console.log(`PayPal webhook: ${eventType}`);
const conn = await Datastore.open();
// Check if we already have this event (idempotency)
try {
await conn.getOne('paypal_events', event.id);
return res.status(200).json({ received: true }); // Already exists
} catch (e) {
// Not found, continue to store
}
// Store new event
await conn.insertOne('paypal_events', {
_id: event.id,
eventType,
resourceType: event.resource_type,
resourceId: resource.id,
rawBody: req.rawBody, // Store raw body for replay capability
createTime: event.create_time,
receivedAt: new Date().toISOString(),
});
// Handle specific event types
switch (eventType) {
case 'CHECKOUT.ORDER.APPROVED':
console.log(`Order approved: ${resource.id}`);
// Capture the payment or fulfill the order
break;
case 'PAYMENT.CAPTURE.COMPLETED':
console.log(`Payment captured: ${resource.id}, Amount: ${resource.amount?.value}`);
// Fulfill the order, send confirmation email
break;
case 'PAYMENT.CAPTURE.DENIED':
console.log(`Payment denied: ${resource.id}`);
// Cancel the order, notify customer
break;
case 'PAYMENT.CAPTURE.REFUNDED':
console.log(`Payment refunded: ${resource.id}`);
// Update order status, adjust inventory
break;
case 'CUSTOMER.DISPUTE.CREATED':
console.log(`Dispute created: ${resource.dispute_id}`);
// Alert team, gather evidence
break;
default:
console.log(`Unhandled event type: ${eventType}`);
}
res.status(200).json({ received: true });
});
export default app.init();
```
Deploy and configure:
```bash
coho deploy
coho set-env PAYPAL_CLIENT_ID "your_client_id" --encrypted
coho set-env PAYPAL_CLIENT_SECRET "your_client_secret" --encrypted
coho set-env PAYPAL_WEBHOOK_ID "your_webhook_id" --encrypted
coho set-env PAYPAL_API_URL "https://api-m.sandbox.paypal.com"
```
Your webhook URL will be: `https://my-paypal-handler-xxxx.api.codehooks.io/dev/paypal/webhooks`
## Configure PayPal Developer Dashboard
1. Go to [PayPal Developer Dashboard](https://developer.paypal.com/dashboard/applications/)
2. Select your app (or create one)
3. Scroll to **Webhooks** and click **Add Webhook**
4. Enter your Codehooks URL: `https://your-app.api.codehooks.io/dev/paypal/webhooks`
5. Select the events you want to receive
6. Click **Save**
7. Copy the **Webhook ID** and add it to your environment:
```bash
coho set-env PAYPAL_WEBHOOK_ID "WH-XXXXXXXXX" --encrypted
```
## PayPal Webhook Event Types
### Checkout Events
| Event | Description | When to Use |
|-------|-------------|-------------|
| `CHECKOUT.ORDER.APPROVED` | Buyer approved the order | Capture the payment |
| `CHECKOUT.ORDER.COMPLETED` | Order completed | Confirm fulfillment |
| `CHECKOUT.PAYMENT-APPROVAL.REVERSED` | Payment approval reversed | Cancel order |
### Payment Capture Events
| Event | Description | When to Use |
|-------|-------------|-------------|
| `PAYMENT.CAPTURE.COMPLETED` | Payment successfully captured | Fulfill order |
| `PAYMENT.CAPTURE.DENIED` | Payment capture denied | Cancel order |
| `PAYMENT.CAPTURE.PENDING` | Payment capture pending | Wait for completion |
| `PAYMENT.CAPTURE.REFUNDED` | Captured payment refunded | Update records |
| `PAYMENT.CAPTURE.REVERSED` | Captured payment reversed | Update records |
### Authorization Events
| Event | Description | When to Use |
|-------|-------------|-------------|
| `PAYMENT.AUTHORIZATION.CREATED` | Authorization created | Track authorization |
| `PAYMENT.AUTHORIZATION.VOIDED` | Authorization voided | Update records |
### Subscription Events
| Event | Description | When to Use |
|-------|-------------|-------------|
| `BILLING.SUBSCRIPTION.CREATED` | Subscription created | Provision access |
| `BILLING.SUBSCRIPTION.ACTIVATED` | Subscription activated | Start service |
| `BILLING.SUBSCRIPTION.CANCELLED` | Subscription cancelled | Revoke access |
| `BILLING.SUBSCRIPTION.EXPIRED` | Subscription expired | Revoke access |
| `PAYMENT.SALE.COMPLETED` | Subscription payment received | Extend access |
### Dispute Events
| Event | Description | When to Use |
|-------|-------------|-------------|
| `CUSTOMER.DISPUTE.CREATED` | Dispute opened | Alert team |
| `CUSTOMER.DISPUTE.RESOLVED` | Dispute resolved | Update records |
| `CUSTOMER.DISPUTE.UPDATED` | Dispute status changed | Track progress |
## Event Payload Examples
### PAYMENT.CAPTURE.COMPLETED
```json
{
"id": "WH-XXXXXXXXXX",
"event_version": "1.0",
"create_time": "2024-01-15T12:00:00.000Z",
"resource_type": "capture",
"event_type": "PAYMENT.CAPTURE.COMPLETED",
"summary": "Payment completed for $50.00 USD",
"resource": {
"id": "5O190127TN364715T",
"amount": {
"currency_code": "USD",
"value": "50.00"
},
"final_capture": true,
"seller_protection": {
"status": "ELIGIBLE"
},
"status": "COMPLETED",
"create_time": "2024-01-15T12:00:00Z",
"update_time": "2024-01-15T12:00:00Z"
},
"links": [
{
"href": "https://api.paypal.com/v2/payments/captures/5O190127TN364715T",
"rel": "self",
"method": "GET"
}
]
}
```
### CHECKOUT.ORDER.APPROVED
```json
{
"id": "WH-YYYYYYYYYY",
"event_version": "1.0",
"create_time": "2024-01-15T11:55:00.000Z",
"resource_type": "checkout-order",
"event_type": "CHECKOUT.ORDER.APPROVED",
"summary": "An order has been approved by buyer",
"resource": {
"id": "5O190127TN364715T",
"intent": "CAPTURE",
"status": "APPROVED",
"purchase_units": [
{
"reference_id": "default",
"amount": {
"currency_code": "USD",
"value": "50.00"
}
}
],
"payer": {
"email_address": "buyer@example.com",
"payer_id": "BUYERID123"
}
}
}
```
## Complete Payment Flow Handler
Handle the full checkout flow from approval to capture:
```javascript
import { app, Datastore } from 'codehooks-js';
app.auth('/paypal/*', (req, res, next) => {
next();
});
async function getPayPalAccessToken() {
const clientId = process.env.PAYPAL_CLIENT_ID;
const clientSecret = process.env.PAYPAL_CLIENT_SECRET;
const baseUrl = process.env.PAYPAL_API_URL || 'https://api-m.sandbox.paypal.com';
const response = await fetch(`${baseUrl}/v1/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
},
body: 'grant_type=client_credentials',
});
const data = await response.json();
return data.access_token;
}
async function verifyPayPalWebhook(req) {
const webhookId = process.env.PAYPAL_WEBHOOK_ID;
const baseUrl = process.env.PAYPAL_API_URL || 'https://api-m.sandbox.paypal.com';
const accessToken = await getPayPalAccessToken();
// Parse the raw body to ensure exact payload preservation
const webhookEvent = JSON.parse(req.rawBody);
const response = await fetch(`${baseUrl}/v1/notifications/verify-webhook-signature`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({
auth_algo: req.headers['paypal-auth-algo'],
cert_url: req.headers['paypal-cert-url'],
transmission_id: req.headers['paypal-transmission-id'],
transmission_sig: req.headers['paypal-transmission-sig'],
transmission_time: req.headers['paypal-transmission-time'],
webhook_id: webhookId,
webhook_event: webhookEvent,
}),
});
const result = await response.json();
return result.verification_status === 'SUCCESS';
}
// Capture payment after order approval
async function capturePayment(orderId) {
const baseUrl = process.env.PAYPAL_API_URL || 'https://api-m.sandbox.paypal.com';
const accessToken = await getPayPalAccessToken();
const response = await fetch(`${baseUrl}/v2/checkout/orders/${orderId}/capture`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
});
return response.json();
}
app.post('/paypal/webhooks', async (req, res) => {
try {
const isValid = await verifyPayPalWebhook(req);
if (!isValid) {
return res.status(403).json({ error: 'Invalid signature' });
}
} catch (error) {
console.error('Verification failed:', error.message);
return res.status(500).json({ error: 'Verification failed' });
}
const event = req.body;
const eventType = event.event_type;
const resource = event.resource;
const conn = await Datastore.open();
// Check if we already have this event (idempotency)
try {
await conn.getOne('paypal_events', event.id);
console.log('Duplicate event:', event.id);
return res.status(200).json({ received: true });
} catch (e) {
// Not found, continue to store
}
// Store new event
await conn.insertOne('paypal_events', {
_id: event.id,
eventType,
resourceId: resource.id,
rawBody: req.rawBody, // Store raw body for replay capability
processed: false,
receivedAt: new Date().toISOString(),
});
switch (eventType) {
case 'CHECKOUT.ORDER.APPROVED': {
console.log(`Order approved: ${resource.id}`);
// Auto-capture the payment
try {
const captureResult = await capturePayment(resource.id);
console.log('Payment captured:', captureResult.id);
await conn.updateOne('orders', { paypalOrderId: resource.id }, {
$set: {
status: 'paid',
captureId: captureResult.purchase_units?.[0]?.payments?.captures?.[0]?.id,
paidAt: new Date().toISOString(),
}
});
} catch (error) {
console.error('Capture failed:', error.message);
}
break;
}
case 'PAYMENT.CAPTURE.COMPLETED': {
console.log(`Payment completed: ${resource.id}`);
await conn.updateOne('orders', { captureId: resource.id }, {
$set: {
status: 'fulfilled',
amount: resource.amount?.value,
currency: resource.amount?.currency_code,
fulfilledAt: new Date().toISOString(),
}
});
break;
}
case 'PAYMENT.CAPTURE.DENIED': {
console.log(`Payment denied: ${resource.id}`);
await conn.updateOne('orders', { captureId: resource.id }, {
$set: {
status: 'denied',
deniedAt: new Date().toISOString(),
}
});
break;
}
case 'PAYMENT.CAPTURE.REFUNDED': {
console.log(`Payment refunded: ${resource.id}`);
await conn.updateOne('orders', { captureId: resource.id }, {
$set: {
status: 'refunded',
refundedAt: new Date().toISOString(),
}
});
break;
}
case 'CUSTOMER.DISPUTE.CREATED': {
console.log(`Dispute created: ${resource.dispute_id}`);
await conn.insertOne('disputes', {
disputeId: resource.dispute_id,
reason: resource.reason,
status: resource.status,
amount: resource.dispute_amount,
createdAt: new Date().toISOString(),
});
break;
}
}
// Mark event as processed
await conn.updateOne('paypal_events', event.id, {
$set: { processed: true, processedAt: new Date().toISOString() }
});
res.status(200).json({ received: true });
});
export default app.init();
```
## PayPal Signature Verification
PayPal uses postback verification — you send the webhook data back to PayPal's API to verify it's authentic. This requires:
1. **OAuth2 Access Token** — Obtained using your Client ID and Secret
2. **Webhook Headers** — Five headers from the incoming request
3. **Webhook ID** — Your registered webhook's ID from the PayPal Dashboard
4. **Original Payload** — The exact webhook body as received
```javascript
async function verifyPayPalWebhook(req) {
const accessToken = await getPayPalAccessToken();
// Parse the raw body to ensure exact payload preservation
const webhookEvent = JSON.parse(req.rawBody);
const response = await fetch(`${baseUrl}/v1/notifications/verify-webhook-signature`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
},
body: JSON.stringify({
auth_algo: req.headers['paypal-auth-algo'],
cert_url: req.headers['paypal-cert-url'],
transmission_id: req.headers['paypal-transmission-id'],
transmission_sig: req.headers['paypal-transmission-sig'],
transmission_time: req.headers['paypal-transmission-time'],
webhook_id: process.env.PAYPAL_WEBHOOK_ID,
webhook_event: webhookEvent,
}),
});
const result = await response.json();
return result.verification_status === 'SUCCESS';
}
```
**Important:** Use `req.rawBody` (the raw string), not `req.body` (parsed JSON). The signature is computed over the exact bytes PayPal sent, and middleware parsing could alter key ordering or whitespace.
## Best Practices
### 1. Handle Events Idempotently
PayPal retries failed deliveries up to 25 times. Use the event ID to prevent duplicate processing:
```javascript
try {
await conn.getOne('paypal_events', { eventId: event.id });
return res.status(200).json({ received: true });
} catch (e) {
// Not found, continue to process
}
```
### 2. Respond Quickly
Store the event first, then return 200 immediately. PayPal expects a response within seconds:
```javascript
const conn = await Datastore.open();
const eventId = req.body.id;
// Check if we already have this event (idempotency)
try {
await conn.getOne('paypal_events', eventId);
return res.status(200).json({ received: true }); // Already exists
} catch (e) {
// Not found, continue to store
}
// Store new event
await conn.insertOne('paypal_events', {
_id: eventId,
eventType: req.body.event_type,
rawBody: req.rawBody, // Store raw body for replay capability
processedAt: null
});
res.status(200).json({ received: true });
```
### 3. Use Environment Variables
```bash
# Sandbox
coho set-env PAYPAL_API_URL "https://api-m.sandbox.paypal.com"
# Production
coho set-env PAYPAL_API_URL "https://api-m.paypal.com"
```
### 4. Fetch Fresh Data
Events can arrive out of order. For critical operations, fetch the latest state:
```javascript
case 'PAYMENT.CAPTURE.COMPLETED':
// Fetch the latest capture details from PayPal API
const capture = await getCapture(resource.id);
// Use capture.status, capture.amount, etc.
break;
```
## Sandbox vs Production
| Environment | API Base URL | Dashboard |
|-------------|--------------|-----------|
| Sandbox | `https://api-m.sandbox.paypal.com` | [sandbox.paypal.com](https://www.sandbox.paypal.com/) |
| Production | `https://api-m.paypal.com` | [paypal.com](https://www.paypal.com/) |
Switch environments by updating:
```bash
coho set-env PAYPAL_API_URL "https://api-m.paypal.com"
coho set-env PAYPAL_CLIENT_ID "your_live_client_id" --encrypted
coho set-env PAYPAL_CLIENT_SECRET "your_live_secret" --encrypted
coho set-env PAYPAL_WEBHOOK_ID "your_live_webhook_id" --encrypted
```
## Why Use Codehooks.io for PayPal Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours | 5 minutes |
| SSL/HTTPS | Configure certificates | Built-in |
| OAuth2 token management | Manual implementation | Simple fetch |
| Database for events | Set up separately | Built-in |
| Idempotency handling | Build key-value store | Built-in |
| Async processing | Configure queues | Built-in workers |
| Monitoring | Set up logging | Built-in logs |
| Cost | Server costs | Free tier available |
## Related Resources
- [PayPal Webhooks Documentation](https://developer.paypal.com/api/rest/webhooks/)
- [PayPal Webhook Event Names](https://developer.paypal.com/api/rest/webhooks/event-names/)
- [PayPal Webhook Integration Guide](https://developer.paypal.com/api/rest/webhooks/rest/)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Alternative payment provider
- [Shopify Webhooks Example](/docs/examples/webhooks/shopify) - E-commerce webhooks
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## SendGrid Webhooks Integration Example: Track Email Delivery & Engagement
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# SendGrid Webhooks Integration Example: Track Email Delivery & Engagement
SendGrid Event Webhooks notify your application when email events occur — messages are delivered, opened, clicked, bounced, or marked as spam. Instead of polling the API, webhooks push real-time event data to your endpoint.
**The problem?** Setting up SendGrid webhooks requires ECDSA signature verification, handling raw request bodies, and processing multiple event types in a single payload.
**The solution?** Deploy a production-ready SendGrid webhook handler with Codehooks.io in under 5 minutes.
:::info New to webhooks?
If you're new to webhooks, check out our guide on [What are webhooks?](/docs/examples/webhooks/what-are-webhooks) to understand the fundamentals before diving into this SendGrid-specific implementation.
:::
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
You'll also need:
- A [SendGrid account](https://signup.sendgrid.com/) with email sending configured
- Access to [SendGrid Mail Settings](https://app.sendgrid.com/settings/mail_settings) for webhook configuration
## Quick Start: Email Event Handler
Create a new Codehooks project:
```bash
coho create my-sendgrid-handler
cd my-sendgrid-handler
npm install @sendgrid/eventwebhook
```
Replace `index.js` with:
```javascript
import { app, Datastore } from 'codehooks-js';
import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook';
// Bypass JWT auth for SendGrid webhook endpoint
app.auth('/sendgrid/*', (req, res, next) => {
next();
});
// Verify SendGrid webhook signature
function verifySignature(publicKey, payload, signature, timestamp) {
const eventWebhook = new EventWebhook();
const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
return eventWebhook.verifySignature(ecPublicKey, payload, signature, timestamp);
}
// Handle SendGrid Event Webhook
app.post('/sendgrid/events', async (req, res) => {
const publicKey = process.env.SENDGRID_WEBHOOK_PUBLIC_KEY;
const signature = req.headers[EventWebhookHeader.SIGNATURE().toLowerCase()];
const timestamp = req.headers[EventWebhookHeader.TIMESTAMP().toLowerCase()];
// Verify signature using raw body
if (publicKey && signature && timestamp) {
const isValid = verifySignature(publicKey, req.rawBody, signature, timestamp);
if (!isValid) {
console.error('Invalid SendGrid signature');
return res.status(403).json({ error: 'Invalid signature' });
}
}
// SendGrid sends an array of events
const events = req.body;
if (!Array.isArray(events)) {
return res.status(400).json({ error: 'Expected array of events' });
}
console.log(`Received ${events.length} SendGrid events`);
const conn = await Datastore.open();
// Process each event
for (const event of events) {
await conn.insertOne('email_events', {
email: event.email,
event: event.event,
timestamp: event.timestamp,
messageId: event.sg_message_id,
category: event.category || [],
receivedAt: new Date().toISOString(),
});
// Handle specific event types
switch (event.event) {
case 'delivered':
console.log(`Email delivered to ${event.email}`);
break;
case 'open':
console.log(`Email opened by ${event.email}`);
break;
case 'click':
console.log(`Link clicked by ${event.email}: ${event.url}`);
break;
case 'bounce':
console.log(`Email bounced for ${event.email}: ${event.reason}`);
break;
case 'spamreport':
console.log(`Spam report from ${event.email}`);
break;
case 'unsubscribe':
console.log(`Unsubscribe from ${event.email}`);
break;
}
}
res.status(200).json({ received: events.length });
});
export default app.init();
```
Deploy and configure:
```bash
coho deploy
coho set-env SENDGRID_WEBHOOK_PUBLIC_KEY "your_public_key_here" --encrypted
```
Your webhook URL will be: `https://my-sendgrid-handler-xxxx.api.codehooks.io/dev/sendgrid/events`
## Configure SendGrid Dashboard
1. Go to [SendGrid Settings → Mail Settings](https://app.sendgrid.com/settings/mail_settings)
2. Click **Event Webhooks** → **Create new webhook**
3. Configure:
- **Friendly Name**: Codehooks Event Handler
- **Post URL**: `https://your-app.api.codehooks.io/dev/sendgrid/events`
- **Actions to be posted**: Select the events you want to track
4. Under **Security features**, enable **Signed Event Webhook**
5. Click **Save** — this generates your public key
6. Copy the **Verification Key** and add it to Codehooks:
```bash
coho set-env SENDGRID_WEBHOOK_PUBLIC_KEY "MFkwEwYHKoZIzj0CA..." --encrypted
```
## SendGrid Event Types
### Delivery Events
| Event | Description | Key Fields |
|-------|-------------|------------|
| `processed` | SendGrid accepted the message | `email`, `sg_message_id` |
| `delivered` | Message delivered to recipient server | `email`, `response` |
| `bounce` | Hard bounce — address doesn't exist | `email`, `reason`, `type` |
| `blocked` | Soft bounce — temporarily rejected | `email`, `reason` |
| `deferred` | Delivery delayed, will retry | `email`, `response` |
| `dropped` | Message rejected by SendGrid | `email`, `reason` |
### Engagement Events
| Event | Description | Key Fields |
|-------|-------------|------------|
| `open` | Recipient opened the email | `email`, `useragent`, `ip` |
| `click` | Recipient clicked a link | `email`, `url`, `useragent` |
| `spamreport` | Marked as spam | `email` |
| `unsubscribe` | Clicked unsubscribe link | `email` |
| `group_unsubscribe` | Unsubscribed from group | `email`, `asm_group_id` |
| `group_resubscribe` | Resubscribed to group | `email`, `asm_group_id` |
## Event Payload Examples
### Delivered Event
```json
{
"email": "user@example.com",
"event": "delivered",
"sg_message_id": "abc123.xyz",
"timestamp": 1701388800,
"smtp-id": "",
"response": "250 OK",
"category": ["newsletter"]
}
```
### Click Event
```json
{
"email": "user@example.com",
"event": "click",
"sg_message_id": "abc123.xyz",
"timestamp": 1701388900,
"url": "https://example.com/promo",
"useragent": "Mozilla/5.0...",
"ip": "192.168.1.1"
}
```
### Bounce Event
```json
{
"email": "invalid@example.com",
"event": "bounce",
"sg_message_id": "abc123.xyz",
"timestamp": 1701388800,
"type": "bounce",
"reason": "550 User not found",
"status": "5.1.1"
}
```
## Advanced: Track Email Campaigns
Store detailed analytics for email campaigns:
```javascript
import { app, Datastore } from 'codehooks-js';
import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook';
app.auth('/sendgrid/*', (req, res, next) => {
next();
});
function verifySignature(publicKey, payload, signature, timestamp) {
const eventWebhook = new EventWebhook();
const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
return eventWebhook.verifySignature(ecPublicKey, payload, signature, timestamp);
}
app.post('/sendgrid/events', async (req, res) => {
const publicKey = process.env.SENDGRID_WEBHOOK_PUBLIC_KEY;
const signature = req.headers[EventWebhookHeader.SIGNATURE().toLowerCase()];
const timestamp = req.headers[EventWebhookHeader.TIMESTAMP().toLowerCase()];
if (publicKey && signature && timestamp) {
const isValid = verifySignature(publicKey, req.rawBody, signature, timestamp);
if (!isValid) {
return res.status(403).json({ error: 'Invalid signature' });
}
}
const events = req.body;
const conn = await Datastore.open();
for (const event of events) {
// Store raw event
await conn.insertOne('email_events', {
...event,
receivedAt: new Date().toISOString(),
});
// Update campaign stats using the message ID
const messageId = event.sg_message_id?.split('.')[0];
if (messageId) {
const updateField = getUpdateField(event.event);
if (updateField) {
await conn.updateOne(
'campaign_stats',
{ messageId },
{
$inc: { [updateField]: 1 },
$set: { lastEventAt: new Date().toISOString() }
},
{ upsert: true }
);
}
}
// Handle bounces and spam reports for list hygiene
if (event.event === 'bounce' || event.event === 'spamreport') {
await conn.updateOne(
'subscribers',
{ email: event.email },
{
$set: {
status: event.event === 'bounce' ? 'bounced' : 'complained',
statusReason: event.reason || event.event,
statusDate: new Date().toISOString(),
}
}
);
}
}
res.status(200).json({ processed: events.length });
});
function getUpdateField(eventType) {
const mapping = {
'processed': 'processed',
'delivered': 'delivered',
'open': 'opens',
'click': 'clicks',
'bounce': 'bounces',
'dropped': 'dropped',
'spamreport': 'spamReports',
'unsubscribe': 'unsubscribes',
};
return mapping[eventType];
}
export default app.init();
```
## Async Processing for High Volume
For high-volume email sending, process events asynchronously:
```javascript
import { app, Datastore } from 'codehooks-js';
import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook';
// Worker for async event processing
app.worker('processEmailEvent', async (req, res) => {
const event = req.body.payload;
const conn = await Datastore.open();
// Store event
await conn.insertOne('email_events', {
...event,
processedAt: new Date().toISOString(),
});
// Handle bounces - update subscriber status
if (event.event === 'bounce' && event.type === 'bounce') {
await conn.updateOne(
'subscribers',
{ email: event.email },
{ $set: { status: 'bounced', bounceReason: event.reason } }
);
}
// Handle spam reports - flag for removal
if (event.event === 'spamreport') {
await conn.updateOne(
'subscribers',
{ email: event.email },
{ $set: { status: 'complained', doNotEmail: true } }
);
}
res.end();
});
app.auth('/sendgrid/*', (req, res, next) => {
next();
});
function verifySignature(publicKey, payload, signature, timestamp) {
const eventWebhook = new EventWebhook();
const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
return eventWebhook.verifySignature(ecPublicKey, payload, signature, timestamp);
}
app.post('/sendgrid/events', async (req, res) => {
const publicKey = process.env.SENDGRID_WEBHOOK_PUBLIC_KEY;
const signature = req.headers[EventWebhookHeader.SIGNATURE().toLowerCase()];
const timestamp = req.headers[EventWebhookHeader.TIMESTAMP().toLowerCase()];
if (publicKey && signature && timestamp) {
const isValid = verifySignature(publicKey, req.rawBody, signature, timestamp);
if (!isValid) {
return res.status(403).json({ error: 'Invalid signature' });
}
}
const events = req.body;
const conn = await Datastore.open();
// Queue each event for async processing
for (const event of events) {
await conn.enqueue('processEmailEvent', event);
}
// Respond immediately
res.status(200).json({ queued: events.length });
});
export default app.init();
```
## SendGrid Signature Verification
SendGrid uses ECDSA (Elliptic Curve Digital Signature Algorithm) to sign webhooks. The signature is sent in two headers:
- `X-Twilio-Email-Event-Webhook-Signature` — Base64-encoded signature
- `X-Twilio-Email-Event-Webhook-Timestamp` — Unix timestamp
**Important:** Signature verification requires the raw request body. Using parsed JSON will fail because the exact bytes matter for cryptographic verification.
```javascript
import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook';
function verifySignature(publicKey, payload, signature, timestamp) {
const eventWebhook = new EventWebhook();
// Convert the public key to ECDSA format
const ecPublicKey = eventWebhook.convertPublicKeyToECDSA(publicKey);
// Verify using raw payload (not parsed JSON)
return eventWebhook.verifySignature(ecPublicKey, payload, signature, timestamp);
}
// In your handler, use req.rawBody (not req.body) for verification
const isValid = verifySignature(publicKey, req.rawBody, signature, timestamp);
```
## Best Practices
### 1. Always Verify Signatures in Production
```javascript
if (!publicKey) {
console.warn('SENDGRID_WEBHOOK_PUBLIC_KEY not set - skipping verification');
} else {
const isValid = verifySignature(publicKey, req.rawBody, signature, timestamp);
if (!isValid) {
return res.status(403).json({ error: 'Invalid signature' });
}
}
```
### 2. Handle Batch Events
SendGrid sends multiple events in a single request. Always process as an array:
```javascript
const events = Array.isArray(req.body) ? req.body : [req.body];
for (const event of events) {
// Process each event
}
```
### 3. Implement Idempotency
Use `sg_event_id` to prevent duplicate processing:
```javascript
const conn = await Datastore.open();
try {
await conn.getOne('email_events', { eventId: event.sg_event_id });
console.log('Event already processed:', event.sg_event_id);
continue;
} catch (e) {
// Not found, continue to process
}
```
### 4. Clean Bounce and Spam Lists
Automatically maintain list hygiene:
```javascript
if (event.event === 'bounce' || event.event === 'spamreport' || event.event === 'unsubscribe') {
await conn.updateOne(
'subscribers',
{ email: event.email },
{ $set: { active: false, reason: event.event } }
);
}
```
## Why Use Codehooks.io for SendGrid Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours | 5 minutes |
| SSL/HTTPS | Configure certificates | Built-in |
| Raw body access | Middleware configuration | Built-in `req.rawBody` |
| Database for events | Set up separately | Built-in |
| Async processing | Configure queues | Built-in workers |
| Monitoring | Set up logging | Built-in logs |
| Cost | Server costs | Free tier available |
## Related Resources
- [SendGrid Event Webhook Documentation](https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook)
- [SendGrid Webhook Security](https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features)
- [SendGrid Event Types Reference](https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/event)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [Twilio Webhooks Example](/docs/examples/webhooks/twilio) - SMS and voice webhooks (SendGrid parent company)
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Payment webhook handler
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## Shopify Webhooks Integration Example: Handle Orders & Inventory Events
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# Shopify Webhooks Integration Example: Handle Orders & Inventory Events
Shopify webhooks notify your application in real-time when events occur in a store — orders are created, products are updated, inventory changes, and more. Instead of polling the Shopify API, webhooks push data to your app instantly.
**The problem?** Setting up Shopify webhooks requires HMAC verification, handling duplicate events, managing webhook subscriptions, and ensuring reliability.
**The solution?** Deploy a Shopify webhook handler with Codehooks.io in under 5 minutes using our ready-made template.
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
## Quick Deploy with Template
The fastest way to get started is using the Codehooks Shopify webhook template:
```bash
coho create myshopifyhandler --template webhook-shopify-minimal
```
You'll see output like:
```
Creating project: myshopifyhandler
Project created successfully!
Your API endpoints:
https://myshopifyhandler-a1b2.api.codehooks.io/dev
```
Then install, deploy, and configure:
```bash
cd myshopifyhandler
npm install
coho deploy
coho set-env SHOPIFY_WEBHOOK_SECRET your-webhook-secret --encrypted
```
That's it! The template includes HMAC verification, event handling, and database storage out of the box.
:::tip Use the Template
The `webhook-shopify-minimal` template includes HMAC verification, error handling, and event storage to get you started quickly. Browse all available templates in the [templates repository](https://github.com/RestDB/codehooks-io-templates).
:::
## Custom Implementation
If you prefer to build from scratch, create a new project and add your Shopify webhook handler code to `index.js`:
```javascript
import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';
// Allow webhook requests without JWT authentication
app.auth('/shopify/*', (req, res, next) => next());
// Shopify webhook endpoint
app.post('/shopify/webhooks', async (req, res) => {
// Verify the webhook signature
const isValid = verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET);
if (!isValid) {
console.error('Invalid Shopify webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
const topic = req.headers['x-shopify-topic'];
const shop = req.headers['x-shopify-shop-domain'];
const eventId = req.headers['x-shopify-event-id'];
const payload = req.body;
const conn = await Datastore.open();
// Check for duplicate events
const existing = await conn.findOneOrNull('shopify_events', eventId);
if (existing) {
console.log('Duplicate event, skipping:', eventId);
return res.json({ received: true, duplicate: true });
}
// Store the event
await conn.insertOne('shopify_events', {
_id: eventId,
topic,
shop,
payload,
processedAt: new Date().toISOString()
});
// Handle different webhook topics
switch (topic) {
case 'orders/create':
await handleOrderCreated(payload, shop);
break;
case 'orders/fulfilled':
await handleOrderFulfilled(payload, shop);
break;
case 'products/update':
await handleProductUpdated(payload, shop);
break;
case 'inventory_levels/update':
await handleInventoryUpdate(payload, shop);
break;
case 'customers/create':
await handleCustomerCreated(payload, shop);
break;
default:
console.log(`Unhandled topic: ${topic}`);
}
res.json({ received: true });
});
async function handleOrderCreated(order, shop) {
console.log(`New order ${order.id} from ${shop}`);
console.log(`Total: ${order.total_price} ${order.currency}`);
console.log(`Customer: ${order.customer?.email}`);
// Your business logic here:
// - Send to fulfillment system
// - Update inventory
// - Send confirmation email
// - Notify warehouse
}
async function handleOrderFulfilled(order, shop) {
console.log(`Order ${order.id} fulfilled`);
// Your business logic:
// - Send shipping notification
// - Update CRM
}
async function handleProductUpdated(product, shop) {
console.log(`Product ${product.id} updated: ${product.title}`);
// Your business logic:
// - Sync to external catalog
// - Update search index
}
async function handleInventoryUpdate(inventory, shop) {
console.log(`Inventory updated for item ${inventory.inventory_item_id}`);
console.log(`New quantity: ${inventory.available}`);
// Your business logic:
// - Alert if low stock
// - Update external systems
}
async function handleCustomerCreated(customer, shop) {
console.log(`New customer: ${customer.email}`);
// Your business logic:
// - Add to email list
// - Create CRM record
}
export default app.init();
```
Deploy:
```bash
coho deploy
```
```
Project: myshopifyhandler-a1b2 Space: dev
Deployed Codehook successfully! 🙌
```
:::tip Finding your API endpoint
Run `coho info` to see your API endpoints and tokens. Your webhook URL will be:
`https://myshopifyhandler-a1b2.api.codehooks.io/dev/shopify/webhooks`
Codehooks also provides alternative URLs without the space name (e.g., `https://gracious-inlet-fd79.codehooks.io/shopify/webhooks`). On paid plans, you can also use custom domains for production use.
:::
## Configure Shopify Webhooks
### Option 1: Via Shopify Admin
1. Go to **Settings → Notifications → Webhooks**
2. Click **Create webhook**
3. Select event (e.g., "Order creation")
4. Enter your Codehooks URL
5. Select format: **JSON**
6. Save
### Option 2: Via Shopify API
```javascript
// Register a webhook programmatically
const response = await fetch(
`https://${shop}/admin/api/2024-01/webhooks.json`,
{
method: 'POST',
headers: {
'X-Shopify-Access-Token': accessToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhook: {
topic: 'orders/create',
address: 'https://your-app.api.codehooks.io/dev/shopify/webhooks',
format: 'json'
}
})
}
);
```
### Set Environment Variables
```bash
coho set-env SHOPIFY_WEBHOOK_SECRET your-webhook-secret --encrypted
```
Find your webhook secret in Shopify Admin → Settings → Notifications → Webhooks (at the bottom of the page).
## Common Shopify Webhook Topics
| Topic | Description | Common Use Case |
|-------|-------------|-----------------|
| `orders/create` | New order placed | Fulfillment, inventory sync |
| `orders/updated` | Order modified | Status updates |
| `orders/fulfilled` | Order shipped | Shipping notifications |
| `orders/cancelled` | Order canceled | Refund processing |
| `products/create` | New product added | Catalog sync |
| `products/update` | Product modified | Search index update |
| `products/delete` | Product removed | Cleanup external systems |
| `inventory_levels/update` | Stock changed | Low stock alerts |
| `customers/create` | New customer | CRM sync, welcome email |
| `fulfillments/create` | Fulfillment created | Tracking updates |
| `refunds/create` | Refund issued | Accounting sync |
| `checkouts/create` | Checkout started | Abandoned cart tracking |
## Shopify Webhook Headers Reference
Shopify includes several headers with every webhook request. These headers are essential for verification, deduplication, and routing:
| Header | Description | Example Value |
|--------|-------------|---------------|
| `X-Shopify-Topic` | The webhook event type | `orders/create` |
| `X-Shopify-Hmac-SHA256` | Base64-encoded HMAC signature for verification | `XfH3...8Q==` |
| `X-Shopify-Shop-Domain` | The shop's myshopify.com domain | `mystore.myshopify.com` |
| `X-Shopify-Webhook-Id` | Unique ID for the webhook subscription | `b123456-...` |
| `X-Shopify-Event-Id` | Unique ID for this specific event (use for deduplication) | `e789012-...` |
| `X-Shopify-Triggered-At` | ISO 8601 timestamp when the event was triggered | `2024-01-15T10:30:00.000Z` |
| `X-Shopify-API-Version` | API version used for the payload | `2024-01` |
```javascript
import { app } from 'codehooks-js';
// Allow webhook requests without JWT authentication
app.auth('/shopify/*', (req, res, next) => next());
app.post('/shopify/webhooks', async (req, res) => {
const topic = req.headers['x-shopify-topic'];
const hmac = req.headers['x-shopify-hmac-sha256'];
const shop = req.headers['x-shopify-shop-domain'];
const webhookId = req.headers['x-shopify-webhook-id'];
const eventId = req.headers['x-shopify-event-id'];
const triggeredAt = req.headers['x-shopify-triggered-at'];
console.log(`Received ${topic} from ${shop} at ${triggeredAt}`);
// ...
});
export default app.init();
```
## Webhook Payload Examples
### orders/create Webhook Payload Example
The `orders/create` webhook fires when a customer completes checkout. Here's the payload structure:
```json
{
"id": 5678901234567,
"email": "customer@example.com",
"created_at": "2024-01-15T10:30:00-05:00",
"updated_at": "2024-01-15T10:30:00-05:00",
"number": 1234,
"order_number": 1234,
"total_price": "149.99",
"subtotal_price": "139.99",
"total_tax": "10.00",
"currency": "USD",
"financial_status": "paid",
"fulfillment_status": null,
"customer": {
"id": 1234567890123,
"email": "customer@example.com",
"first_name": "John",
"last_name": "Doe",
"phone": "+1-555-123-4567",
"tags": "vip,returning",
"orders_count": 5,
"total_spent": "599.95"
},
"billing_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "New York",
"province": "NY",
"zip": "10001",
"country": "US"
},
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "New York",
"province": "NY",
"zip": "10001",
"country": "US"
},
"line_items": [
{
"id": 9876543210987,
"product_id": 1111111111111,
"variant_id": 2222222222222,
"title": "Premium Widget",
"quantity": 2,
"price": "69.99",
"sku": "WIDGET-001",
"vendor": "Acme Co"
}
],
"shipping_lines": [
{
"title": "Standard Shipping",
"price": "9.99",
"code": "standard"
}
],
"discount_codes": [],
"note": "Please gift wrap",
"tags": ""
}
```
### checkouts/create Webhook Payload Example
The `checkouts/create` webhook fires when a customer initiates checkout — useful for abandoned cart tracking and recovery campaigns.
```json
{
"id": 9876543210987,
"token": "abc123def456",
"cart_token": "cart_789xyz",
"email": "customer@example.com",
"created_at": "2024-01-15T10:25:00-05:00",
"updated_at": "2024-01-15T10:25:00-05:00",
"completed_at": null,
"currency": "USD",
"total_price": "149.99",
"subtotal_price": "139.99",
"total_tax": "10.00",
"customer": {
"id": 1234567890123,
"email": "customer@example.com",
"first_name": "John",
"last_name": "Doe",
"phone": "+1-555-123-4567"
},
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "New York",
"province": "NY",
"zip": "10001",
"country": "US"
},
"line_items": [
{
"product_id": 1111111111111,
"variant_id": 2222222222222,
"title": "Premium Widget",
"quantity": 2,
"price": "69.99",
"sku": "WIDGET-001"
}
],
"abandoned_checkout_url": "https://mystore.myshopify.com/checkouts/abc123/recover"
}
```
**Key fields in checkouts/create:**
| Field | Description | Notes |
|-------|-------------|-------|
| `email` | Customer's email address | Available if customer entered email or is logged in |
| `customer.id` | Shopify customer ID | Only present if customer is logged in |
| `abandoned_checkout_url` | Recovery URL to send to customer | Use in abandoned cart emails |
| `completed_at` | Null for active checkouts | Becomes timestamp when order is placed |
:::tip checkouts/create vs orders/create
Use `checkouts/create` for abandoned cart tracking — it fires when checkout begins. Use `orders/create` for fulfillment and order processing — it fires only when payment is complete.
:::
### carts/create Webhook Payload Example
The `carts/create` webhook fires when a customer adds their first item to cart — useful for early engagement tracking.
```json
{
"id": "cart_abc123def456",
"token": "abc123def456",
"created_at": "2024-01-15T10:20:00-05:00",
"updated_at": "2024-01-15T10:20:00-05:00",
"currency": "USD",
"customer_id": 1234567890123,
"line_items": [
{
"id": 9876543210987,
"product_id": 1111111111111,
"variant_id": 2222222222222,
"title": "Premium Widget",
"quantity": 1,
"price": "69.99",
"sku": "WIDGET-001",
"properties": {}
}
],
"note": ""
}
```
**Key fields in carts/create:**
| Field | Description | Notes |
|-------|-------------|-------|
| `customer_id` | Shopify customer ID | **Only present if customer is logged in** |
| `token` | Cart token | Use to track cart across sessions |
| `line_items` | Products in cart | Array of items with product details |
:::note Customer identification in carts
The `customer_id` field is only included in `carts/create` and `carts/update` when the customer is logged into their account. For guest shoppers, you won't have customer identification until they enter their email at checkout.
:::
### carts/update Webhook Payload Example
The `carts/update` webhook fires when items are added, removed, or quantities change:
```json
{
"id": "cart_abc123def456",
"token": "abc123def456",
"created_at": "2024-01-15T10:20:00-05:00",
"updated_at": "2024-01-15T10:22:00-05:00",
"currency": "USD",
"customer_id": 1234567890123,
"line_items": [
{
"id": 9876543210987,
"product_id": 1111111111111,
"variant_id": 2222222222222,
"title": "Premium Widget",
"quantity": 3,
"price": "69.99",
"sku": "WIDGET-001"
},
{
"id": 9876543210988,
"product_id": 1111111111112,
"variant_id": 2222222222223,
"title": "Widget Accessory Pack",
"quantity": 1,
"price": "19.99",
"sku": "WIDGET-ACC-001"
}
],
"note": "Gift for a friend"
}
```
### inventory_levels/update Webhook Payload Example
The `inventory_levels/update` webhook fires when inventory quantities change — essential for low-stock alerts and multi-channel inventory sync.
```json
{
"inventory_item_id": 1234567890123,
"location_id": 9876543210987,
"available": 5,
"updated_at": "2024-01-15T10:35:00-05:00"
}
```
**Key fields in inventory_levels/update:**
| Field | Description | Notes |
|-------|-------------|-------|
| `inventory_item_id` | ID of the inventory item | Links to product variant |
| `location_id` | ID of the inventory location | Warehouse or store location |
| `available` | Current available quantity | After the update |
**Example: Low stock alerts with Slack:**
```javascript
import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';
app.auth('/shopify/*', (req, res, next) => next());
app.post('/shopify/webhooks/inventory', async (req, res) => {
// Verify signature
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { inventory_item_id, location_id, available } = req.body;
const conn = await Datastore.open();
// Low stock alert
if (available <= 5 && available > 0) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `Low stock alert: Item ${inventory_item_id} has only ${available} units`
})
});
}
// Out of stock alert
if (available === 0) {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `Out of stock: Item ${inventory_item_id} at location ${location_id}`
})
});
}
// Store inventory level for tracking
await conn.updateOne('inventory_levels',
{ inventory_item_id, location_id },
{ $set: { available, updatedAt: new Date().toISOString() } },
{ upsert: true }
);
res.json({ received: true });
});
export default app.init();
```
## Shopify Webhook Retry Policy
Shopify automatically retries failed webhook deliveries using exponential backoff:
- **Retry window:** Up to 48 hours
- **Backoff schedule:** Retries at increasing intervals (1 min, 2 min, 5 min, 10 min, etc.)
- **Failure threshold:** After **19 consecutive failures**, Shopify automatically removes the webhook subscription
- **Success response:** Any 2xx status code (200, 201, 204, etc.)
- **Failure response:** 4xx or 5xx status codes, or timeout (>5 seconds)
**Monitoring webhook health:**
1. Go to **Shopify Admin → Settings → Notifications → Webhooks**
2. Check the status indicator next to each webhook
3. Failed webhooks show error counts and last failure time
:::warning Webhook removal after failures
If your endpoint returns errors 19 times in a row, Shopify will delete the webhook subscription entirely. You'll need to re-register it. Implement monitoring to catch issues before this happens.
:::
## Shopify HMAC Verification
Shopify signs all webhook payloads with an HMAC-SHA256 signature. Always verify this signature before processing.
### Using webhook-verify (Recommended)
The [`webhook-verify`](https://www.npmjs.com/package/webhook-verify) package provides a simple, unified API for verifying webhooks from 20+ providers including Shopify, Stripe, GitHub, Slack, and more:
```bash
npm install webhook-verify
```
```javascript
import { verify } from 'webhook-verify';
app.post('/shopify/webhooks', async (req, res) => {
const isValid = verify(
'shopify',
req.rawBody,
req.headers,
process.env.SHOPIFY_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
});
```
The same package works for other webhook providers — just change the provider name:
```javascript
// Stripe webhooks
verify('stripe', req.rawBody, req.headers, process.env.STRIPE_WEBHOOK_SECRET);
// GitHub webhooks
verify('github', req.rawBody, req.headers, process.env.GITHUB_WEBHOOK_SECRET);
// Slack webhooks
verify('slack', req.rawBody, req.headers, process.env.SLACK_SIGNING_SECRET);
```
### Manual Verification
If you prefer to implement verification manually without dependencies:
```javascript
import crypto from 'crypto';
function verifyShopifyWebhook(req) {
const hmacHeader = req.headers['x-shopify-hmac-sha256'];
const secret = process.env.SHOPIFY_WEBHOOK_SECRET;
// Must use raw body, not parsed JSON
const body = req.rawBody;
const calculatedHmac = crypto
.createHmac('sha256', secret)
.update(body, 'utf8')
.digest('base64');
// Use timing-safe comparison to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(hmacHeader),
Buffer.from(calculatedHmac)
);
} catch (e) {
return false;
}
}
```
**Important:** Always use `req.rawBody` (the raw string), not `req.body` (parsed JSON). The signature is computed over the exact bytes Shopify sent.
## Handling Duplicate Events
Shopify may send the same webhook multiple times. Use the `X-Shopify-Event-Id` header for idempotency:
```javascript
import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';
app.auth('/shopify/*', (req, res, next) => next());
app.post('/shopify/webhooks', async (req, res) => {
// Always verify first
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const eventId = req.headers['x-shopify-event-id'];
const conn = await Datastore.open();
// Check if we've already processed this event
const existing = await conn.findOneOrNull('processed_events', eventId);
if (existing) {
console.log('Duplicate event, skipping:', eventId);
return res.json({ received: true });
}
// Mark as processed first (before heavy work)
await conn.insertOne('processed_events', {
_id: eventId,
topic: req.headers['x-shopify-topic'],
processedAt: new Date().toISOString()
});
// Process the event (your business logic here)
const payload = req.body;
console.log(`Processing ${req.headers['x-shopify-topic']}:`, payload.id);
res.json({ received: true });
});
export default app.init();
```
## Best Practices for Shopify Webhooks
### 1. Respond Quickly
Shopify expects a response within 5 seconds. Verify, store the event, and respond immediately — then process asynchronously:
```javascript
import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';
app.auth('/shopify/*', (req, res, next) => next());
app.post('/shopify/webhooks', async (req, res) => {
// Verify signature first
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const conn = await Datastore.open();
const eventId = req.headers['x-shopify-event-id'];
// Check if we already have this event (idempotency)
const existing = await conn.findOneOrNull('shopify_events', eventId);
if (existing) {
return res.json({ received: true });
}
// Store event for async processing
await conn.insertOne('shopify_events', {
_id: eventId,
topic: req.headers['x-shopify-topic'],
shopDomain: req.headers['x-shopify-shop-domain'],
payload: req.body,
processedAt: null
});
// Respond immediately, process later via worker queue or cron job
res.json({ received: true });
});
export default app.init();
```
### 2. Handle Event Ordering
Events may arrive out of order. Use the `updated_at` timestamp from the payload to avoid overwriting newer data with older events:
```javascript
const payloadUpdatedAt = new Date(req.body.updated_at);
// Fetch existing record
const existing = await conn.findOneOrNull('products', req.body.id);
// Only update if this event is newer than what we have
if (!existing || new Date(existing.updatedAt) < payloadUpdatedAt) {
await conn.updateOne('products', req.body.id, {
$set: { ...req.body, updatedAt: req.body.updated_at }
}, { upsert: true });
}
```
### 3. Handle Mandatory Compliance Webhooks (GDPR)
If your app is in the Shopify App Store, you **must** handle these mandatory webhooks for GDPR/privacy compliance:
```javascript
import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';
app.auth('/shopify/*', (req, res, next) => next());
// Mandatory webhook: Customer requests their data
app.post('/shopify/customers/data_request', async (req, res) => {
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { customer, shop_domain } = req.body;
console.log(`Data request from ${customer.email} for shop ${shop_domain}`);
// Return all stored customer data
// You have 30 days to respond
res.json({ received: true });
});
// Mandatory webhook: Erase customer data (GDPR right to be forgotten)
app.post('/shopify/customers/redact', async (req, res) => {
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { customer, shop_domain } = req.body;
const conn = await Datastore.open();
// Delete all customer data from your database
await conn.removeMany('customers', { email: customer.email });
await conn.removeMany('orders', { customerId: customer.id });
console.log(`Erased data for customer ${customer.id}`);
res.json({ received: true });
});
// Mandatory webhook: Erase shop data when app is uninstalled
app.post('/shopify/shop/redact', async (req, res) => {
if (!verify('shopify', req.rawBody, req.headers, process.env.SHOPIFY_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { shop_domain } = req.body;
const conn = await Datastore.open();
// Delete all data for this shop
await conn.removeMany('shop_data', { shop: shop_domain });
console.log(`Erased all data for shop ${shop_domain}`);
res.json({ received: true });
});
export default app.init();
```
| Mandatory Topic | When It Fires | Your Responsibility |
|-----------------|---------------|---------------------|
| `customers/data_request` | Customer requests their data | Return all stored data within 30 days |
| `customers/redact` | Customer requests deletion | Delete all personal data |
| `shop/redact` | 48 hours after app uninstall | Delete all shop data |
| `app/uninstalled` | App is uninstalled | Clean up, revoke access |
:::warning Required for App Store
Failure to implement these mandatory webhooks will result in your app being rejected from the Shopify App Store.
:::
## Why Use Codehooks.io for Shopify Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours/Days | 5 minutes |
| HMAC verification | Manual implementation | Easy with `webhook-verify` + `req.rawBody` |
| Scaling | Configure infrastructure | Automatic |
| Database for dedup | Set up separately | Built-in |
| Templates | Build from scratch | Ready-to-use |
| Cost | Server costs | Free tier available |
## Related Resources
- [webhook-verify on npm](https://www.npmjs.com/package/webhook-verify) - Unified webhook signature verification for 20+ providers
- [Shopify Webhook Template](https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-shopify-minimal) - Ready-to-deploy template
- [All Codehooks Templates](https://github.com/RestDB/codehooks-io-templates) - Browse all webhook templates
- [Shopify Webhooks Documentation](https://shopify.dev/docs/apps/build/webhooks)
- [Shopify Webhook Topics Reference](https://shopify.dev/docs/api/webhooks)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Similar payment webhook handler
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## Slack Webhooks Integration Example: Build & Deploy a Bot in Minutes
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# Slack Webhooks Integration Example: Build & Deploy a Bot in Minutes
Slack bots use webhooks to receive messages, handle slash commands, and send real-time notifications. Whether you need a simple alert bot or an AI-powered assistant with memory, this guide shows you how to build and deploy one in minutes.
**What is a Slack bot?** A Slack bot is an automated program that interacts with users in a Slack workspace - responding to messages, executing commands, and posting notifications via webhook endpoints.
**The problem?** Building a Slack bot requires setting up servers, handling OAuth, managing webhook endpoints, and dealing with Slack's API complexity.
**The solution?** Deploy a production-ready Slack bot with Codehooks.io templates in under 5 minutes.
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
## Quick Deploy with Template
The fastest way to create a Slack bot is using our ready-made template:
```bash
coho create myslackbot --template slack-memory-bot
```
You'll see output like:
```
Creating project: myslackbot
Project created successfully!
Your API endpoints:
https://myslackbot-a1b2.api.codehooks.io/dev
```
Then install, deploy, and configure:
```bash
cd myslackbot
npm install
coho deploy
coho set-env SLACK_BOT_TOKEN xoxb-your-token --encrypted
coho set-env SLACK_SIGNING_SECRET your-signing-secret --encrypted
```
```
Project: myslackbot-a1b2 Space: dev
Deployed Codehook successfully! 🙌
```
:::tip Finding your API endpoint
Run `coho info` to see your API endpoints and tokens.
:::
This template includes:
- Slash command handling
- Event subscriptions
- Conversation memory (stores context)
- AI integration ready
- Database storage
:::tip Browse Templates
We have multiple Slack templates available:
- `slack-memory-bot` - AI bot with conversation memory
- `webhook-slack-minimal` - Simple webhook handler with signature verification
Browse all templates in the [templates repository](https://github.com/RestDB/codehooks-io-templates).
:::
## Understanding Slack Integration Types
### 1. Incoming Webhooks (Send messages TO Slack)
Send notifications from your app to a Slack channel:
```javascript
import { app } from 'codehooks-js';
// Your app triggers this to send a Slack notification
app.post('/notify-slack', async (req, res) => {
const { channel, message } = req.body;
// Send to Slack incoming webhook
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: message,
channel: channel
})
});
res.json({ success: true });
});
export default app.init();
```
### 2. Slash Commands (Users trigger your bot)
Handle `/commands` that users type in Slack:
:::info Automatic Form Parsing
Slack sends slash command data as `application/x-www-form-urlencoded` (not JSON). Codehooks automatically parses this into `req.body`, so you can access fields like `command`, `text`, and `user_id` directly.
:::
```javascript
import { app } from 'codehooks-js';
// Slack sends POST request when user types /mycommand
app.post('/slack/commands', async (req, res) => {
const { command, text, user_name, channel_name } = req.body;
// Verify the request is from Slack
if (!verifySlackRequest(req)) {
return res.status(401).json({ error: 'Unauthorized' });
}
switch (command) {
case '/weather':
const weather = await getWeather(text);
return res.json({
response_type: 'in_channel',
text: `Weather for ${text}: ${weather}`
});
case '/help':
return res.json({
response_type: 'ephemeral', // Only visible to user
text: 'Available commands: /weather [city], /help'
});
default:
return res.json({ text: `Unknown command: ${command}` });
}
});
export default app.init();
```
### 3. Event Subscriptions (React to Slack events)
Respond to messages, reactions, channel events:
```javascript
import { app } from 'codehooks-js';
import { Datastore } from 'codehooks-js';
// Slack sends events to this endpoint
app.post('/slack/events', async (req, res) => {
const { type, event, challenge } = req.body;
// Handle Slack URL verification
if (type === 'url_verification') {
return res.json({ challenge });
}
// Handle events
if (type === 'event_callback') {
switch (event.type) {
case 'message':
// Don't respond to bot messages (avoid loops)
if (event.bot_id) break;
await handleMessage(event);
break;
case 'app_mention':
// Bot was @mentioned
await handleMention(event);
break;
case 'reaction_added':
await handleReaction(event);
break;
}
}
res.json({ ok: true });
});
async function handleMessage(event) {
const conn = await Datastore.open();
// Store message for context/memory
await conn.insertOne('slack_messages', {
user: event.user,
channel: event.channel,
text: event.text,
ts: event.ts,
createdAt: new Date().toISOString()
});
// Respond if message contains trigger word
if (event.text.toLowerCase().includes('hello bot')) {
await postSlackMessage(event.channel, `Hello <@${event.user}>!`);
}
}
async function postSlackMessage(channel, text) {
await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SLACK_BOT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ channel, text })
});
}
export default app.init();
```
## Building an AI Slack Bot with Memory
Create a bot that remembers conversation context:
```javascript
import { app } from 'codehooks-js';
import { Datastore } from 'codehooks-js';
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
app.post('/slack/events', async (req, res) => {
const { type, event, challenge } = req.body;
if (type === 'url_verification') {
return res.json({ challenge });
}
if (event?.type === 'app_mention' && !event.bot_id) {
// Process asynchronously to respond quickly
res.json({ ok: true });
await handleAIResponse(event);
return;
}
res.json({ ok: true });
});
async function handleAIResponse(event) {
const conn = await Datastore.open();
const userId = event.user;
const userMessage = event.text.replace(/<@[^>]+>/g, '').trim();
// Get conversation history for this user
const history = await conn.getMany('conversations', { userId }, {
sort: { createdAt: -1 },
limit: 10
}).toArray();
// Build messages array for OpenAI
const messages = [
{ role: 'system', content: 'You are a helpful Slack assistant.' },
...history.reverse().flatMap(h => [
{ role: 'user', content: h.userMessage },
{ role: 'assistant', content: h.botResponse }
]),
{ role: 'user', content: userMessage }
];
// Call OpenAI
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: 'gpt-4',
messages,
max_tokens: 500
})
});
const data = await response.json();
const botResponse = data.choices[0].message.content;
// Store conversation for memory
await conn.insertOne('conversations', {
userId,
userMessage,
botResponse,
channel: event.channel,
createdAt: new Date().toISOString()
});
// Post response to Slack
await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Authorization': `Bearer ${SLACK_BOT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel: event.channel,
text: botResponse,
thread_ts: event.ts // Reply in thread
})
});
}
export default app.init();
```
## Setting Up Your Slack App
### 1. Create a Slack App
1. Go to [api.slack.com/apps](https://api.slack.com/apps)
2. Click **Create New App** → **From scratch**
3. Enter app name and select workspace
### 2. Configure OAuth & Permissions
Add these Bot Token Scopes:
- `chat:write` - Send messages
- `commands` - Handle slash commands
- `app_mentions:read` - Detect @mentions
- `channels:history` - Read channel messages
- `im:history` - Read DMs
### 3. Set Up Event Subscriptions
1. Enable **Event Subscriptions**
2. Enter Request URL: `https://your-app.api.codehooks.io/dev/slack/events`
3. Subscribe to bot events:
- `message.channels`
- `message.im`
- `app_mention`
### 4. Create Slash Commands
1. Go to **Slash Commands**
2. Click **Create New Command**
3. Enter command (e.g., `/mycommand`)
4. Request URL: `https://your-app.api.codehooks.io/dev/slack/commands`
### 5. Install to Workspace
1. Go to **Install App**
2. Click **Install to Workspace**
3. Copy the **Bot User OAuth Token**
### 6. Set Environment Variables
```bash
coho set-env SLACK_BOT_TOKEN xoxb-your-token --encrypted
coho set-env SLACK_SIGNING_SECRET your-signing-secret --encrypted
coho set-env OPENAI_API_KEY sk-your-key --encrypted # If using AI
```
## Sending Rich Messages (Block Kit)
Create interactive, formatted messages:
```javascript
async function sendRichMessage(channel, orderData) {
await fetch('https://slack.com/api/chat.postMessage', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SLACK_BOT_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel,
blocks: [
{
type: 'header',
text: { type: 'plain_text', text: 'New Order Received!' }
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Order ID:*\n${orderData.id}` },
{ type: 'mrkdwn', text: `*Total:*\n$${orderData.total}` },
{ type: 'mrkdwn', text: `*Customer:*\n${orderData.customer}` },
{ type: 'mrkdwn', text: `*Status:*\n${orderData.status}` }
]
},
{
type: 'actions',
elements: [
{
type: 'button',
text: { type: 'plain_text', text: 'View Order' },
url: `https://yourapp.com/orders/${orderData.id}`
},
{
type: 'button',
text: { type: 'plain_text', text: 'Mark Shipped' },
action_id: 'ship_order',
value: orderData.id
}
]
}
]
})
});
}
```
## Verifying Slack Requests
Always verify that requests come from Slack:
```javascript
import crypto from 'crypto';
function verifySlackRequest(req) {
const signingSecret = process.env.SLACK_SIGNING_SECRET;
const timestamp = req.headers['x-slack-request-timestamp'];
const signature = req.headers['x-slack-signature'];
// Check timestamp is recent (prevent replay attacks)
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5;
if (timestamp < fiveMinutesAgo) {
return false;
}
// Create signature base string
const sigBasestring = `v0:${timestamp}:${req.rawBody}`;
// Calculate expected signature
const mySignature = 'v0=' + crypto
.createHmac('sha256', signingSecret)
.update(sigBasestring)
.digest('hex');
// Compare signatures
return crypto.timingSafeEqual(
Buffer.from(mySignature),
Buffer.from(signature)
);
}
```
## Common Use Cases
### Notification Bot
Send alerts from your app to Slack channels:
- Deployment notifications
- Error alerts
- New user signups
- Order notifications
### Support Bot
Handle customer inquiries:
- Ticket creation via slash commands
- Auto-responses to common questions
- Escalation to human agents
### DevOps Bot
Automate development workflows:
- CI/CD status updates
- Pull request notifications
- Incident management
### AI Assistant
Intelligent conversational bot:
- Answer questions with context
- Summarize documents
- Generate content
## Why Use Codehooks.io for Slack Bots?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours/Days | 5 minutes |
| Server management | Configure & maintain | Serverless |
| Database for memory | Set up separately | Built-in |
| Scaling | Manual configuration | Automatic |
| Templates | Build from scratch | Ready-to-use |
| Cost | Server costs | Free tier available |
## Related Resources
- [Slack Bot Templates](https://github.com/RestDB/codehooks-io-templates) - Ready-to-deploy templates
- [Slack API Documentation](https://api.slack.com/docs)
- [Block Kit Builder](https://app.slack.com/block-kit-builder) - Design rich messages
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [Discord Bot Webhooks Example](/docs/examples/webhooks/discord) - Another chat platform integration
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Payment webhook handler
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## Stripe Webhooks Integration Example: Handle Payments with Signature Verification
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# Stripe Webhooks Integration Example: Handle Payments with Signature Verification
A Stripe webhooks integration lets your application receive real-time notifications when events happen in your Stripe account — payments succeed, subscriptions renew, disputes are opened, and more. Instead of polling the Stripe API, webhooks push real-time event data to your endpoint as a JSON payload.

**The problem?** Setting up Stripe webhooks correctly takes time. You need to verify signatures, handle retries, manage event ordering, and ensure reliability. Building custom retry logic alone can take days.
**The solution?** Stop building custom retry logic—Codehooks handles webhook reliability at the platform level. Deploy a production-ready Stripe webhook handler in under 5 minutes using our ready-made template, with automatic retries and signature verification built in.
:::info New to webhooks?
If you're new to webhooks, check out our guide on [What are webhooks?](/docs/examples/webhooks/what-are-webhooks) to understand the fundamentals before diving into this Stripe-specific implementation.
:::
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
## Quick Deploy with Template
The fastest way to get started is using the Codehooks Stripe webhook template:
```bash
coho create mystripehandler --template webhook-stripe-minimal
```
You'll see output like:
```
Creating project: mystripehandler
Project created successfully!
Your API endpoints:
https://mystripehandler-a1b2.api.codehooks.io/dev
```
Then install, deploy, and configure:
```bash
cd mystripehandler
npm install
coho deploy
coho set-env STRIPE_SECRET_KEY sk_xxxxx --encrypted
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encrypted
```
That's it! The template includes signature verification, event handling, and database storage out of the box.
:::tip Use the Template
The `webhook-stripe-minimal` template is production-ready with proper signature verification, error handling, and event storage. Browse all available templates in the [templates repository](https://github.com/RestDB/codehooks-io-templates).
:::
## Custom Implementation
If you prefer to build from scratch or customize further, create a new project and add your Stripe webhook handler code to `index.js`:
```javascript
import { app } from 'codehooks-js';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Stripe webhook endpoint with signature verification
app.post('/stripe/webhooks', async (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
// Verify the webhook signature
event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
endpointSecret
);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
return res.status(400).json({ error: 'Invalid signature' });
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('Payment succeeded:', paymentIntent.id);
// Update your database, send confirmation email, etc.
break;
case 'invoice.paid':
const invoice = event.data.object;
console.log('Invoice paid:', invoice.id);
// Extend subscription, update records
break;
case 'customer.subscription.created':
const subscription = event.data.object;
console.log('New subscription:', subscription.id);
// Provision access, welcome email
break;
case 'customer.subscription.deleted':
const canceledSub = event.data.object;
console.log('Subscription canceled:', canceledSub.id);
// Revoke access, send win-back email
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.json({ received: true });
});
export default app.init();
```
Deploy:
```bash
coho deploy
```
```
Project: mystripehandler-a1b2 Space: dev
Deployed Codehook successfully! 🙌
```
:::tip Finding your API endpoint
Run `coho info` to see your API endpoints and tokens. Your webhook URL will be:
`https://mystripehandler-a1b2.api.codehooks.io/dev/stripe/webhooks`
:::
## Configure Stripe Dashboard
1. Go to [Stripe Dashboard → Webhooks](https://dashboard.stripe.com/webhooks)
2. Click **Add endpoint**
3. Enter your Codehooks URL: `https://your-app.api.codehooks.io/dev/stripe/webhooks`
4. Select events to listen for (or choose "All events")
5. Copy the **Signing secret** and add it to your Codehooks secrets:
```bash
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encrypted
coho set-env STRIPE_SECRET_KEY sk_xxxxx --encrypted
```
## Finding Your STRIPE_WEBHOOK_SECRET
Your Stripe webhook signing secret (starting with `whsec_`) is required for signature verification. Here's how to find it:
1. Go to [Stripe Dashboard → Developers → Webhooks](https://dashboard.stripe.com/webhooks)
2. Click on your webhook endpoint (or create one if you haven't already)
3. In the endpoint details, find **Signing secret**
4. Click **Reveal** to show the secret (it starts with `whsec_`)
5. Copy the secret and add it to your Codehooks environment:
```bash
coho set-env STRIPE_WEBHOOK_SECRET whsec_your_secret_here --encrypted
```
:::warning Keep Your Secret Secure
Never commit your `STRIPE_WEBHOOK_SECRET` to version control. The `--encrypted` flag ensures it's stored securely in Codehooks. Each webhook endpoint has its own unique signing secret—test and live mode endpoints have different secrets.
:::
## invoice.paid vs checkout.session.completed
One of the most common questions when building Stripe integrations is which event to listen for: `invoice.paid` or `checkout.session.completed`? Here's when to use each:
### checkout.session.completed
Use this event for **one-time payments** and **initial Checkout sessions**:
- Triggers when a customer completes Stripe Checkout
- Best for one-time purchases, donations, or initial subscription signups
- Contains the full Checkout Session object with customer and payment details
```javascript
case 'checkout.session.completed':
const session = event.data.object;
if (session.mode === 'payment') {
// One-time payment - fulfill the order
await fulfillOrder(session);
} else if (session.mode === 'subscription') {
// Initial subscription - provision access
await provisionSubscription(session);
}
break;
```
### invoice.paid
Use this event for **subscriptions** and **recurring payments**:
- Triggers every time an invoice is successfully paid
- Covers both initial subscription payments AND renewals
- Essential for SaaS apps with recurring billing
```javascript
case 'invoice.paid':
const invoice = event.data.object;
// Extend or renew subscription access
await extendSubscriptionAccess(invoice.subscription, invoice.customer);
break;
```
### Recommendation for SaaS Apps
For most SaaS applications, **listen to `invoice.paid`** as your primary event. It fires for:
- Initial subscription payments
- Monthly/yearly renewals
- Plan upgrades that generate prorated invoices
- Manual invoice payments
If you need to capture the exact moment a customer completes checkout (for analytics, welcome emails, etc.), also listen to `checkout.session.completed`.
**Typical SaaS setup:**
- `checkout.session.completed` → Send welcome email, track conversion
- `invoice.paid` → Extend subscription access, update billing records
- `customer.subscription.deleted` → Revoke access
## Common Stripe Webhook Events
| Event | Description | Common Use Case |
|-------|-------------|-----------------|
| `payment_intent.succeeded` | Payment completed successfully | Update order status, send receipt |
| `payment_intent.payment_failed` | Payment attempt failed | Notify customer, retry logic |
| `invoice.paid` | Invoice payment succeeded | Extend subscription access |
| `invoice.payment_failed` | Invoice payment failed | Send dunning email |
| `customer.subscription.created` | New subscription started | Provision access |
| `customer.subscription.updated` | Subscription changed | Update plan limits |
| `customer.subscription.deleted` | Subscription canceled | Revoke access |
| `checkout.session.completed` | Checkout completed | Fulfill order |
| `charge.dispute.created` | Dispute opened | Alert team, gather evidence |
## Event Payload Types: Snapshot vs Thin Events
Stripe offers two event payload formats:
- **Snapshot events** (default): Include a full copy of the affected object at the time of the event. This is the traditional format most integrations use.
- **Thin events**: Include only the event type and object ID. Your handler must fetch the current object state via the Stripe API.
For most use cases, snapshot events are recommended as they provide all the data you need without additional API calls. Use thin events when you always need the latest state or want to reduce payload size.
## Stripe Delivery and Retry Behavior
Stripe attempts to deliver webhooks for up to 3 days with exponential backoff:
- If your endpoint returns a non-2xx response, Stripe retries
- Events may arrive out of order (use `created` timestamp for ordering)
- The same event may be delivered multiple times (use `event.id` for idempotency)
## Stripe Webhook Signature Verification
Stripe signs all webhook payloads with a signature header (`Stripe-Signature`). Always verify this signature to ensure the webhook came from Stripe:
```javascript
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
function verifyStripeWebhook(req) {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
try {
return stripe.webhooks.constructEvent(
req.rawBody, // Raw request body (not parsed JSON)
sig,
endpointSecret
);
} catch (err) {
throw new Error(`Webhook Error: ${err.message}`);
}
}
```
**Important:** Use `req.rawBody` (the raw string), not `req.body` (parsed JSON). Signature verification requires the exact bytes Stripe sent.
## Monitoring Stripe Webhooks
Effective monitoring helps you catch issues before they impact customers. Here's how to monitor your Stripe webhooks:
### Stripe Dashboard Monitoring
The Stripe Dashboard provides built-in webhook monitoring:
1. Go to [Stripe Dashboard → Developers → Webhooks](https://dashboard.stripe.com/webhooks)
2. Click on your endpoint to see:
- **Delivery success rate** — percentage of successfully delivered webhooks
- **Recent deliveries** — list of recent webhook attempts with status codes
- **Failed events** — webhooks that failed and their retry status
Failed webhooks show the HTTP status code and response body, making debugging straightforward.
### Real-Time Logs with Codehooks
Codehooks automatically logs all incoming webhooks. Monitor them in real-time:
```bash
coho logs --follow
```
This streams all webhook activity, including:
- Incoming requests with timestamps
- Your application's console.log output
- Errors and stack traces
For a specific number of recent log entries:
```bash
coho logs --tail 50
```
### What to Monitor
Key metrics to watch:
- **Signature verification failures** — may indicate misconfigured secrets or attack attempts
- **Processing errors** — exceptions in your event handling code
- **Response times** — ensure you return 200 within 20 seconds
- **Event types** — unexpected events may indicate Stripe configuration changes
## Best Practices for Stripe Webhooks
### 1. Return 200 Quickly
Stripe expects a 2xx response within 20 seconds. Store the event first, then return immediately. Process complex logic later:
```javascript
import { app, Datastore } from 'codehooks-js';
app.post('/stripe/webhooks', async (req, res) => {
// Verify signature first
const event = verifyStripeWebhook(req);
const conn = await Datastore.open();
// Check if we already have this event (idempotency)
try {
await conn.getOne('stripe_events', event.id);
return res.json({ received: true }); // Already exists
} catch (e) {
// Not found, continue to store
}
// Store new event
await conn.insertOne('stripe_events', {
_id: event.id,
type: event.type,
rawBody: req.rawBody, // Store raw body for replay capability
created: event.created,
processedAt: null
});
res.json({ received: true });
});
```
### 2. Handle Duplicate Events
Stripe may send the same event multiple times. Use idempotency:
```javascript
import { Datastore } from 'codehooks-js';
async function handleStripeEvent(event) {
const conn = await Datastore.open();
// Check if we've already processed this event
try {
await conn.getOne('processed_events', event.id);
console.log('Event already processed:', event.id);
return;
} catch (e) {
// Not found, continue to process
}
// Process the event
await processEvent(event);
// Mark as processed
await conn.insertOne('processed_events', {
_id: event.id,
type: event.type,
processedAt: new Date().toISOString()
});
}
```
### 3. Handle Event Ordering
Events may arrive out of order. Check timestamps or use `created` field:
```javascript
case 'customer.subscription.updated':
const conn = await Datastore.open();
const subscription = event.data.object;
let existingRecord = null;
try {
existingRecord = await conn.getOne('subscriptions', subscription.id);
} catch (e) {
// Not found, will create new record
}
// Only update if this event is newer
if (!existingRecord || event.created > existingRecord.lastEventTime) {
await conn.updateOne('subscriptions', subscription.id, {
$set: {
status: subscription.status,
lastEventTime: event.created
}
});
}
break;
```
## Why Use Codehooks.io for Stripe Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours/Days | 5 minutes |
| Signature verification | Manual implementation | Built-in support |
| Scaling | Configure infrastructure | Automatic |
| Retries | Build retry logic | Platform handles |
| Monitoring | Set up logging | Built-in logs |
| Cost | Server costs + maintenance | Free tier available |
## Related Resources
- [Stripe Webhook Template](https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-stripe-minimal) - Ready-to-deploy template
- [All Codehooks Templates](https://github.com/RestDB/codehooks-io-templates) - Browse all webhook templates
- [Stripe Webhooks Documentation](https://docs.stripe.com/webhooks)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Secure Zapier/Make/n8n/IFTTT Webhooks](/blog/secure-zapier-make-n8n-webhooks-signature-verification) - Forward verified Stripe events to automation platforms
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [PayPal Webhooks Example](/docs/examples/webhooks/paypal) - Alternative payment provider
- [Shopify Webhooks Example](/docs/examples/webhooks/shopify) - E-commerce webhook handler
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
---
## Twilio Webhooks Integration Example: Handle SMS & Voice Events
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# Twilio Webhooks Integration Example: Handle SMS & Voice Events
Twilio webhooks notify your application when communication events occur — incoming SMS messages, voice calls, delivery status updates, and more. Instead of polling the Twilio API, webhooks push real-time event data to your endpoint.
**The problem?** Setting up Twilio webhooks requires signature verification, TwiML response formatting, handling the `application/x-www-form-urlencoded` content type, and responding within 15 seconds.
**The solution?** Deploy a production-ready Twilio webhook handler with Codehooks.io in under 5 minutes.
:::info New to webhooks?
If you're new to webhooks, check out our guide on [What are webhooks?](/docs/examples/webhooks/what-are-webhooks) to understand the fundamentals before diving into this Twilio-specific implementation.
:::
## Prerequisites
Before you begin, [sign up for a free Codehooks.io account](https://account.codehooks.io/login) and install the CLI:
```bash
npm install -g codehooks
```
You'll also need:
- A [Twilio account](https://www.twilio.com/try-twilio) with a phone number
- Your Twilio Account SID and Auth Token (found in the [Twilio Console](https://console.twilio.com/))
## Quick Start: Incoming SMS Handler
Create a new Codehooks project:
```bash
coho create my-twilio-handler
cd my-twilio-handler
npm install twilio
```
Replace `index.js` with:
```javascript
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
// Bypass JWT auth for Twilio webhook endpoints
app.auth('/twilio/*', (req, res, next) => {
next();
});
// Handle incoming SMS messages
app.post('/twilio/sms', async (req, res) => {
// Validate the request is from Twilio
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const isValid = validateRequest(authToken, signature, url, req.body);
if (!isValid) {
console.error('Invalid Twilio signature');
return res.status(403).send('Forbidden');
}
// Extract message details from the webhook
const { From, To, Body, MessageSid } = req.body;
console.log(`SMS from ${From}: ${Body}`);
// Store the message in the database
const conn = await Datastore.open();
await conn.insertOne('sms_messages', {
messageSid: MessageSid,
from: From,
to: To,
body: Body,
direction: 'inbound',
receivedAt: new Date().toISOString(),
});
// Respond with TwiML
res.set('Content-Type', 'text/xml');
res.send(`
Thanks for your message! We received: "${Body}"
`);
});
export default app.init();
```
Deploy and configure:
```bash
coho deploy
coho set-env TWILIO_AUTH_TOKEN your_auth_token_here --encrypted
```
Your webhook URL will be: `https://my-twilio-handler-xxxx.api.codehooks.io/dev/twilio/sms`
## Configure Twilio Console
1. Go to [Twilio Console → Phone Numbers](https://console.twilio.com/us1/develop/phone-numbers/manage/incoming)
2. Click on your phone number
3. Under **Messaging**, set:
- **A MESSAGE COMES IN**: Webhook
- **URL**: `https://your-app.api.codehooks.io/dev/twilio/sms`
- **HTTP Method**: POST
4. Click **Save**
## Common Twilio Webhook Events
### Incoming SMS Parameters
When Twilio receives an SMS to your number, it sends these parameters:
| Parameter | Description | Example |
|-----------|-------------|---------|
| `MessageSid` | Unique message identifier | `SM1234567890abcdef` |
| `From` | Sender's phone number | `+15551234567` |
| `To` | Your Twilio number | `+15559876543` |
| `Body` | Message text content | `Hello world` |
| `NumMedia` | Number of attached media files | `0` |
| `FromCity` | Sender's city (if available) | `San Francisco` |
| `FromState` | Sender's state | `CA` |
| `FromCountry` | Sender's country | `US` |
### SMS Status Callback Parameters
When you send an SMS via Twilio API with a `statusCallback` URL, you receive updates:
| Parameter | Description |
|-----------|-------------|
| `MessageSid` | Unique message identifier |
| `MessageStatus` | Current status: `queued`, `sent`, `delivered`, `failed`, `undelivered` |
| `ErrorCode` | Error code if failed (e.g., `30003` for unreachable) |
| `ErrorMessage` | Human-readable error description |
### Voice Call Parameters
When someone calls your Twilio number:
| Parameter | Description |
|-----------|-------------|
| `CallSid` | Unique call identifier |
| `From` | Caller's phone number |
| `To` | Your Twilio number |
| `CallStatus` | Status: `ringing`, `in-progress`, `completed`, `failed` |
| `Direction` | `inbound` or `outbound-api` |
## SMS Status Callback Handler
Track delivery status of outbound messages:
```javascript
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
app.auth('/twilio/*', (req, res, next) => {
next();
});
// Handle SMS delivery status updates
app.post('/twilio/status', async (req, res) => {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const isValid = validateRequest(authToken, signature, url, req.body);
if (!isValid) {
return res.status(403).send('Forbidden');
}
const { MessageSid, MessageStatus, ErrorCode, ErrorMessage } = req.body;
console.log(`Message ${MessageSid} status: ${MessageStatus}`);
// Update message status in database
const conn = await Datastore.open();
await conn.updateOne('sms_messages', { messageSid: MessageSid }, {
$set: {
status: MessageStatus,
errorCode: ErrorCode || null,
errorMessage: ErrorMessage || null,
updatedAt: new Date().toISOString(),
}
});
// Handle specific statuses
if (MessageStatus === 'failed' || MessageStatus === 'undelivered') {
console.error(`Message ${MessageSid} failed: ${ErrorCode} - ${ErrorMessage}`);
// Trigger retry logic or alert
}
res.status(200).send('OK');
});
export default app.init();
```
## Voice Call Handler with TwiML
Handle incoming voice calls:
```javascript
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
app.auth('/twilio/*', (req, res, next) => {
next();
});
// Handle incoming voice calls
app.post('/twilio/voice', async (req, res) => {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const isValid = validateRequest(authToken, signature, url, req.body);
if (!isValid) {
return res.status(403).send('Forbidden');
}
const { CallSid, From, To, CallStatus } = req.body;
console.log(`Incoming call from ${From}`);
// Log the call
const conn = await Datastore.open();
await conn.insertOne('calls', {
callSid: CallSid,
from: From,
to: To,
status: CallStatus,
direction: 'inbound',
receivedAt: new Date().toISOString(),
});
// Respond with TwiML for voice
res.set('Content-Type', 'text/xml');
res.send(`
Hello! Thank you for calling. Please leave a message after the beep.We did not receive a recording. Goodbye.
`);
});
export default app.init();
```
## Complete Multi-Event Handler
Handle all Twilio webhook types in one deployment:
```javascript
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
// Middleware to validate all Twilio requests
function validateTwilioRequest(req, res, next) {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
const isValid = validateRequest(authToken, signature, url, req.body);
if (!isValid) {
console.error('Invalid Twilio signature');
return res.status(403).send('Forbidden');
}
next();
}
// Bypass JWT for all Twilio endpoints
app.auth('/twilio/*', (req, res, next) => {
next();
});
// Incoming SMS
app.post('/twilio/sms', validateTwilioRequest, async (req, res) => {
const { From, Body, MessageSid } = req.body;
const conn = await Datastore.open();
await conn.insertOne('events', {
type: 'sms.received',
messageSid: MessageSid,
from: From,
body: Body,
timestamp: new Date().toISOString(),
});
// Auto-reply
res.set('Content-Type', 'text/xml');
res.send(`
Thanks! We got your message.
`);
});
// SMS Status Callback
app.post('/twilio/sms/status', validateTwilioRequest, async (req, res) => {
const { MessageSid, MessageStatus, ErrorCode } = req.body;
const conn = await Datastore.open();
await conn.insertOne('events', {
type: 'sms.status',
messageSid: MessageSid,
status: MessageStatus,
errorCode: ErrorCode,
timestamp: new Date().toISOString(),
});
res.status(200).send('OK');
});
// Incoming Voice Call
app.post('/twilio/voice', validateTwilioRequest, async (req, res) => {
const { CallSid, From, To } = req.body;
const conn = await Datastore.open();
await conn.insertOne('events', {
type: 'call.received',
callSid: CallSid,
from: From,
to: To,
timestamp: new Date().toISOString(),
});
res.set('Content-Type', 'text/xml');
res.send(`
Hello, thank you for calling!
`);
});
// Voice Status Callback
app.post('/twilio/voice/status', validateTwilioRequest, async (req, res) => {
const { CallSid, CallStatus, CallDuration } = req.body;
const conn = await Datastore.open();
await conn.insertOne('events', {
type: 'call.status',
callSid: CallSid,
status: CallStatus,
duration: CallDuration,
timestamp: new Date().toISOString(),
});
res.status(200).send('OK');
});
export default app.init();
```
## Twilio Signature Verification
Twilio signs all webhook requests with the `X-Twilio-Signature` header using HMAC-SHA1 and your Auth Token. Always verify this signature to ensure requests are legitimately from Twilio.
```javascript
import twilio from 'twilio';
const { validateRequest } = twilio;
function verifyTwilioWebhook(req) {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
// IMPORTANT: Use the exact URL Twilio is calling
// Include the full URL with https:// and any query parameters
const url = `https://${req.headers.host}${req.originalUrl}`;
return validateRequest(authToken, signature, url, req.body);
}
```
**Important:** Always use Twilio's official SDK for signature validation rather than implementing your own. The SDK handles edge cases like URL encoding and parameter ordering.
## Twilio Webhook Timeouts
Twilio enforces a **15-second timeout**. If your endpoint doesn't respond in time, Twilio marks the delivery as failed and may retry.
**Best practice:** Acknowledge immediately, process asynchronously:
```javascript
import { app, Datastore } from 'codehooks-js';
import twilio from 'twilio';
const { validateRequest } = twilio;
// Define a worker for async processing
app.worker('processSms', async (req, res) => {
const { from, body, messageSid } = req.body.payload;
// Do heavy processing here (AI analysis, external API calls, etc.)
console.log(`Processing message ${messageSid} from ${from}`);
// Update database with results
const conn = await Datastore.open();
await conn.updateOne('sms_messages', { messageSid }, {
$set: {
processed: true,
processedAt: new Date().toISOString(),
}
});
res.end();
});
app.auth('/twilio/*', (req, res, next) => {
next();
});
app.post('/twilio/sms', async (req, res) => {
const authToken = process.env.TWILIO_AUTH_TOKEN;
const signature = req.headers['x-twilio-signature'];
const url = `https://${req.headers.host}${req.originalUrl}`;
if (!validateRequest(authToken, signature, url, req.body)) {
return res.status(403).send('Forbidden');
}
const { From, Body, MessageSid } = req.body;
// Store immediately
const conn = await Datastore.open();
await conn.insertOne('sms_messages', {
messageSid: MessageSid,
from: From,
body: Body,
processed: false,
receivedAt: new Date().toISOString(),
});
// Queue for async processing
await conn.enqueue('processSms', {
from: From,
body: Body,
messageSid: MessageSid,
});
// Respond immediately with TwiML (within milliseconds)
res.set('Content-Type', 'text/xml');
res.send(`
Got it! Processing your request...
`);
});
export default app.init();
```
## Best Practices for Twilio Webhooks
### 1. Always Validate Signatures
Never skip signature validation in production. Attackers can send fake webhooks to your endpoint.
### 2. Use Environment Variables
```bash
coho set-env TWILIO_AUTH_TOKEN ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx --encrypted
coho set-env TWILIO_ACCOUNT_SID your_account_sid --encrypted
```
### 3. Handle Idempotency
Twilio may retry failed webhooks. Use `MessageSid` or `CallSid` as idempotency keys:
```javascript
const conn = await Datastore.open();
try {
await conn.getOne('sms_messages', { messageSid: MessageSid });
console.log('Already processed:', MessageSid);
res.status(200).send('OK');
return;
} catch (e) {
// Not found, continue to process
}
```
### 4. Return Proper Status Codes
- `200-299`: Success, Twilio won't retry
- `4xx/5xx`: Twilio will retry with exponential backoff
### 5. Log Everything
```javascript
console.log('Twilio webhook received:', {
type: 'sms',
from: From,
messageSid: MessageSid,
timestamp: new Date().toISOString(),
});
```
Check logs with: `coho logs --follow`
## Why Use Codehooks.io for Twilio Webhooks?
| Feature | DIY Setup | Codehooks.io |
|---------|-----------|--------------|
| Setup time | Hours | 5 minutes |
| SSL/HTTPS | Configure certificates | Built-in |
| Signature validation | Manual implementation | Use Twilio SDK |
| Database for logs | Set up separately | Built-in |
| Async processing | Configure queues | Built-in workers |
| Monitoring | Set up logging | Built-in logs |
| Cost | Server costs | Free tier available |
## Related Resources
- [Twilio Webhooks Documentation](https://www.twilio.com/docs/usage/webhooks)
- [Twilio Webhook Security](https://www.twilio.com/docs/usage/webhooks/webhooks-security)
- [TwiML Reference](https://www.twilio.com/docs/messaging/twiml)
- [Codehooks.io Quick Start](/docs/quickstart-cli)
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build your own outgoing webhook system
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Learn webhook fundamentals
- [Stripe Webhooks Example](/docs/examples/webhooks/stripe) - Payment webhook handler
- [Slack Webhooks Example](/docs/examples/webhooks/slack) - Another messaging integration
- [Browse All Webhook Examples](/docs/examples/webhooks) - Explore all webhook integration examples
- [Quick Deploy Templates](/docs/examples/examples-overview) - Ready-to-deploy backend templates
` or ``) because Twilio needs to know how to handle the interaction. Status callbacks are informational - they report what happened (delivered, failed, etc.) and just need a 200 OK response."
},
{
q: "How do I test Twilio webhooks locally?",
a: "With Codehooks.io, you don't need local testing or ngrok. Just run `coho deploy` and you have a live HTTPS URL in seconds. Configure that URL in the Twilio Console and test with real messages. Check logs with `coho logs --follow`."
},
{
q: "Does Twilio retry failed webhooks?",
a: "Yes, Twilio retries failed webhook deliveries with exponential backoff. Return a 2xx status code to indicate success. If you return 4xx or 5xx, or if your endpoint times out, Twilio will retry the request."
},
{
q: "How do I handle MMS messages with media?",
a: "MMS webhooks include `NumMedia` (count of attachments) and `MediaUrl0`, `MediaUrl1`, etc. for each attachment. Download and store media files as needed. The `MediaContentType0` parameter tells you the MIME type of each file."
},
{
q: "What format does Twilio send webhook data in?",
a: "Twilio sends webhooks as `application/x-www-form-urlencoded` (not JSON). Most frameworks parse this automatically into `req.body`. The Codehooks.io runtime handles this parsing for you."
}
]}
/>
---
## What Are Webhooks? The Complete Guide with Examples
*(Note: This content contains MDX/JSX code)*
import FAQSection from '@site/src/components/FAQSection';
# What Are Webhooks? The Complete Guide with Examples
A **webhook** is an automated message sent from one application to another when a specific event occurs. Instead of constantly asking "did anything happen?" (polling), webhooks push data to you in real-time the moment something happens.
Think of it like this: rather than repeatedly calling a store to check if your order shipped, the store calls *you* when it ships. That's the webhook model.
## How Webhooks Work
Webhooks follow a simple pattern:
1. **You provide a URL** — Your application exposes an HTTP endpoint (the "webhook URL") that can receive POST requests
2. **An event occurs** — Something happens in the source application (payment received, form submitted, file uploaded)
3. **Data is pushed to you** — The source application sends an HTTP POST request to your URL with event data as JSON
```mermaid
flowchart LR
subgraph Source["Source Application"]
S[Stripe / GitHub Shopify / Slack]
end
subgraph Your["Your Application"]
E[Webhook Endpoint]
end
S -->|"HTTP POST + JSON"| E
```
Here's what a typical webhook payload looks like (from Stripe):
```json
{
"id": "evt_1234567890",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_abc123",
"amount": 2000,
"currency": "usd",
"customer": "cus_xyz789"
}
},
"created": 1699900000
}
```
Your endpoint receives this data and can immediately take action — update a database, send an email, trigger a workflow.
## Webhooks vs APIs: What's the Difference?
Both webhooks and APIs enable applications to communicate, but they work in opposite directions:
| Aspect | API (Pull) | Webhook (Push) |
|--------|------------|----------------|
| **Direction** | You request data | Data is sent to you |
| **Trigger** | Your application initiates | External event initiates |
| **Timing** | On-demand | Real-time |
| **Efficiency** | May require polling | No wasted requests |
| **Use case** | Fetching data, CRUD operations | Event notifications |
### The Polling Problem
Without webhooks, you'd need to repeatedly call an API to check for updates:
```javascript
// ❌ Polling - inefficient, wasteful
setInterval(async () => {
const orders = await fetch('/api/orders?status=new');
// Check if anything changed...
}, 5000); // Every 5 seconds = 17,280 requests/day
```
With webhooks, you receive data only when something happens:
```javascript
// ✅ Webhook - efficient, real-time
app.post('/webhooks/orders', (req, res) => {
const order = req.body;
// Process immediately when event occurs
res.status(200).send('OK');
});
```
## Real-World Webhook Examples
Webhooks power countless integrations across the web:
### Payment Processing
- **[Stripe](/docs/examples/webhooks/stripe)** sends webhooks when payments succeed, fail, or dispute
- **[PayPal](/docs/examples/webhooks/paypal)** notifies you of completed transactions
- **[Shopify](/docs/examples/webhooks/shopify)** alerts your system about new orders
### Developer Tools
- **[GitHub](/docs/examples/webhooks/github)** triggers webhooks on push, pull request, or issue events
- **GitLab** notifies CI/CD pipelines of code changes
- **[Jira](/docs/examples/webhooks/jira)** sends updates when tickets are created or modified
### Communication
- **[Slack](/docs/examples/webhooks/slack)** sends webhooks for messages, commands, and events
- **[Discord](/docs/examples/webhooks/discord)** notifies bots of user interactions
- **[Twilio](/docs/examples/webhooks/twilio)** pushes incoming SMS and call events
### Email Services
- **[SendGrid](/docs/examples/webhooks/sendgrid)** tracks email delivery, opens, and clicks
- **[Mailgun](/docs/examples/webhooks/mailgun)** sends delivery and engagement events
### AI Platforms
- **[OpenAI](/docs/examples/webhooks/openai)** notifies when Deep Research, batch jobs, or fine-tuning complete
### Business Automation
- **Mailchimp** sends subscriber activity webhooks
- **Typeform** notifies you of form submissions
- **Calendly** alerts you when meetings are scheduled
## Webhook Payload Structure
Most webhooks send JSON data via HTTP POST. A typical payload includes:
```json
{
"event": "order.created",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"id": "order_123",
"customer_email": "customer@example.com",
"total": 99.99,
"items": [
{ "name": "Widget", "quantity": 2, "price": 49.99 }
]
},
"signature": "sha256=abc123..."
}
```
Key components:
- **Event type** — What happened (e.g., `order.created`, `payment.failed`)
- **Timestamp** — When the event occurred
- **Data** — The actual payload with event details
- **Signature** — Cryptographic proof the request is authentic
## Webhook Security Best Practices
Since webhooks are publicly accessible endpoints, security is critical:
### 1. Verify Signatures
Most webhook providers sign requests with HMAC. Always verify:
```javascript
import crypto from 'crypto';
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}
```
### 2. Use HTTPS
Always use HTTPS endpoints. Never accept webhooks over plain HTTP.
### 3. Validate Timestamps
Reject old requests to prevent replay attacks:
```javascript
const timestamp = req.headers['x-webhook-timestamp'];
const fiveMinutesAgo = Date.now() - (5 * 60 * 1000);
if (new Date(timestamp).getTime() < fiveMinutesAgo) {
return res.status(401).send('Request too old');
}
```
### 4. Respond Quickly
Store the event first, then return a 200 status immediately. Process complex logic later:
```javascript
app.post('/webhook', async (req, res) => {
const conn = await Datastore.open();
const eventId = req.body.id; // Most providers include an event ID
// Check if we already have this event (idempotency)
try {
await conn.getOne('webhook_events', eventId);
return res.status(200).send('OK'); // Already exists
} catch (e) {
// Not found, continue to store
}
// Store new event
await conn.insertOne('webhook_events', {
_id: eventId,
rawBody: req.rawBody, // Store raw body for replay capability
receivedAt: new Date().toISOString(),
processedAt: null
});
res.status(200).send('OK');
});
```
### 5. Handle Retries
Webhook providers retry failed deliveries. Make your handler idempotent:
```javascript
app.post('/webhook', async (req, res) => {
const eventId = req.body.id;
// Check if already processed
const existing = await db.findOne('processed_events', { eventId });
if (existing) {
return res.status(200).send('Already processed');
}
// Process and record
await processEvent(req.body);
await db.insertOne('processed_events', { eventId, processedAt: new Date() });
res.status(200).send('OK');
});
```
## Building a Webhook Endpoint
Here's a complete webhook handler using Codehooks.io:
```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
// Webhook endpoint
app.post('/webhooks/orders', async (req, res) => {
// 1. Verify signature
const signature = req.headers['x-webhook-signature'];
const isValid = verifySignature(req.rawBody, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Parse event
const { event, data } = req.body;
// 3. Handle event types
switch (event) {
case 'order.created':
await handleNewOrder(data);
break;
case 'order.shipped':
await handleShipment(data);
break;
case 'order.cancelled':
await handleCancellation(data);
break;
}
// 4. Acknowledge receipt
res.status(200).json({ received: true });
});
async function handleNewOrder(order) {
const conn = await Datastore.open();
await conn.insertOne('orders', {
...order,
receivedAt: new Date().toISOString()
});
// Trigger downstream actions
await sendConfirmationEmail(order.customer_email);
await updateInventory(order.items);
}
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature || ''),
Buffer.from(expected)
);
}
export default app.init();
```
Deploy in seconds:
```bash
coho deploy
```
## Sending Webhooks (Outgoing)
You can also send webhooks from your application to notify others:
:::tip Production-Ready Webhook Delivery
For a complete webhook delivery system with retries, queues, and HMAC signing, use our ready-made template: `coho create myapp --template webhook-delivery` or see the [Build a Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) guide.
:::
```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
async function sendWebhook(url, event, data, secret) {
const payload = JSON.stringify({ event, data, timestamp: new Date().toISOString() });
// Create signature
const signature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Webhook-Signature': signature
},
body: payload
});
return response.ok;
}
// Example: Notify subscribers when an order is created
app.post('/orders', async (req, res) => {
const order = await createOrder(req.body);
// Get all webhook subscribers
const conn = await Datastore.open();
const subscribers = await conn.getMany('webhook_subscribers', {
event: 'order.created'
}).toArray();
// Send webhooks to all subscribers
for (const sub of subscribers) {
await sendWebhook(sub.url, 'order.created', order, sub.secret);
}
res.json(order);
});
export default app.init();
```
## Testing Webhooks
:::tip No Tunneling Needed with Codehooks.io
With Codehooks.io, you don't need ngrok or local tunneling. Just run `coho deploy` and your webhook endpoint is live instantly. Use `coho info` to get your public URL.
:::
### Local Development
If you're running a local server, use tools like [ngrok](https://ngrok.com) or [localtunnel](https://localtunnel.me) to expose it:
```bash
ngrok http 3000
# Gives you: https://abc123.ngrok.io
```
### Webhook Testing Services
- **[Webhook.site](https://webhook.site)** — Inspect incoming webhooks
- **[RequestBin](https://requestbin.com)** — Debug webhook payloads
- **[Hookdeck Console](https://hookdeck.com)** — Test and replay webhooks
### Stripe CLI Example
```bash
stripe listen --forward-to localhost:3000/webhooks/stripe
stripe trigger payment_intent.succeeded
```
## Common Webhook Patterns
### Event Queuing
For high-volume webhooks, queue events for processing with a worker:
```javascript
import { app, Datastore } from 'codehooks-js';
// Receive webhook and add to queue
app.post('/webhook', async (req, res) => {
// Quick acknowledgment
res.status(200).send('OK');
// Add to queue for async processing
const conn = await Datastore.open();
await conn.enqueue('webhook-events', req.body);
});
// Queue worker processes events asynchronously
app.worker('webhook-events', async (req, res) => {
const event = req.body.payload;
console.log('Processing webhook:', event.type);
// Handle the event (database updates, notifications, etc.)
await processWebhookEvent(event);
res.end();
});
export default app.init();
```
For complex multi-step processing, use the [Workflow API](/docs/workflow-api):
```javascript
import { app, Datastore } from 'codehooks-js';
// Define a workflow for processing webhooks
const orderWorkflow = app.createWorkflow({
name: 'process-order',
steps: {
validate: async (state, goto) => {
// Validate the order
state.validated = true;
return goto('notify', state);
},
notify: async (state, goto) => {
// Send notifications
await sendNotification(state.orderId);
return goto('sync', state);
},
sync: async (state, goto) => {
// Sync with external systems
await syncOrder(state.orderId);
return goto('$end', state);
}
}
});
app.post('/webhook', async (req, res) => {
res.status(200).send('OK');
// Start workflow for complex processing
await orderWorkflow.start({
orderId: req.body.data.id
});
});
export default app.init();
```
### Webhook Retries with Exponential Backoff
When sending webhooks, retry with increasing delays:
```javascript
async function sendWithRetry(url, payload, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (response.ok) return true;
} catch (error) {
console.log(`Attempt ${attempt + 1} failed`);
}
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
await sleep(Math.pow(2, attempt) * 1000);
}
return false;
}
```
## Ready-to-Deploy Webhook Templates
Skip the boilerplate and deploy production-ready webhook handlers in minutes using [Codehooks.io templates](https://github.com/RestDB/codehooks-io-templates):
### Receiving Webhooks (Inbound)
| Template | Command | Description |
|----------|---------|-------------|
| Stripe | `coho create myapp --template webhook-stripe-minimal` | Payment events with signature verification |
| GitHub | `coho create myapp --template webhook-github-minimal` | Push, PR, and issue events with HMAC-SHA256 |
| Shopify | `coho create myapp --template webhook-shopify-minimal` | Orders, products, customers with HMAC verification |
| Slack | `coho create myapp --template webhook-slack-minimal` | Events and commands with signature verification |
| Discord | `coho create myapp --template webhook-discord-minimal` | Slash commands with Ed25519 verification |
| Twilio | `coho create myapp --template webhook-twilio-minimal` | SMS and voice events with TwiML responses |
| Clerk | `coho create myapp --template webhook-clerk-minimal` | Auth events (signup, login) with Svix verification |
### Sending Webhooks (Outbound)
| Template | Command | Description |
|----------|---------|-------------|
| Webhook Delivery | `coho create myapp --template webhook-delivery` | Full webhook system with retries, queues, HMAC signing |
All templates include signature verification, error handling, and are ready for production use.
## Getting Started with Webhooks
Ready to implement webhooks? Here are integration guides for popular services:
### Payment & E-commerce
- [Stripe Webhooks](/docs/examples/webhooks/stripe) — Payment events with signature verification
- [PayPal Webhooks](/docs/examples/webhooks/paypal) — Transaction notifications with postback verification
- [Shopify Webhooks](/docs/examples/webhooks/shopify) — Orders, products, and customer events
### Developer Tools
- [GitHub Webhooks](/docs/examples/webhooks/github) — Push, PR, and issue events
- [Jira Webhooks](/docs/examples/webhooks/jira) — Issue and project updates
### Communication & Messaging
- [Slack Bot Webhooks](/docs/examples/webhooks/slack) — Commands, events, and interactive messages
- [Discord Bot Webhooks](/docs/examples/webhooks/discord) — Slash commands and interactions
- [Twilio Webhooks](/docs/examples/webhooks/twilio) — SMS and voice call events
### Email Services
- [SendGrid Webhooks](/docs/examples/webhooks/sendgrid) — Email delivery, opens, clicks, and bounces
- [Mailgun Webhooks](/docs/examples/webhooks/mailgun) — Email events with HMAC verification
### AI Platforms
- [OpenAI Webhooks](/docs/examples/webhooks/openai) — Deep Research, batch jobs, and fine-tuning completion
### Automation Platforms
Using Zapier, Make, n8n, or IFTTT? These platforms don't verify webhook signatures by default. Learn how to add security:
- [Secure Your Zapier/Make/n8n/IFTTT Webhooks](/blog/secure-zapier-make-n8n-webhooks-signature-verification) — Add signature verification to automation workflows
### Build Your Own
- [Build a Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) — Send outgoing webhooks with retries, queues, and HMAC signing
---
## Overview
*(Note: This content contains MDX/JSX code)*
import TOCInline from '@theme/TOCInline';
import myImageUrl from './images/quickstart/collection-with-data.webp';
import CodeBlock from '@theme/CodeBlock'
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import Link from '@docusaurus/Link';
import Badge from '@mui/material/Badge';
import {Button, Grid} from '@mui/material';
import Studio1Png from './images/studio/studio1.webp';
import Studio2Png from './images/studio/studio2.webp';
import Studio3Png from './images/studio/studio3.webp';
import Studio4Png from './images/studio/json-schema.webp';
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
import FolderOpenIcon from '@mui/icons-material/FolderOpen';
import LayersIcon from '@mui/icons-material/Layers';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import Admonition from '@theme/Admonition';
import {
CogIcon,
CheckCircleIcon,
CheckIcon,
DocumentCheckIcon,
DocumentTextIcon,
RocketLaunchIcon,
CloudArrowUpIcon,
MagnifyingGlassPlusIcon,
LockClosedIcon,
PlayIcon,
CircleStackIcon,
DocumentPlusIcon,
KeyIcon,
ClockIcon,
RectangleStackIcon,
ChartBarSquareIcon,
LinkIcon,
QueueListIcon,
SparklesIcon
} from '@heroicons/react/24/outline';
export const NumberIcon = ({ index = -1, style = {}}) => (
{index}
);
export const codex0 = `import {app} from 'codehooks-js'
// Use Crudlify to create a REST API for any database collection
app.crudlify()
// bind to serverless runtime
export default app.init();
`;
export const codex1 = `const URL = 'https://myproject-fafb.api.codehooks.io/dev/orders';
response = await fetch(URL+'?status=OPEN', /* options */)
const result = await response.json()
`;
export const codex2 = `import {app, datastore} from 'codehooks-js'
// REST API route
app.get('/sales', async (req, res) => {
// highlight-next-line
const conn = await datastore.open();
const query = { status: "OPEN" };
const options = { sort: { date: 1 } }
conn.getMany('orders', query, options).json(res); // stream JSON to client
})
// bind to serverless runtime
export default app.init();
`;
export const codex3 = `import {app, datastore} from 'codehooks-js'
// REST API route
app.post('/cacheit', async (req, res) => {
const conn = await datastore.open();
const opt = {ttl: 60*1000};
// highlight-next-line
await conn.set('my_cache_key', '1 min value', opt);
res.send('Look dad, I have my own cache now!');
})
// bind to serverless runtime
export default app.init();
`;
export const codex4 = `import {app} from 'codehooks-js'
// serve a direcory of Blob storage files
// highlight-next-line
app.storage({route:"/documents", directory: "/myblobs"})
// CRUD REST API
app.crudlify()
// bind to serverless runtime
export default app.init();`;
export const codex5 = `import app from 'codehooks-js'
// Trigger a function with a CRON expression
// highlight-next-line
app.job('*/5 * * * *', (_, job) => {
console.log('Hello every 5 minutes')
// Do stuff ...
job.end()
});
export default app.init();`;
export const codex6 = `import { app, Datastore } from 'codehooks-js'\n
// register a Queue task worker
// highlight-next-line
app.worker('ticketToPDF', (req, res) => {
const { email, ticketid } = req.body.payload;
//TODO: implement code to fetch ticket data, produce PDF
// and send email with attachment using Mailgun, Sendgrid or Amazon SES
// ...and implement error handling
res.end(); // done processing queue item
})\n
// REST API
app.post('/createticket', async (req, res) => {
const { email, ticketid } = req.body;
const conn = await Datastore.open();
// highlight-next-line
await conn.enqueue("ticketToPDF", { email, ticketid });
res.status(201).json({"message": \`Check email $\{email\}\`});
})\n
export default app.init();
`;
export const codex7 = `import { app, Datastore, aggregation } from 'codehooks-js'
... // collapsed code
const spec = {
$group: {
$field: "month",
$max: ["sales", "winback"],
$sum: "sales"
}
}
const db = await datastore.open();
const dbstream = db.getMany('salesData');
// highlight-next-line
const result = await aggregation(dbstream, spec)
... // collapsed code
`;
export const codex8 = `{
"oct": {
"sales": {
"max": 1477.39,
"sum": 1234.05
},
"winback": {
"max": 22.0
}
},
"nov": {
"sales": {
"max": 2357.00,
"sum": 5432.00
},
"winback": {
"max": 91.0
}
},
}`;
export const codex9 = `import {app} from 'codehooks-js'
// serve /dist for react frontend
app.static({
route: '/',
directory: '/dist',
default: 'index.html',
notFound: '/index.html'
}, cacheFunction)
// Optionally define cache function for static assets
function cacheFunction(req, res, next) {
const ONE_DAY = 24 \* 60 \* 60 \* 1000; // 24 hours in milliseconds
res.set('Cache-Control', \`public, max-age=\${ONE_DAY}, s-maxage=\${ONE_DAY}\`)
res.set('Expires', new Date(Date.now() + ONE_DAY).toUTCString())
res.removeHeader('Pragma');
next()
}
// CRUD REST API (if you need it)
app.crudlify({}, {prefix: '/api'})
// bind to serverless runtime
export default app.init();
`;
export const codex10 = `export default {
build: {
outDir: '../backend-src/dist', // Points to your codehooks source folder
},
};`;
export const webhookStripe = `import {app} from 'codehooks-js'
import Stripe from 'stripe';
app.post('/webhooks/stripe', async (req, res) => {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.rawBody, sig, endpointSecret);
} catch (err) {
console.log('Webhook signature verification failed:', err.message);
return res.status(400).send('Webhook Error: ' + err.message);
}
// Handle the event
switch (event.type) {
case 'payment_intent.succeeded':
const paymentIntent = event.data.object;
console.log('PaymentIntent was successful!', paymentIntent.id);
break;
case 'customer.subscription.created':
const subscription = event.data.object;
console.log('New subscription created:', subscription.id);
break;
default:
console.log('Unhandled event type', event.type);
}
res.json({received: true});
});
export default app.init();
`;
export const SmallFeature = ({
icon,
link = '#',
title = 'No title',
content =
);
};
## The agent-native backend platform
Codehooks.io is the **agent-native backend platform**. A complete, isolated backend — API routes, NoSQL database, key-value store, worker queues, cron jobs, and static asset hosting — that deploys in under 5 seconds from a single CLI command. Everything is built in. Nothing needs to be wired together.
The entire lifecycle is CLI-native:
- Create a project (`coho create`)
- Deploy code and assets (`coho deploy`)
- Inspect logs in real time (`coho log -f`)
- Query the database directly (`coho query`)
- Manage secrets, schemas, settings — all from the terminal
This means any tool that can run shell commands — Claude Code, Cursor, Codex, Cline, or a custom agent — can autonomously create, deploy, verify, and iterate on a production backend without human intervention. For the developer, the job becomes: specify what you want, review what the agent built, and verify it works.
Create projects using the CLI or the web-based [Studio](/docs/studio). A Codehooks.io project contains one or more isolated Spaces — complete self-contained environments with their own database, settings, and code.
The **[`codehooks-js`](https://www.npmjs.com/package/codehooks-js)** library is the foundation for all Codehooks development, offering 8 specialized APIs for building APIs, webhooks, event-driven architectures, and workflow automation. Explore each capability below to see how Codehooks makes development effortless.
:::tip Start Building in Minutes
Get up to speed fast with these essential resources:
- 🤖 [AI Agent Setup](/docs/ai-agent-setup) - Set up Claude Code, Cursor, Codex, or any AI agent to build on Codehooks
- 📋 [API Cheat Sheet](/docs/apicheatsheet) - All essential APIs on one page
- 🎥 [7-minute quick introduction](https://www.youtube.com/watch?v=3LGRhLU5a5A) - Join us building a complete CRUD API from scratch (it's easy)
:::
:::tip Codehooks CLI tool
To deploy code (and much more), you use the Codehooks CLI tool:
`npm install codehooks -g`
Code editing and deployment can also be done from the web based UI.
:::
## API and Webhook Development
}
title="Handle webhooks from any service"
content={
Codehooks makes it easy to receive and process webhooks from popular
services like Stripe, GitHub, Shopify, and more. The{' '}
req.rawBody property provides access to the raw request
body, which is essential for webhook signature verification.
The example below shows a Stripe webhook handler that verifies the webhook
signature using req.rawBody and processes different event types:
{webhookStripe}
This same pattern works for any webhook provider that requires signature
verification, including:
Stripe - Payment events and subscription updates
GitHub - Repository events, pull requests, and
CI/CD triggers
Shopify - Order notifications and inventory updates
Twilio - SMS and voice event notifications
And many more...
}
nextsteps={[
Codehooks webhook templates,
Stripe webhooks example,
Custom REST API routes,
Process webhooks asynchronously with Queues
,
Chain webhook events into Workflows,
]}
/>
}
title="Get a flying start with an automatic CRUD REST API"
content={
Consider a Codehooks project, for example myproject-fafb.
Under the hood, Codehooks deploys an automatic REST API which is made up
of the following really short - but complete application code:
{codex0}
It's just 3 lines of code 🔥, and it delivers a fully secure CRUD REST
API with a NoSQL database for JSON queries and updates. Client apps can
instantly connect to your data API, for example from a JavaScript app
like the example code snippet shown below:
{codex1}
}
nextsteps={[
Database REST API detailed docs,
Data validation and schema support
,
Develop custom REST APIs,
Auto-generate OpenAPI documentation,
Cheat Sheet for all the APIs,
]}
/>
Generate interactive Swagger UI documentation directly from your code.
Define schemas with Zod, Yup, or JSON Schema and Codehooks automatically
creates OpenAPI 3.0 specs. No manual YAML writing required.
}
title="Easy database API inspired by MongoDB"
content={
Sometimes a standard CRUD REST API isn't enough. With custom JavaScript
functions you can create logic that integrates directly to the NoSQL
datastore. No need to think about drivers, versions, protocol or
security, the database is just there, available as an intrinsic object
in every serverless function.
{codex2}
}
nextsteps={[
Getting started,
Database API,
NoSQL query language
,
Database indexing,
Check output logs with the CLI,
Query data with the CLI,
]}
/>
}
title="Speed up your application with a Key-Value cache"
content={
The Codehooks data engine includes a simple key-value datastore. A
key-value datastore has many use-cases, and is ideal for caching due to
the swift data access and simple, maintenance-friendly architecture. A
simple example is shown below, where a string is stored for 60 seconds
with the ttl option.
{codex3}
}
nextsteps={[
Key-Value API,
Tutorials,
]}
/>
## Background Jobs
}
title="CRON expressions + your function keeps running"
content={
Use simple CRON expressions to automate and manage the execution of
routine tasks. You can schedule a job to run at multiple, disparate
times. For example, you can schedule a task to run at 2 AM on Sundays
and 4 PM on Wednesdays with the same CRON job.
In the simple example below, the CRON expression{' '}
*/5 * * * * will trigger a JavaScript function each 5
minutes.
}
title="Scale workloads with queues and worker functions"
content={
Use persistent queues and "worker" functions to create robust,
efficient, and highly responsive back-end systems. Queues scales
processing across multiple nodes and will restore and continue working
in case of errors and system failures.
Check out the example below, where a REST API request starts a complex
task by adding it to the Queue for later processing.
Create and deploy robust, scalable workflows using queued functions and state management.
Build reliable backend systems with automatic retry, state persistence, and distributed processing.
The example below shows a simple workflow that processes customer onboarding with email validation.
{`import { app } from 'codehooks-js';
// define the workflow
const workflow = app.createWorkflow('customerOnboarding', 'Customer signup and email validation', {
START: async function (state, goto) {
state = { email: state.email, validated: false };
await sendWelcomeEmail(state.email);
goto('waitForValidation', state);
},
waitForValidation: async function (state, goto) {
if (state.validated) {
goto('complete', state);
} else {
wait({wait_message: 'Waiting for subflow to complete'});
}
},
complete: function (state, goto) {
state.status = 'completed';
goto(null, state); // workflow complete
}
});
// Start workflow from REST API
app.post('/start-onboarding', async (req, res) => {
const result = await workflow.start({ email: req.body.email });
res.send(result);
});
export default app.init();`}
}
nextsteps={[
Workflow API,
Example Workflows
]}
/>
## Blob Storage API
}
title="Store and manage files programmatically"
content={
Codehooks provides a built-in blob storage system for uploading,
storing, and serving files through your applications. Use the File API
to handle file operations programmatically in your serverless functions.
You can upload files to blob storage using the CLI command:
The example below shows how to handle file uploads, downloads, and
management through the File API in your application code:
{codex4}
}
nextsteps={[
File API,
Upload files with the CLI,
]}
/>
## Data Aggregation
}
title="Easy to use data aggregation format"
content={
Transform your live data streams into insights with just a few lines of
code. Codehooks has a simple but powerful JSON-based aggregation
expression language to specify data transformations. Groups and sums can
be nested to create both simple and advanced data outputs. The
aggregation API can process input from any JSON data-stream, for example
the query output from the NoSQL datastore. 🔥
{codex7}
The example aggreagation transformation produces JSON output similar to
the example below.
{codex8}
}
nextsteps={[Aggregation API]}
/>
## Frontend Hosting Setup
}
title="Host your frontend applications alongside your API"
content={
There are several benefits to hosting the frontend along with the backend API:
No CORS issues - Frontend and API share the same domain
Simplified deployment - Single command deploys both frontend and backend
Cost effective - No need for separate hosting services
Codehooks can serve your frontend applications as static assets, making
it easy to deploy full-stack SPA and PWA applications from a single codebase. All you need to do
is to include the HTML and CSS files in the source code and
then use the app.static file handler to serve them along with the backend code.
This hosting option is perfect if you don't need the high scalability offered by CDN-hosted frontends.
Important: Codehooks only serves static assets, not server-side
rendered content. For frameworks like Next.js, Nuxt and SvelteKit, you must configure
them for static generation/export (SSG).
Simply copy your framework's build or dist folder into your Codehooks
project source and configure the static handler to serve it. When you run codehooks deploy, both your API and your frontend will be deployed:
{codex9}
Note: The default and{' '}
notFound settings are crucial for client-side routing. They
ensure that all routes (like /about or{' '}
/users/123) serve the index.html file,
allowing your frontend router to handle navigation instead of the server
returning 404 errors.
This setup works seamlessly with popular frameworks configured for
static builds:
Pure HTML/CSS: Just keep the source with backend code from the start (in the dist/ folder in this example)
React: Copy your build/ folder (after{' '}
npm run build)
Vue: Copy your dist/ folder (after{' '}
npm run build)
Next.js: Configure static export (
next export), then copy your out/ folder
Nuxt: Configure static generation (
nuxt generate), then copy your dist/{' '}
folder
Svelte: Copy your build/ folder (after{' '}
npm run build)
SvelteKit: Configure static adapter, then copy your{' '}
build/ folder
Angular: Copy your dist/ folder (after{' '}
ng build)
Astro: Copy your dist/ folder (after{' '}
npm run build)
Gatsby: Copy your public/ folder
(after gatsby build)
We recommend automating this step by setting your frontend framework's output directory to point directly to your backend source folder.
This eliminates the manual copy step in your deployment workflow.
Example with Vite (in vite.config.js):
{codex10}
The example shown here is without any authentication. For most useful applications you would need to set up your frontend to use an authentication framework (Clerk, Auth0, Stack-Auth, codehooks-auth).
}
nextsteps={[
Example of static site hosting with Codehooks, React and codehooks-auth
,
Static file serving documentation
,
Authentication
,
]}
/>
## Next Steps
Read this far? Great 🙌
It's time to get your feet wet 💦. Why don't you start thinking about how codehooks' toolbox can help you build your solution. You can get started with the [quickstart for developers](/docs/quickstart-cli) or just [sign up](https://account.codehooks.io) and create your project and space using the web UI. You can easily upload some CSV data and immediately test your new API.
The documentation or tutorials are great places to start. If you need more help, feel free to contact us on the chat.
---
## Codehooks Studio
*(Note: This content contains MDX/JSX code)*
import StudioCarousel from '@site/src/components/StudioCarousel';
import imgDataManager from './images/carousel/carousel-datamanager.webp';
import imgSettings from './images/carousel/carousel-settings.webp';
import imgCodeEditor from './images/carousel/carousel-codeeditor.webp';
import imgApi from './images/carousel/carousel-api.webp';
import imgLog from './images/carousel/carousel-log.webp';
import imgProject from './images/carousel/carousel-project.webp';
export const slides = [
{ image: imgDataManager, alt: 'Data Manager', label: 'Data Manager', caption: 'Query and manage your data using MongoDB-like queries.' },
{ image: imgSettings, alt: 'Settings', label: 'Settings', caption: 'Manage tokens, environment variables, JWKS auth, and custom domains.' },
{ image: imgCodeEditor, alt: 'Code Editor', label: 'Code Editor', caption: 'Built-in code editor with syntax highlighting. Deploy in seconds.' },
{ image: imgApi, alt: 'API Routes', label: 'API', caption: 'See deployed API routes and get working example code for cURL, JavaScript, and Python.' },
{ image: imgLog, alt: 'Live Logs', label: 'Logs', caption: 'Watch live API and function logs in real time.' },
{ image: imgProject, alt: 'Project Overview', label: 'Project', caption: 'Add spaces (environments), developers, see billing info, and manage your project.' },
];
# Codehooks Studio
The Codehooks Studio is a browser-based dashboard for managing your backend. While the [CLI](/docs/cli) is the fastest way to develop and deploy, the Studio gives you a visual interface for everything else — inspecting data, viewing logs, managing settings, and more.
## What you can do
- **Query & manage data** — Browse collections, run MongoDB-like queries, edit documents, import/export data
- **Edit & deploy code** — Built-in code editor with syntax highlighting and one-click deploy
- **View API routes** — See all deployed endpoints with auto-generated cURL, JavaScript, and Python examples
- **Stream live logs** — Watch API and function logs in real time for debugging
- **Manage settings** — Configure API tokens, environment variables, JWKS authentication, and custom domains
- **Manage your project** — Add spaces (environments), invite developers, and view billing
## Getting started
1. Sign up at [account.codehooks.io](https://account.codehooks.io/login?signup)
2. Create a project — or use `coho create` from the CLI
3. Open the Studio from your project dashboard
The Studio and CLI work together. Deploy from the terminal, manage from the browser.
---
## Linking Alpine.js to a Database REST API: An Easy Tutorial
*(Note: This content contains MDX/JSX code)*
# Linking Alpine.js to a Database REST API: An Easy Tutorial
In this guide, we'll explore creating a dynamic web application with Alpine.js. We'll set up a frontend using Alpine.js, a minimalistic JavaScript/HTML framework, and integrate it with a comprehensive REST API database backend. For rapid design, we'll use [DaisyUI](https://daisyui.com/) and [Tailwind CSS](https://tailwindcss.com/). This project offers a hands-on way to see these technologies in action.

## Project setup
After creating a [new account](https://account.codehooks.io/login?signup) (if you don't have one already), you should create a new project using the the Codehooks account web app (click the [**NEW PROJECT +**] button). Give your project any name you like, in this example we'll use the projectname **alpine**.

### Log in to your account with the CLI
Make sure to install the latest version of the Codehooks CLI.
```bash
npm i -g codehooks
```
Then, log in to your account.
```bash
coho login
```
### Create the Codehooks source files
Next, create a new directory to hold the source code, e.g. _myalpine_.
```bash
mkdir myalpine
cd myalpine
```
The CLI command `coho init` will attach the local project directory to a Codehooks project (the CLI will present a list), this creates a default Codehooks app file `index.js` and a project specific `config.js` file.
```bash
coho init
```
This command creates a default backend app `index.js`, change this code to the following:
```js
/*
Alpine.js REST API data example
*/
import { app } from 'codehooks-js';
app.static({ route: '/static', directory: '/app' });
// Use Crudlify to create a REST API for any collection
app.crudlify();
// bind to serverless runtime
export default app.init();
```
In same the project directory, install the [codehooks-js](https://www.npmjs.com/package/codehooks-js) NPM package.
```bash
npm i codehooks-js
```
Deploy the default CRUD backend server.
```bash
coho deploy
```
We'll repeat the deploy command after adding the client side Alpine.js html files (see next sections).
### Create the database and deploy the backend server
To create a database of football players we use this [Kaggle dataset](https://www.kaggle.com/datasets/vivovinco/20212022-football-player-stats). The dataset is a CSV file `2021-2022-Football-Player-Stats.csv` with 2921 players.
We can import the full dataset with the CLI command `coho import`, note that this file has a different separator (_semicolon_) and encoding (_latin1_).
```bash
coho import -f ~/Downloads/2021-2022-Football-Player-Stats.csv -c football --separator ';' --encoding 'latin1'
```
You can also import data in the Studio app, either way you should see a **football** collection like the one shown in the screen shot below.

The database backend is now ready to serve REST API calls from web client applications. Keep reading to learn how to create the Alpine.js web app.
### Create the Alpine.js source files
In the project directory, create a new a sub directory (e.g. `app`) to hold the Alpine.js files.
```bash
mkdir app
```
In the subdirectory _app_, create two new files for the alpine-js source code.
```bash
cd app
touch index.js script.js
```
The project setup is now ready, and your project directory should have the following structure:
```
.
├── app
│ ├── index.html
│ └── script.js
├── config.json
├── index.js
└── node_modules
```
## The Alpine.js frontend application
The Alpine.js app (located in the `app/index.html` file) demonstrates the interactive search page for football players.
The working application is shown in the screen shot below (running locally from the file system).

You can test a [live version of the Alpine.js app here.](https://amiable-zephyr-e964.codehooks.io/static/index.html)
This example app demonstrates two important features in Alpine.js:
1. Loading external data into a global datamodel using [Alpine store](https://alpinejs.dev/globals/alpine-store)
2. Changing the application state by calling a method on the global datamodel when an input area changes using [Alpine events](https://alpinejs.dev/directives/on#keyboard-events)
The `app/index.html` code is shown below, notice line 14,16 with the event listener calling a method on the global datamodel, and line 27-34 iterating on the rows in the global datamodel. Make sure to copy the source code below info your local files to test.
:::note CSS
This example also uses [DaisyUI](https://daisyui.com/) and [Tailwind CSS](https://tailwindcss.com/).
:::
```html {14,16,27-34} title="index.html" showLineNumbers
Player
Squad
Nation
Index
```
The application global state (let's call it `coho`) is an [Alpine store](https://alpinejs.dev/globals/alpine-store) and it's implemented in the `app/script.js` file shown below.
```js {7} showLineNumbers
// replace this with your project url, use CLI command 'coho info' to find yours
const MY_CODEHOOKS_URL = '/football';
// replace this with your own read-only key, use CLI command 'coho add-token' to create a new one
const MY_API_KEY = '0b49638f-56c3-48e9-8725-7f3c20f25316';
document.addEventListener('alpine:init', () => {
Alpine.store('coho', {
loading: false,
search: '',
players: [],
async getData() {
this.loading = true;
this.players = await getDataFromAPI(this.search);
this.loading = false;
},
});
});
// Fetch data from Codehooks REST API
async function getDataFromAPI(search) {
var myHeaders = new Headers();
myHeaders.append('x-apikey', MY_API_KEY); // read-only token
myHeaders.append('Content-Type', 'application/json');
var requestOptions = {
method: 'GET',
headers: myHeaders,
redirect: 'follow',
};
var query = '{}';
console.log('getData', search);
if (search.length > 0) {
query = JSON.stringify({ Player: { $regex: search, $options: 'gi' } });
}
var hints = JSON.stringify({
sort: { Squad: 1, Nation: 1 },
$fields: { Player: 1, Nation: 1, Squad: 1 },
});
var URL = `${MY_CODEHOOKS_URL}?q=${query}&h=${hints}`;
console.log(URL);
const response = await fetch(URL, requestOptions);
return response.json();
}
```
## Deploy the complete Alpine.js front-end and backend application
After adding the client side files as described above, you can run the CLI command `deploy` again.
This will upload the static assets so it can be served under your domain.
```bash
coho deploy
```
Open the `app/index.html` file in your web browser to test the Alpine.js web app locally, or open the auto generated domain, e.g. [https://amiable-zephyr-e964.codehooks.io/static/index.html](https://amiable-zephyr-e964.codehooks.io/static/index.html).
## Conclusion
This tutorial provided you with a hands-on example which combined Alpine.js with a REST API backend using Codehooks.io. By following the steps outlined, you'll gain a deeper understanding of how you can apply these technologies in your own projects.
Source code at [Github here.](https://github.com/RestDB/codehooks-io-examples/tree/main/alpinejs-players)
---
import FAQSection from '@site/src/components/FAQSection';
---
## Streamline Your Backend with JSON Schema on Codehooks.io
*(Note: This content contains MDX/JSX code)*
In modern app development, having a reliable backend is essential. [Codehooks.io](https://codehooks.io) offers a powerful [NoSQL datastore](/docs/nosql-database-api), [JSON schema](https://json-schema.org/) validation, and a secure [REST API](/docs/database-rest-api) to keep your data consistent and your app scalable.

### Why Backend Structure Matters
A well-structured backend is key to:
1. **Data Integrity**: JSON Schema enforces data structure, preventing invalid data from entering your system.
2. **Security**: Strong authentication mechanisms protect your data.
3. **Scalability**: Ready to handle growing data loads as your app expands.
4. **Flexibility**: Seamless REST API integration connects easily with various clients.
### Getting Started with Codehooks.io
1. **Setup**: Start by creating an account on Codehooks.io, installing the CLI, and initializing your project.
```bash
npm install -g codehooks
codehooks login
mkdir myproject && cd myproject
codehooks init
```
Get your project's endpoint URLs:
```bash
codehooks info
```
2. **Define JSON Schema**: Maintain data consistency by defining a JSON Schema.
Example `personSchema.json`:
```js
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Person",
"type": "object",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" },
"age": { "type": "integer", "minimum": 0 },
"email": { "type": "string", "format": "email" }
},
"required": ["firstName", "lastName", "email"]
}
```
Apply the schema to your collection:
```bash
codehooks add-schema --collection 'person' --schema './personSchema.json'
```
3. **Import Data**: Easily import data in JSON, CSV, or Excel format.
```bash
codehooks import --filepath './persondata.json' --collection 'person'
```
4. **Use the REST API**: Interact with your data via the REST API.
Example for creating a record:
```javascript
const newRecord = {
lastName: 'Doe',
email: 'john.doe@example.com',
age: 30,
};
fetch('https://your-project-url/person', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer YOUR_JWT_TOKEN',
},
body: JSON.stringify(newRecord),
})
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error('Error:', error));
```
JSON Schema validation ensures only data that fits your schema gets through, returning a `400` error if it doesn't.
For example, if a required field is missing, the validator will return a `400` error like this:
```js
{
"schemaError": [
{
"instancePath": "",
"schemaPath": "#/required",
"keyword": "required",
"params": {
"missingProperty": "firstName"
},
"message": "must have required property 'firstName'"
}
]
}
```
### Codehooks.io Studio
Prefer using a UI? [Codehooks Studio](/docs/studio) offers a clean interface for managing your datastore, importing data, and setting up schemas—all without needing to write any code.

### Wrapping Up
Using JSON Schema on Codehooks.io ensures your backend is solid, with enforced data validation, ready-made APIs, and the scalability to grow with your app. Let Codehooks.io manage the backend so you can focus on building your application.
### Further Reading and Learning
- **[Understanding JSON Schema](https://json-schema.org/understanding-json-schema/)**
- A comprehensive guide to defining and using JSON Schema for data validation.
- **[RESTful API Design Best Practices](https://restfulapi.net/rest-api-design-tutorial-with-example/)**
- Best practices for designing and implementing RESTful APIs.
- **[JavaScript Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)**
- Learn how to use the Fetch API in JavaScript for making HTTP requests.
- **[Securing APIs with JWT](https://auth0.com/docs/tokens/json-web-tokens)**
- A guide to securing your APIs using JSON Web Tokens (JWT).
---
## SQL vs NoSQL: When to use and key differences
*(Note: This content contains MDX/JSX code)*
# SQL vs NoSQL: When to use?
Choosing between SQL and NoSQL databases is a crucial decision that can significantly impact your project's success. This guide provides an in-depth comparison of SQL vs NoSQL, helping you understand their differences and choose the right database type for your needs.

## SQL vs NoSQL Overview
1. [SQL vs NoSQL: Key Differences](#sql-vs-nosql-key-differences)
2. [SQL vs NoSQL Performance Comparison](#sql-vs-nosql-performance-comparison)
3. [Practical Examples: SQL vs NoSQL in Action](#practical-examples-sql-vs-nosql-in-action)
4. [SQL to NoSQL Query Mapping Examples](#sql-to-nosql-query-mapping-examples)
5. [Popular Backend Services and Their Database Types](#popular-backend-services-and-their-database-types)
6. [Conclusion: Making the Right Choice](#conclusion-making-the-right-choice)
## SQL vs NoSQL: Key Differences
Before diving into the details of each database type, let's compare the key differences between SQL and NoSQL:
| Feature | SQL Databases | NoSQL Databases |
| --------------- | ----------------------------- | ------------------------------------------- |
| Data Model | Structured, table-based | Flexible (document, key-value, graph, etc.) |
| Schema | Predefined, fixed | Dynamic, flexible |
| Scalability | Vertical | Horizontal |
| ACID Compliance | Yes | Varies (some offer ACID compliance) |
| Consistency | Strong | Eventual (in most cases) |
| Use Cases | Complex queries, transactions | High traffic, big data, real-time web apps |
:::info What is ACID?
ACID is an acronym that stands for Atomicity, Consistency, Isolation, and Durability. These properties ensure database transactions are processed reliably:
- **Atomicity**: All operations in a transaction succeed or the entire transaction is rolled back.
- **Consistency**: A transaction brings the database from one valid state to another.
- **Isolation**: Concurrent transactions do not interfere with each other.
- **Durability**: Once a transaction is committed, it remains so, even in the event of power loss or system failures.
:::
SQL databases are known for strong ACID compliance, while NoSQL databases may sacrifice some ACID properties for increased performance and scalability.
## SQL vs NoSQL Performance Comparison
| Aspect | SQL | NoSQL |
| ----------------- | ------------------------------------- | ------------------------------ |
| Read Performance | Excellent for complex queries | Excellent for simple queries |
| Write Performance | Good, but can slow with scale | Excellent, especially at scale |
| Scalability | Vertical (harder to scale) | Horizontal (easier to scale) |
| Large Datasets | Can struggle with very large datasets | Handles large datasets well |
| Complex Joins | Excellent | Limited or not supported |
The performance difference between SQL and NoSQL can vary greatly depending on the specific use case and implementation.
## Practical Examples: SQL vs NoSQL in Action
Let's compare how you might implement a simple data storage and retrieval operation in both SQL and NoSQL. These examples highlight the syntactical and conceptual differences between the two approaches.
### SQL Example (using PostgreSQL):
```sql
-- Create a table
CREATE TABLE items (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
category VARCHAR(50)
);
-- Insert data
INSERT INTO items (name, category) VALUES ('Hammer', 'tools');
-- Retrieve data
SELECT * FROM items WHERE category = 'tools';
```
### NoSQL Example (using Codehooks.io API):
```javascript
import { app, datastore } from 'codehooks-js';
// Store data
app.post('/items', async (req, res) => {
const conn = await datastore.open();
const result = await conn.insertOne('items', req.body);
res.json(result);
});
// Retrieve data
app.get('/items', async (req, res) => {
const conn = await datastore.open();
conn.getMany('items', { category: 'tools' }).json(res);
});
```
As demonstrated, SQL requires a predefined schema and uses structured query language, while NoSQL offers a more flexible, schema-less approach with a simpler API. Each has its advantages depending on your specific use case.
## SQL vs NoSQL: Query Examples for comparison
The following list shows [SQL](https://en.wikipedia.org/wiki/SQL) example statements expressed as NoSQL queries. Note that this is specific to codehooks.io (which use a query language similar to MongoDB). Read more about codehooks.io NoSQL query language in the [codehooks.io documentation](/docs/nosql-database-query-language).
#### `SELECT * FROM users`
```js
/*
* SQL statement:
* SELECT * FROM users
* expressed as a nosql database query
*/
const db = await Datastore.open();
db.find('users');
```
#### `SELECT user_id, status FROM users`
```js
const query = {};
const opt = {
hints: { $fields: { user_id: 1, status: 1 } },
};
db.find('users', query, opt);
```
#### `SELECT * FROM users WHERE status = "A"`
```js
const query = { status: 'A' };
db.find('users', query);
```
#### `SELECT * FROM users WHERE status != "A"`
```js
const query = { status: { $not: 'A' } };
db.find('users', query);
```
#### `SELECT * FROM users WHERE status = "A" AND age = 50`
```js
const query = { status: 'A', age: 50 };
db.find('users', query);
```
#### `SELECT * FROM users WHERE status = "A" OR age = 50`
```js
const query = { $or: [{ status: 'A' }, { age: 50 }] };
db.find('users', query);
```
#### `SELECT * FROM users WHERE age > 25`
```js
const query = { age: { $gt: 25 } };
db.find('users', query);
```
#### `SELECT * FROM users WHERE user_id like "bc%"`
```js
const query = { user_id: /^bc/ };
db.find('users', query);
```
#### `SELECT * FROM users WHERE status = "A" ORDER BY name ASC`
```js {5}
// Use the CLI to create a sorted index
// $ codehooks createindex --collection users --index name
const query = { status: 'A' };
const opt = {
sort: { name: 1 },
};
db.find('users', query, opt);
```
#### `SELECT * FROM users WHERE status = "A" ORDER BY name DESC`
```js {6}
// Use the CLI to create a sorted index
// $ codehooks createindex --collection users --index name
const query = { status: 'A' };
const opt = {
sort: { name: -1 },
};
db.find('users', query, opt);
```
#### `SELECT COUNT(*) FROM users`
```js
const query = {};
const opt = {
hints: { $onlycount: true },
};
db.find('users', query, opt);
```
#### `SELECT COUNT(*) FROM users WHERE age > 30`
```js
const query = { age: { $gt: 30 } };
const opt = {
hints: { $onlycount: true },
};
db.find('users', query, opt);
```
#### `SELECT * FROM users LIMIT 1`
```js
const query = {};
const opt = { limit: 1 };
db.find('users', query, opt);
```
#### `SELECT * FROM users LIMIT 5 SKIP 10`
```js
const query = {};
const opt = {
limit: 5,
offset: 10,
};
db.find('users', query, opt);
```
## SQL vs NoSQL: Best Use Cases
| SQL Use Cases | NoSQL Use Cases |
| ------------------------ | ------------------- |
| Financial systems | Real-time big data |
| ERP systems | Content management |
| CRM applications | IoT applications |
| E-commerce platforms | Social networks |
| Legacy systems migration | Gaming applications |
Choose SQL when you need ACID compliance, complex queries, and have a well-defined, stable schema. Opt for NoSQL when dealing with large volumes of unstructured data, requiring high scalability, or needing rapid development with changing data models.
## Popular Backend Services and Their Database Types
When choosing a backend service for your project, it's important to understand which database type they use. Here's a list of popular backend services and their primary database types:
| Service | Primary Database Type | Notes |
| ------------------------------------------------------------------------ | --------------------- | ----------------------------------------------------------- |
| [Supabase](https://supabase.com/) | SQL (PostgreSQL) | Offers real-time capabilities and PostgREST API |
| [Firebase](https://firebase.google.com/) | NoSQL | Uses Cloud Firestore, a document-based NoSQL database |
| [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) | NoSQL | Managed MongoDB service, document-based NoSQL |
| [AWS RDS](https://aws.amazon.com/rds/) | SQL | Supports multiple SQL engines (MySQL, PostgreSQL, etc.) |
| [Restdb.io](https://restdb.io/) | NoSQL | Document-based NoSQL with a REST API |
| [Codehooks.io](https://codehooks.io/) | NoSQL | Flexible NoSQL with built-in API and serverless functions |
| [Fauna](https://fauna.com/) | Multi-model | Supports both relational and document models |
| [Azure Cosmos DB](https://azure.microsoft.com/en-us/services/cosmos-db/) | Multi-model | Supports multiple NoSQL models (document, key-value, graph) |
| [Google Cloud Datastore](https://cloud.google.com/datastore) | NoSQL | Schemaless NoSQL datastore |
| [Amazon DynamoDB](https://aws.amazon.com/dynamodb/) | NoSQL | Key-value and document database |
This list showcases the diversity of database options available in modern backend services. Some services, like Fauna and Azure Cosmos DB, even offer multi-model capabilities, allowing you to leverage both SQL and NoSQL paradigms within a single service.
When selecting a backend service, consider not only the database type but also factors such as:
- Scalability and performance
- Pricing model (fixed or usage based)
- Integration with other services
- Developer experience and tooling
- Compliance and security features
Remember that the best choice depends on your specific project requirements, expected data volume, and development team's expertise.
## Conclusion: Making the Right Choice
Choosing between SQL and NoSQL depends on your specific project requirements. Consider factors such as data structure, scalability needs, consistency requirements, and development flexibility when making your decision.
SQL might be the better choice if you need:
- Strong data consistency
- Complex queries and transactions
- A predefined, stable schema
NoSQL could be more suitable if you require:
- Flexibility in data structure
- High scalability for large amounts of data
- Faster performance for simple read/write operations
Remember, it's not always an either-or decision. Some projects benefit from using both SQL and NoSQL databases to leverage the strengths of each.
At Codehooks.io, we offer a powerful NoSQL Database API that combines the flexibility of NoSQL with the ease of use of a REST API. It's an excellent choice for projects that require rapid development and scalability.
---
Ready to explore NoSQL for your project? Check out the [Codehooks.io NoSQL Database API](/docs/nosql-database-api) to get started.
---
## Easy API Integration Tutorial: Step-by-Step Guide with Examples
*(Note: This content contains MDX/JSX code)*
# Easy API Integration Tutorial: Step-by-Step Guide with Examples
APIs and webhooks are essential for connecting different systems and creating smooth user experiences. Whether you're pulling data from external services or receiving real-time events, API integration is at the heart of modern application development.
This tutorial walks you through the entire process—from understanding what API integration means to building robust integrations. You'll learn the tools, step-by-step process, and best practices that make integration work.

### What is API Integration? (API Integration Meaning)
API integration is the process of connecting two or more software applications through their APIs to enable data exchange and extend functionality. This is what makes modern software work together—allowing disparate systems to communicate and share information seamlessly.
**API integration examples include:**
- Connecting your app to Stripe for payment processing
- Pulling weather data from a weather API
- Sending emails through SendGrid or Mailgun
- Receiving real-time events via webhooks when orders ship or payments succeed
Instead of building these features from scratch, API integration allows you to leverage existing services, saving time and effort while creating more powerful applications.
### Why API Integration Matters
#### Enhancing Application Functionality
API integration enhances your application's capabilities by allowing you to leverage powerful external services. For instance, integrating a payment gateway API adds payment processing capabilities to your app without building a payment solution yourself. Similarly, APIs for analytics, location services, or messaging systems can enhance your application by providing additional features that users expect today.
Webhook integrations take this further by enabling real-time automation. When a customer completes a purchase, a webhook can instantly trigger inventory updates, send confirmation emails, and notify your shipping provider—all without polling or manual intervention.
Additionally, APIs reduce development costs and allow you to build more robust applications faster by reusing existing solutions.
### How to Integrate API: Step-by-Step Tutorial
Here's the API integration process that works for any service:
1. **Understand the API Documentation**: Start by thoroughly reviewing the API documentation. Documentation provides a roadmap for how to authenticate, make requests, handle responses, and understand error messages. It often includes examples of how to use the API, which can be valuable.
2. **Get API Access**: Obtain the necessary API keys or tokens. Most services require authentication to ensure only authorized clients can access their APIs.
3. **Set Up Authentication**: Choose the appropriate authentication method (e.g., API key, OAuth). Implement it in your code to establish a secure connection to the service.
4. **Choose the Right Tools**: Use the appropriate libraries or tools for your environment. Most programming languages offer packages or modules that simplify working with HTTP requests and parsing responses.
5. **Test the API Integration**: Use tools like [Postman](https://www.postman.com/), [Bruno](https://www.usebruno.com/) or cURL to manually test API endpoints. Make sure the API responses meet your expectations before you add them into your application's codebase.
6. **Handle Errors Gracefully**: Expect that things may go wrong. Build error handling into your API integration to handle common issues like network failures or rate limits.
7. **Set Up Webhook Endpoints** (if applicable): For services that push events to your app, create dedicated endpoints to receive webhooks. Implement signature verification to ensure requests are authentic, and use idempotency keys to handle duplicate deliveries.
8. **Monitor and Maintain**: APIs evolve over time, so it's important to keep your integration updated. Monitor API deprecations or updates to ensure compatibility with future versions.
### API Integration Tools
The right API integration tools can significantly streamline your workflow. Here's what you need for easy API integration:
**Testing and Development Tools**
- **[Functionize](https://www.functionize.com/automated-testing/automated-api-testing), [Postman](https://www.postman.com/), [Bruno](https://www.usebruno.com/), [Insomnia](https://insomnia.rest/)** : Interactive API testing platforms that allow you to test endpoints, manage collections, and collaborate with team members
- **[cURL](https://curl.se/)**: Command-line tool for making HTTP requests, perfect for quick testing and automation scripts
- **[Swagger/OpenAPI](https://swagger.io/)**: Tools for API documentation and testing that help you understand and interact with APIs
**API Management Platforms**
- **[Kong](https://konghq.com/), [AWS API Gateway](https://aws.amazon.com/api-gateway/), [Azure API Management](https://azure.microsoft.com/en-us/products/api-management/)**: Provide authentication, rate limiting, monitoring, and analytics for your APIs
- **[Apigee](https://cloud.google.com/apigee)**: Enterprise-grade API management with advanced security and analytics features
**Integration Platforms**
- **[Zapier](https://zapier.com/), [Make](https://www.make.com/), [Microsoft Power Automate](https://powerautomate.microsoft.com/)**: No-code platforms for connecting different services and automating workflows
- **[Pipedream](https://pipedream.com/)**: Developer-focused integration platform that combines visual workflows with custom code capabilities
- **[Codehooks.io](https://codehooks.io/)**: Serverless backend platform for building custom APIs and webhook endpoints that integrate multiple services, with built-in databases, job queues, and MCP (Model Context Protocol) support
- **[MuleSoft](https://www.mulesoft.com/), [Dell Boomi](https://boomi.com/)**: Enterprise integration platforms for complex B2B integrations
**Monitoring and Analytics**
- **[Datadog](https://www.datadoghq.com/), [New Relic](https://newrelic.com/)**: Monitor API performance, track errors, and analyze usage patterns
- **[Pingdom](https://www.pingdom.com/), [StatusCake](https://www.statuscake.com/)**: Uptime monitoring to ensure your integrated APIs remain available
**Development Libraries and SDKs**
- **[Axios](https://axios-http.com/) (JavaScript), [Requests](https://requests.readthedocs.io/) (Python), [Guzzle](https://docs.guzzlephp.org/) (PHP)**: HTTP client libraries that simplify API calls in different programming languages
- **Official SDKs**: Many API providers offer language-specific SDKs that handle authentication and provide convenient methods
These tools can help you build more reliable integrations, reduce development time, and maintain better oversight of your API ecosystem.
### Common API Integration Challenges (And How to Solve Them)
- **Inconsistent Documentation**: Sometimes API documentation can be outdated or incomplete. In such cases, contacting the provider or joining developer forums can help clarify ambiguities.
- **Authentication and Security**: Setting up authentication (especially OAuth) can be complex. Make sure to follow best practices, such as using secure protocols (HTTPS) and rotating API keys regularly.
- **Handling Rate Limits**: APIs often enforce rate limits to manage the load on their servers. To overcome this, implement retry logic and manage request pacing to avoid hitting these limits.
- **Error Handling**: It's crucial to account for different types of errors like timeouts, 400/500 status codes, and other unexpected responses for the API integration. Use clear logging to understand when and why errors occur. When you have multiple API integrations in one "transaction", make sure you have a strategy to retry or undo changes.
- **Versioning Issues**: API integrations evolve, and newer versions of APIs may not always be backward-compatible. To avoid integration breaking, always test and maintain version compatibility or build flexibility into your code for API changes.
- **Webhook Reliability**: When receiving webhooks, you need to handle potential issues like duplicate deliveries, out-of-order events, and burst traffic during peak times. Implement idempotency checks and consider using a queue to process events reliably.
### Backend API Integration: Building a Unified API Layer
When working with multiple API integrations, managing different endpoints, authentication methods, and data formats becomes complex. Building your own backend (also known as Backend for Frontend, or BFF) provides a unified interface that makes API integration seamless and easier to maintain.
:::info Backend for Frontend (BFF)
A Backend for Frontend (BFF) is an architectural pattern where a dedicated backend service is created specifically to support a particular frontend or client application. This approach allows you to tailor your API to the specific needs of your frontend, optimizing data transfer and simplifying client-side logic. BFFs are particularly useful when dealing with multiple external APIs or when you need to aggregate data from various sources before sending it to the client.
:::
Using a backend like the one you can create with [Codehooks.io](https://codehooks.io), you can tie multiple APIs together and provide a single endpoint for your frontend to interact with. This approach offers several benefits:
- **Centralized Logic**: By building a custom backend, you can consolidate business logic that involves multiple APIs. For instance, if your application needs to pull data from both a payment service and a customer management service, you can handle that logic on your backend, simplifying the frontend code.
- **Consistent Data Format**: Different APIs may return data in different formats. Your backend can standardize these formats, ensuring that your frontend only needs to handle a consistent structure. This reduces complexity on the client side and makes your code easier to maintain.
- **Improved Security**: Instead of exposing multiple third-party APIs directly to the frontend, which might involve managing different sets of credentials, your backend can act as a gatekeeper. This way, you only need to secure your own API, keeping the credentials and tokens for the third-party APIs safe.
- **Caching and Rate Limit Management**: By building your own backend, you can implement caching strategies to reduce the number of API calls, improving performance and helping to manage rate limits. Codehooks.io allows you to easily build and deploy backend functions that can handle caching and other data optimizations.
- **Integrated Databases and Worker Queues**: Codehooks.io offers integrated document and key-value databases, which allow you to store and manage data related to API integrations easily. This means you can persist data fetched from various APIs, aggregate information, and provide a more responsive experience for your users. Additionally, worker queues and job scheduling features can help automate tasks such as making periodic API calls, processing large datasets, or handling other background jobs, making the integration process more seamless and efficient.
- **Webhook Event Processing**: Your backend can receive webhooks from multiple services, validate their signatures, normalize the data, and then trigger the appropriate actions—whether that's updating your database, calling other APIs, or queuing background jobs.
#### API Integration Example: E-commerce with Multiple APIs
Here's a practical API integration example using Codehooks.io. Suppose you're building an e-commerce application that needs to integrate with several services:
- A payment gateway for handling transactions
- An inventory management system for tracking stock levels
- A shipping API for calculating delivery costs
Instead of integrating each of these APIs directly in your frontend, you can use Codehooks.io to build a single backend endpoint that coordinates these services. Your backend might expose a `/checkout` endpoint that:
1. **Validates Cart Items**: Calls the inventory API to ensure the items are in stock.
2. **Calculates Shipping Costs**: Fetches delivery options and costs from the shipping API.
3. **Processes Payment**: Interacts with the payment gateway to complete the transaction.
With this setup, your frontend only needs to call the `/checkout` endpoint, while all the complex logic and multiple API calls are handled seamlessly by your backend on Codehooks.io.
Your backend can also handle webhooks from these services. For example, when Stripe sends a `payment_intent.succeeded` webhook, your endpoint can verify the signature, update the order status in your database, and then call the shipping API to initiate fulfillment—creating a fully automated order flow.
:::tip AI-Assisted Development with Codehooks
Codehooks.io provides excellent resources for AI-assisted backend development:
- **[AI Agent Prompt Template](/docs/ai-agent-setup)**: A comprehensive template for generating Codehooks.io backend APIs using ChatGPT, Claude, Cursor and other development agents.
- **[MCP Server](https://github.com/RestDB/codehooks-mcp-server)**: Model Context Protocol server that allows AI agents to directly manage, deploy, and interact with your Codehooks backend through natural conversation
These tools make it incredibly easy to build complex API integrations through simple conversation with AI assistants.
:::
---
import FAQSection from '@site/src/components/FAQSection';
---
## Key Takeaways
1. **API integration meaning** - Connecting applications to exchange data automatically
2. **How to integrate APIs** - Follow the 8-step process from documentation to monitoring
3. **API integration tools** - Use the right testing, management, and development tools
4. **Backend API integration** - Build unified backends to manage multiple integrations
5. **Webhook handling** - Use signature verification and idempotency for reliability
Whether you're building your first API integration or managing complex multi-service architectures, the principles remain the same: understand the documentation, handle errors gracefully, and use the right tools for the job.
Ready to start? [Create a free Codehooks.io account](https://codehooks.io) and deploy your first API integration in minutes.
---
## Building Stateful Workflows in JavaScript: A Guide to Codehooks Workflow API
*(Note: This content contains MDX/JSX code)*
# Smarter Logic Flows with Codehooks Workflows
At **Codehooks**, our mission is to simplify the development of automations, integrations, and backend APIs. As your app logic grows more complex — onboarding flows, async jobs, conditional steps — it becomes clear: **You need a better way to organize and execute business logic**.
That's why we built the **Codehooks Workflow API** — a lightweight, serverless-friendly way to run **stateful workflows in JavaScript**, without the overhead of bulky orchestrators or external tools.

## Why Workflows Matter
- **Step-by-step Logic**: Break down complex tasks into simple, ordered steps. Each step works independently, making it easy to find and fix issues.
- **Retries and Conditional Branches**: Automatically retry failed steps and choose different paths based on conditions. Like an if/else statement, but for your whole workflow.
- **Pause/Wait States and Resume Triggers**: Pause workflows until something happens, like getting an API response or user approval. Continues automatically when ready.
- **Persistent State Management**: Safely saves all your workflow data between steps. Keeps track of everything, even if there are errors or delays.
- **Cloud-scale Execution**: Runs smoothly in the cloud with multiple workflows at once. Each workflow runs separately without interfering with others.
- **Code-First & LLM-Ready**: Define workflows in plain JavaScript code, making them perfect for LLM-assisted development. AI can help write, review, and improve your workflows just like any other code.
## Getting Started: Your First Workflow
```js title="index.js"
import { app } from 'codehooks-js'
const workflow = app.createWorkflow('parityCheck', 'Check if a number is even or odd', {
begin: async (state, goto) => {
state.number = Math.floor(Math.random() * 100)
goto('check', state)
},
check: async (state, goto) => {
const step = (state.number % 2 === 0) ? 'even' : 'odd'
goto(step, state) // branch
},
even: async (state, goto) => {
console.log(`${state.number} is even`)
goto('end', state)
},
odd: async (state, goto) => {
console.log(`${state.number} is odd`)
goto('end', state)
},
end: async (state, goto) => {
console.log('Workflow finished', state)
goto(null, state) // null complete the workflow
}
})
// REST API to start a new workflow instance
app.post('/start', async (req, res) => {
const result = await workflow.start({"Some initial": "state"});
res.json(result);
});
// export app interface to serverless runtime
export default app.init();
```
The above JavaScript workflow code can also be visualized as a diagram like the Flow chart diagram shown below:
```mermaid
flowchart TD
Start([Start]) --> Begin
Begin[begin] -->|generate random number| Check
Check{check} -->|number % 2 === 0| Even
Check -->|number % 2 !== 0| Odd
Even[even] -->|log even number| End
Odd[odd] -->|log odd number| End
End[end] --> Finish([Finish])
style Start fill:#2e7d32,stroke:#1b5e20,stroke-width:2px
style Finish fill:#c62828,stroke:#b71c1c,stroke-width:2px
style Check fill:#e65100,stroke:#bf360c,stroke-width:2px
style Begin fill:#00838f,stroke:#006064,stroke-width:2px
style Even fill:#00838f,stroke:#006064,stroke-width:2px
style Odd fill:#00838f,stroke:#006064,stroke-width:2px
style End fill:#00838f,stroke:#006064,stroke-width:2px
```
## Designed for Developers
- **JavaScript-native**: Write workflows in plain JavaScript using async/await. No special syntax to learn - just write normal JavaScript code.
- **Composable**: Break workflows into reusable modules that you can import and combine. Add middleware for common functionality like logging or error handling.
- **Serverless**: Built for serverless from the ground up. Stores workflow state separately from functions to keep memory usage low.
- **Distributed-safe**: Handles concurrent updates safely across multiple workers. Uses atomic operations to prevent data conflicts.
## LLM-Friendly Development
- **Code-First Approach**: Since workflows are defined in plain JavaScript, LLMs can easily understand, generate, and modify workflow code. No need to translate between visual tools and code.
- **Self-Documenting**: Each step is a named function with clear inputs and outputs, making it easy for LLMs to analyze and explain workflow logic.
- **Pattern Recognition**: Common workflow patterns are expressed as standard JavaScript patterns, allowing LLMs to suggest improvements and best practices.
- **Error Prevention**: LLMs can help catch common workflow issues like missing state transitions or infinite loops before runtime.
## Common Use Cases
- **User Onboarding**: Implement multi-step registration flows with validation, OAuth integration, and role-based access control. Handle edge cases like partial completions and timeouts.
- **Approval Flows**: Create approval workflows where multiple people need to review and approve something. Track who approved what and when, and send reminders for pending approvals.
- **Data Sync and Async Jobs**: Process large datasets with checkpointing and resumable execution. Includes built-in support for rate limiting, batching, and error handling.
- **External Event Triggers**: Listen for events from outside sources like webhooks and message queues. Handles duplicate events and ensures reliable processing.
** Ready to Build Smarter Backends?**
👉 [View the Workflow API docs →](/docs/workflow-api)
---
## Best Vibe Coding Tools & Why AI Agents Work Better with Simple Backend Infrastructure
*(Note: This content contains MDX/JSX code)*
# Best Vibe Coding Tools & Why AI Agents Work Better with Simple Backend Infrastructure
*Updated February 2026 with new tools (Windsurf, OpenAI Codex CLI, Gemini CLI) and refreshed pricing, stats, and feature descriptions across all entries.*
Ever notice how some days you're a caffeinated code ninja ready to refactor an entire codebase, while other days you can barely muster the energy to fix a typo? Welcome to the wonderfully human world of **vibe coding** – and the tools that get it.
## The Origin Story: How "Vibe Coding Tools" Became a Thing
The term "vibe coding" and the concept of "vibe coding tools" was popularized by Andrej Karpathy in a [February 2025 tweet](https://x.com/karpathy/status/1886192184808149383), where the renowned AI researcher and former Tesla AI director coined the phrase to describe development tools that seem to intuitively understand your current energy level and coding mood – tools that adapt to you rather than forcing you to adapt to them.

The concept resonated deeply with the developer community because it captured something we'd all experienced but never quite articulated. Picture this: It's 2 AM, you're debugging a particularly stubborn API integration, your coffee has gone cold, and suddenly your AI assistant suggests exactly the right solution with minimal fuss. That's a vibe coding tool – it just _gets_ what you need right now.
Karpathy's observation sparked widespread discussion about how our development tools should feel more human-centered. The concept gained traction as developers started sharing their own experiences of tools that felt more intuitive during different coding sessions. Some tools excel when you're in deep-focus mode, others shine during collaborative sessions, and some are perfect for those "I just want to ship something quickly" moments.
## Debunking the Misconception: "Vibe Coding == Can't Code"?
Before we dive deeper, let's address the elephant in the room. Some people mistakenly assume that vibe coding tools are a crutch for developers who "can't really code." This couldn't be further from the truth.
Vibe coding isn't about replacing programming skills – it's about amplifying them. The most skilled developers often embrace these tools because they understand that writing code is just one part of building great software. Senior engineers don't prove their worth by manually typing boilerplate or reinventing wheels; they prove it by solving complex problems efficiently and shipping quality products.
Think of it this way: a master chef doesn't grind their own spices to prove they can cook – they use the best tools available to focus on creating amazing dishes. Similarly, experienced developers use vibe coding tools to eliminate repetitive tasks and focus on architecture, problem-solving, and innovation.
The reality is that vibe coding tools often require _more_ programming knowledge to use effectively, not less. Understanding when and how to leverage AI assistance, knowing which abstractions to trust, and being able to debug generated code all require deep technical expertise.
Vibe coding tools share several characteristics:
- **Contextual Intelligence**: They understand not just what you're coding, but how you're coding
- **Minimal Friction**: They get out of your way when you're in the zone
- **Adaptive UI/UX**: The interface feels right for your current task and energy level
- **Smart Defaults**: They make good assumptions so you can focus on the creative work
- **Mood Flexibility**: They work whether you're feeling methodical or experimental
- **Intelligent Laziness**: They embrace the programmer virtue that being lazy often leads to more elegant, efficient solutions
The last point deserves special mention. As any experienced developer knows, "lazy" programmers often write the best code – not because they cut corners, but because they naturally gravitate toward solutions that require less maintenance, fewer lines of code, and more automation. Why write 50 lines of boilerplate when `app.crudlify()` does it in one? Why manually configure CORS, rate limiting, and database connections when your tool handles it intelligently? Vibe coding tools embody this philosophy by doing the heavy lifting so you can focus on solving actual business problems rather than fighting infrastructure.
## The Essential Vibe Coding Tools Toolkit
The vibe coding tools landscape is exploding with innovation. From AI-powered IDEs to natural language app builders, there are dozens of tools emerging that understand and adapt to developer energy and workflow. The tools we'll explore here represent just the tip of the iceberg – a curated selection of standout platforms that exemplify what makes a tool truly "vibe-friendly."
**Real-world vibe scenario**: Sarah's got a weekend project idea – a mood-tracking app for her team. She's feeling creative but not in the mood for infrastructure setup. She spins up a Codehooks backend (`app.crudlify()` for instant CRUD), uses v0 to generate a clean React dashboard, and Cursor to refactor the logic. Three hours later, she's got a working app deployed. That's vibe coding in action – tools that match her energy level and let her focus on the creative problem-solving.
### 1. v0 by Vercel
[v0](https://v0.app) is Vercel's AI-powered development agent that has evolved from a UI component generator into a full-stack application builder. It can plan, code, and deploy entire Next.js applications from natural language descriptions. While it supports various frameworks, it really works best using React, Tailwind and shadcn/ui components. What makes v0 a perfect vibe tool: You can preview, modify and seamlessly deploy these pages to the cloud.
**The "Rapid Prototyping" Vibe**: v0 excels when you're in that "I need a frontend for my API" mood. You can literally describe your UI in plain English and get React components that work immediately. Perfect for Codehooks developers who want to quickly build frontends for their serverless APIs.
```
// Prompt: "Create a dashboard showing API statistics with charts"
// v0 generates: Complete React component with charts, responsive design, and clean styling
```
**Best Vibe**: When you're feeling experimental and want to see ideas come to life instantly without getting bogged down in setup
### 2. Cursor
[Cursor](https://cursor.com) has quickly become the gold standard for AI-powered code editors. Unlike traditional IDEs with AI bolted on, Cursor was built from the ground up with AI as a first-class citizen. It supports multiple LLMs (Claude Sonnet 4.5, GPT-5, Gemini, and others), letting you choose the model that matches your current task and preference. It understands your entire codebase context and can make suggestions that anticipate your next move. Like many modern AI coding tools, Cursor supports MCP (Model Context Protocol), enabling seamless integration with external data sources and services.
**The "I'm in the zone" Vibe**: Cursor's AI can follow your coding patterns and suggest entire functions or refactors that match your style. It's like having a coding partner who's read your entire codebase and knows exactly what you're trying to build.
**Best Vibe**: When you're in flow state and want an AI that keeps up with your thought process without breaking your rhythm
### 3. Bolt.new
[Bolt.new](https://bolt.new) is an AI-driven platform that lets you prompt, edit, and deploy full-stack web apps right in your browser. It's built on WebContainers tech, meaning you get a legit dev environment—NPM packages, Supabase integration, and Netlify deployment included.
**The "Build Everything Fast" Vibe**: Bolt shines when you want to iterate quickly. Its "diffs" feature means you can make changes and see updates instantly without full rebuilds. Think of it as the tool for "weekend hackathon energy" – when you want to build a complete MVP in hours, not days.
**Best Vibe**: When you're in that "let's ship something complete" mood and want AI that can handle frontend, backend, and deployment with speed
### 4. Lovable.dev
[Lovable.dev](https://lovable.dev) emphasizes ease of use and accessibility for full-stack application development. It uses AI to translate user prompts into functional code with minimal input and focuses on collaborative workflows.
**The "Thoughtful Architecture" Vibe**: Unlike Bolt's "ship fast" approach, Lovable takes time to plan. It suggests project structure, discusses trade-offs, and integrates with GitHub for proper version control. Perfect when you're building something that needs to scale or when working with a team. Lovable supports multiple backends, but has a deep integration with Supabase which enables advanced features like schemas, migrations and authentication.
**Best Vibe**: When you're feeling methodical and want an AI that thinks through architecture, considers maintainability, and helps you avoid technical debt from day one
### 5. Claude Code
[Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) is the agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster through natural language commands. This is the ultimate vibe tool for developers who live in the terminal.
**The "Terminal Native" Vibe**: Claude Code maps and explains entire codebases in a few seconds. It uses agentic search to understand project structure and dependencies without you having to manually select context files. When you're feeling like a command-line wizard and want AI that speaks your language.
**Best Vibe**: When you're in deep-work mode and want powerful AI assistance without leaving your terminal environment
### 6. GitHub Copilot & Claude via Extensions
[GitHub Copilot](https://github.com/features/copilot) and Claude (through various VS Code extensions and integrations) have revolutionized the vibe coding experience. Both VS Code and Claude support MCP (Model Context Protocol), creating a powerful ecosystem where AI assistants can access external data sources, databases, and services. Copilot has evolved well beyond autocomplete — its Coding Agent mode can autonomously create pull requests, modify multiple files, and run tests, while offering model selection across Claude Opus 4, GPT-5, and o3. Claude shines in conversational debugging and architectural discussions through extensions and API integrations. These tools adapt to your coding energy – suggesting simple completions when you're tired, or diving deep into complex refactors when you're energized.
**Best Vibe**: When you're prototyping and want to focus on logic rather than syntax, or when you need a rubber duck that actually codes back
### 7. Cline
[Cline](https://cline.bot) is the autonomous AI coding agent that's taken the developer community by storm with over 5M installs and 57k GitHub stars. Unlike traditional autocomplete tools, Cline operates as your collaborative AI partner directly within VS Code (and other IDEs), capable of creating files, editing code, executing terminal commands, and even browsing the web. What sets Cline apart is its thoughtful, human-in-the-loop approach – it breaks down complex tasks into clear steps, explains its reasoning, and asks for approval before making changes. With full MCP support, Cline can connect to external databases, live documentation, and hundreds of specialized tools, making it incredibly versatile for different development workflows.
**The "Autonomous Assistant" Vibe**: Cline shines when you're dealing with complex, multi-step tasks or unfamiliar codebases. It's like having a skilled junior developer who can handle the heavy lifting while you focus on architecture and business logic. Perfect for those "I need to scaffold this entire feature" or "help me debug this mysterious issue" moments.
**Best Vibe**: When you're feeling overwhelmed by a large task and want an AI that can break it down, execute it step-by-step, and learn from your codebase context while maintaining full transparency about what it's doing
### 8. Windsurf
[Windsurf](https://windsurf.com) (formerly Codeium) is a full AI IDE that rivals Cursor with its own take on agentic development. Its standout feature is Cascade — a multi-file AI agent that can reason across your entire codebase, plan changes, and execute them across multiple files in a single flow. It also includes Supercomplete (predictive autocomplete), MCP integrations, and a built-in memory system that learns your project over time.
**The "Integrated Flow" Vibe**: Windsurf shines when you want everything in one place. In-editor app previews, deployment, and multi-file refactoring without ever leaving the IDE. It's the tool for developers who want deep AI integration without configuring plugins or extensions.
**Best Vibe**: When you're building something substantial and want an AI that can see and modify your whole project at once, not just the file you're looking at
### 9. OpenAI Codex CLI
[OpenAI Codex CLI](https://github.com/openai/codex) is an open-source terminal coding agent built in Rust. It's OpenAI's answer to Claude Code — a lightweight, fast agent that lives in your terminal and can read, write, and execute code autonomously. It supports MCP, follows the AGENTS.md convention for project context, and works with multiple GPT-5 models.
**The "Open Source Terminal" Vibe**: Codex CLI appeals to developers who want the terminal-native workflow of Claude Code but prefer OpenAI's models or want a fully open-source tool they can customize and self-host.
**Best Vibe**: When you want a fast, hackable terminal agent with no vendor lock-in on the tool itself
### 10. Gemini CLI
[Gemini CLI](https://github.com/google-gemini/gemini-cli) is Google's free, open-source terminal AI agent. The standout feature is its 1M token context window — it can ingest massive codebases in a single pass. It's completely free with a Google account (1,000 requests/day), supports MCP, and handles multi-file operations.
**The "Massive Context" Vibe**: When you're working on a large codebase and need an AI that can hold the entire project in context without chunking or summarizing. The free tier makes it an easy addition to any developer's toolkit.
**Best Vibe**: When you're exploring or refactoring a large codebase and want an AI that can see everything at once without hitting context limits
### 11. Emergent
[Emergent](https://emergent.sh) is one of the fastest-growing vibe coding platforms, reaching $100M ARR just eight months after launch with over 6 million users. Founded by the twin brothers behind Amazon SageMaker and Dunzo, it takes a fundamentally different approach — instead of helping you write code, you describe the app you want and Emergent's multi-agent system handles planning, coding, testing, and deployment autonomously. The platform generates full-stack applications you can export to GitHub, so you own the code.
**The "Describe and Ship" Vibe**: Emergent shines when you have a clear app idea and want to go from description to deployed product with minimal hands-on coding. It's particularly powerful for domain experts and non-technical founders who know exactly what they want to build. Like Bolt.new and Lovable, it handles the full stack — but leans harder into autonomous end-to-end generation rather than interactive editing. Keep an eye on credit consumption for complex projects.
**Best Vibe**: When you have a specific app vision and want AI to handle the entire build pipeline — planning, coding, and deployment — while you focus on product decisions
---
## Backend Complexity Can Kill the Vibe
Here's something I've noticed after using all these tools: the most beautiful AI assistant in the world becomes useless the moment you hit backend complexity. You can have Cursor suggesting perfect React components and Claude helping with brilliant algorithms, but if deploying your app requires juggling AWS services, configuring databases, and debugging CORS issues, your creative flow dies a brutal death.
Even worse is the version nightmare – trying to make a PostgreSQL client, Redis cache, message queue library, job scheduler, and authentication middleware all play nicely together. Each service has its own connection patterns, error handling, and configuration quirks. Your AI assistant might suggest brilliant code, but it can't debug why your job queue isn't talking to your database, or why your authentication tokens work in development but fail in production.
The dirty secret of most "vibe coding" tools is that they solve the easy part (generating code) but ignore the hard part (making it actually work in production). This is why coherent backend approaches matter so much for AI-powered development. When your AI agents can understand your entire stack through a simple, consistent interface, they become exponentially more useful.
Some platforms get this right. **Supabase** deserves huge credit here – their consistent MCP-supported environment works beautifully with tools like Lovable and Bolt.new. When your database, authentication, real-time subscriptions, and edge functions all share the same client patterns and error handling, AI tools can reason about your entire stack instead of just fragments.
Think about it: an AI assistant that can deploy your code, query your database, and handle authentication through a unified CLI is fundamentally different from one that can only suggest React components while you manually handle everything else. CLI-native platforms make this real — when every operation is a shell command, any agent with terminal access becomes a full-stack developer.
This is why coherent backend platforms matter for vibe coding – not because they're magical, but because they eliminate the integration complexity that breaks your flow. Whether it's Supabase's unified PostgreSQL ecosystem, Codehooks' CLI-native serverless simplicity (`coho deploy` and `app.crudlify()` handle everything in one abstraction), or other platforms designed for consistency, the key is having fewer moving parts that work together seamlessly.
---
## What Makes Infrastructure Vibe-Friendly (A Slightly Biased Take)
Full disclosure: you're reading this on the Codehooks blog, so take this with a grain of salt. But after using these AI tools extensively, I've learned some things about what infrastructure actually enables vibe coding rather than killing it.
The tools above adapt to your coding style and energy, but they need a solid foundation to shine. Here's what I look for in vibe-friendly backend platforms:
**Codehooks — CLI-native backend for AI agents**: The serverless backend that just works. When your AI tools are suggesting brilliant solutions, the last thing you want is to get bogged down in deployment configs, database setup, or CORS issues. [Codehooks](https://codehooks.io) handles all of this invisibly:
- **CLI-native**: Everything is done from the terminal — `coho create`, `coho deploy`, `coho log -f`. Any AI agent with shell access (Claude Code, Codex CLI, Cursor) can autonomously create, deploy, and verify a backend
- **Instant CRUD APIs**: `app.crudlify()` gives you a full REST API with validation in one line
- **Claude Code skill**: Install the [Codehooks plugin](/docs/ai-agent-setup) and Claude Code gets full platform context, templates, and the `/codehooks:backend` command
- **Invisible infrastructure**: Authentication, rate limiting, CORS, database connections, and static hosting handled automatically
```javascript
import { app } from 'codehooks-js';
import { z } from 'zod';
const Todo = z.object({
title: z.string().min(1),
completed: z.boolean().default(false)
});
// One line: full CRUD API with Zod validation
app.crudlify({ todos: Todo }, { prefix: '/api' });
// Serve a React SPA from the same deployment
app.static({ route: '/', directory: '/static', default: 'index.html' });
export default app.init();
```
```bash
# The full deploy cycle — what your AI agent runs
coho deploy # Backend live in ~5 seconds
coho log -f # Stream logs to verify
```
**A note on lock-in**: Yes, using Codehooks represents a (tiny) vendor lock-in, but not more than you could replace in an afternoon with Node.js, Redis, and MongoDB. The `codehooks-js` library is essentially a convenient wrapper around standard web APIs and database operations. Your business logic remains portable, and the abstractions map directly to common patterns any Node.js developer would recognize.
Think of Codehooks as the reliable friend who handles logistics while you focus on the creative work. When v0 generates your frontend and Cursor refines your logic, Codehooks ensures your backend never becomes a bottleneck.
---
## How to Evaluate Vibe Coding Tools
The best vibe coding tools are deeply personal. What feels intuitive to one developer might feel clunky to another. Here's how to evaluate whether a tool will actually improve your workflow:
### The 15-Minute Test
Can you get meaningfully productive within 15 minutes of starting? Great vibe tools have exceptional onboarding. If you're fighting with configuration or unclear documentation in the first quarter-hour, it's probably not going to get better.
### Integration Reality Check
How well does it play with your existing stack? The most powerful individual tool becomes useless if it doesn't integrate with your current workflow. Look for:
- **API compatibility**: Can it talk to your existing services?
- **Data export**: Can you get your work out if needed?
- **Tool chain fit**: Does it complement your current IDE, deployment, and collaboration tools?
### Flow State Test
Does the tool maintain or break your coding flow? Pay attention to:
- **Context switching**: How often does it force you to leave your main coding environment?
- **Interruption patterns**: Does it suggest at the right moments or constantly distract?
- **Learning curve**: Does it get better as you use it more, or stay equally difficult?
### The "Saturday Project" Standard
The ultimate test: Could you use this tool to build and deploy a weekend project without getting frustrated? If a tool can handle the full cycle from idea to production smoothly, it's probably vibe-friendly.
## The Future of Vibe Coding
As AI becomes more prevalent in development, we're seeing tools that adapt to individual coding styles and preferences. The tools we've covered are just the beginning. The next wave will likely include:
- **Cross-tool vibe awareness**: Your Cursor session influencing your Codehooks deployment suggestions, or v0 components automatically integrating with your existing API patterns
- **Energy-based interfaces**: Tools that detect when you're in deep-focus mode vs. exploratory mode and adjust their suggestions accordingly
- **Contextual deployment intelligence**: Platforms that understand not just what you're building, but why and how you prefer to ship
- **Collaborative vibe matching**: Team tools that adapt to the collective energy and working style of your entire dev team
The MCP protocol adoption we're seeing in tools like Codehooks and Cursor suggests a future where vibe coding tools talk to each other, creating seamless workflows that feel almost telepathic.
## Building Your Vibe
The ultimate vibe coding setup is one that feels invisible – tools that amplify your capabilities without demanding attention. The key is understanding that different tools serve different roles in your vibe stack.
**Adaptive development tools** like Cursor, Claude, and v0 adapt to your coding style and energy level. They're the ones that "feel" your mood and adjust their suggestions accordingly, whether you're working on frontend, backend, or full-stack code.
**Backend infrastructure** like Codehooks and Supabase is different – it's the invisible foundation that makes vibe coding possible. When your backend "just works" without configuration, deployment headaches, or infrastructure management, your adaptive AI tools can focus on the creative work. These platforms don't adapt to your coding style; instead, they eliminate the friction that would break your vibe in the first place.
Think of it as the difference between responsive collaborators and reliable foundations. You need both: AI tools that understand your energy and infrastructure that never interrupts your flow.
Remember: the goal isn't to use every trending tool, but to curate a toolkit where frontend intelligence meets backend simplicity. Your vibe coding setup should make you excited to open your laptop and start building.
What's your current coding vibe, and what tools are helping you achieve it? The beauty of vibe coding tools is that they're as unique as the developers who use them.
Start small: pick one category to focus on first and build from there. Maybe begin with a vibe-friendly code editor like Cursor, then add backend infrastructure that doesn't break your flow. The goal isn't to use every tool, but to curate a setup that amplifies your natural coding rhythm.
---
_Ready to experience vibe-friendly backend development? [Try Codehooks.io](https://codehooks.io) and discover why developers love having their infrastructure just work. When your backend vibes with your workflow, everything else falls into place._
---
## API vs REST API: Simple Guide with Clear Differences and Examples
*(Note: This content contains MDX/JSX code)*
# API vs REST API: Beginner-Friendly Guide
An API is an interface that lets software communicate by defining how to make requests and receive responses. A REST API is a specific kind of API that follows REST principles over HTTP—using methods like GET, POST, PUT, and DELETE to work with resources. All REST APIs are APIs, but not all APIs are REST.
Confused? Don't worry. In this post, you'll learn everything about the **API and REST API domain**—from core concepts and key differences to how REST+JSON became the web's standard. We'll explore why REST APIs dominate today's development landscape, compare different backend service models, and finally show you how to **build your own APIs in practice** using [Codehooks.io](https://codehooks.io).
## API vs REST API (Difference)
| Topic | API (general) | REST API |
| ----------- | ------------------------------------- | --------------------------------- |
| Definition | Interface for software to communicate | API that follows REST constraints |
| Protocols | Any (HTTP, gRPC, SOAP, SDK) | HTTP with standard methods |
| Data format | Any (JSON, XML, binary) | Usually JSON |
| URLs | Not required | Resource URLs like `/users/123` |
| Examples | GraphQL API, SOAP service, SDK calls | GitHub REST API, Twitter REST API |

## What is an API?
**[API](https://en.wikipedia.org/wiki/API)** stands for **Application Programming Interface**.
Think of an API as a _messenger_ that allows two different applications to communicate. For example:
- A payment API processes transactions.
- A maps API provides geolocation and routing data.
- A messaging API sends SMS or chat messages.
APIs hide complexity and give developers clean, documented entry points. Instead of dealing with internal systems directly, you simply make calls to the API and get back data or results.
:::note The menu analogy
A textbook classic: think of an API as a restaurant menu.
- The kitchen = the service
- The menu = the API
- The waiter = the request/response carrying orders and results
You don’t need to know how the kitchen works; the menu tells you what you can request and how you’ll get it back.
:::
---
## What is a REST API?
A REST API is an API designed around REST (Representational State Transfer): a resource‑oriented way to use HTTP so clients and servers can interact predictably and at scale.
REST constraints (plain English):
- Client–server: UI and data/service concerns are separated.
- Stateless: each request includes everything needed; no server session.
- Cacheable: responses indicate if they can be cached.
- Uniform interface:
- Resource identifiers: stable URLs like `/users/123`.
- Representations: the same resource can be sent as JSON/XML (or other formats), negotiated with `Accept`/`Content-Type`.
- Standard methods: `GET` (read), `POST` (create), `PUT` (replace), `PATCH` (partial update), `DELETE` (delete).
- Self‑descriptive messages: status codes and headers explain how to handle responses.
- Hypermedia (optional): responses can include links to related actions.
- Layered system: proxies/gateways/CDNs can sit between client and server.
In practice, REST models “things” (resources) with nouns, exposes them at URLs, and manipulates them with standard HTTP methods, returning machine‑readable representations (often JSON) plus meaningful status codes.
:::info Under the hood: A simple HTTP POST
If you're new to HTTP, this is the text which is actually sent and received when you POST a new todo resource. The client here is actually the `curl` command (as you can see from the User-Agent header).
Request (create a new todo resource with HTTP POST)
```
POST https://.api.codehooks.io/dev/api/todos HTTP/1.1
Host: .api.codehooks.io
Authorization: Bearer
Content-Type: application/json
Accept: application/json
User-Agent: curl/8.6.0
{
"title": "Read about REST",
"done": false
}
```
Response
```
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Cache-Control: no-store
Date: Tue, 24 Sep 2025 10:15:32 GMT
{
"_id": "66f2c47b7f7b4d7f9d2d5a41",
"title": "Read about REST",
"done": false,
"createdAt": "2025-09-24T10:15:32.421Z"
}
```
### HTTP message anatomy
Request
- Method: action to perform (POST, GET, PUT, PATCH, DELETE). Example: POST
- URL: scheme + host + path (+ optional query). Example: `https://.api.codehooks.io/dev/api/todos`
- Headers: metadata (Authorization, Content-Type, Accept, User-Agent)
- Body: payload sent to the server (often JSON), e.g. `{ "title": "Read about REST", "done": false }`
Response
- Status line: protocol + status code + reason, e.g. HTTP/1.1 201 Created
- Headers: response metadata (Content-Type, Cache-Control, Date, ETag, Location)
- Body: representation returned (often JSON), e.g. `{ "_id": "...", "title": "Read about REST", ... }`
:::
---
## Why REST APIs Matter for Developers
Developers love REST APIs because they’re:
- **Easy to use:** No special tooling required, just HTTP requests.
- **Interoperable:** Works across languages, platforms, and devices.
- **Well-documented:** Most modern APIs publish REST endpoints and examples.
- **Flexible:** Great for microservices, integrations, and mobile/web apps.
That’s why REST APIs are everywhere—from social media and e-commerce to finance and IoT.
---
## REST API best practices
- Use resource nouns: `/users`, `/orders/:id`
- Correct status codes: `200`, `201`, `204`, `400`, `401`, `404`, `429`, `500`
- Idempotency (repeatable w/ same result) for `PUT` and `DELETE`; keep `GET` safe
- Pagination and filtering: `?limit=20&cursor=`
- Caching: `ETag`, `Cache-Control`
- Authentication: `Authorization: Bearer `
### Common mistakes to avoid
- Using verbs in URLs (e.g., `/createUser`) instead of resource nouns (`/users`)
- Returning `200 OK` for all outcomes instead of specific status codes
- Confusing `PUT` (replace) with `PATCH` (partial update)
- Omitting pagination and caching headers on list endpoints
## When to use REST — and when not to
- Use REST for simple, interoperable HTTP CRUD with many clients.
- Prefer GraphQL for complex graphs/over‑fetching issues.
- Prefer gRPC for low‑latency, strongly typed service‑to‑service traffic.
- SOAP remains for some legacy enterprise contracts.
---
## Why REST and JSON Dominate the Integration Landscape
Over the past decade, **REST combined with JSON** has become the default way for applications, platforms, and services to integrate. There are several reasons for this dominance:
1. **Ubiquity of HTTP:** REST leverages the existing web infrastructure. Every language, framework, and platform can make HTTP requests without special libraries or protocols.
2. **Lightweight data format:** [JSON](https://www.json.org/) is human-readable, compact, and easy to parse. Unlike [XML](https://en.wikipedia.org/wiki/XML), it doesn't require heavy markup, and most modern languages have built-in JSON support.
3. **Developer experience:** JSON maps naturally to data structures in [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript), [Python](https://www.python.org/), [Java](https://www.oracle.com/java/), [Go](https://golang.org/), and many others. This makes it faster to prototype and reduces the risk of errors.
4. **Performance and scalability:** JSON over HTTP is efficient enough for web-scale applications, while being simpler to cache, debug, and distribute.
5. **Ecosystem adoption:** Nearly all major APIs—from [Twitter](https://developer.twitter.com/en) and [GitHub](https://docs.github.com/en/rest) to [AWS](https://aws.amazon.com/api-gateway/) and [Google](https://developers.google.com/)—offer REST+JSON endpoints. This sets a standard expectation for developers.
6. **Interoperability:** REST and JSON work well across web, mobile, IoT, and backend systems. A single API can serve many clients with minimal effort.
In short, REST and JSON became the industry standard because they hit the sweet spot: **simple, universal, and good enough** for most integration scenarios.
---
## A Brief History: SOAP → REST → GraphQL
To understand REST’s dominance, it helps to see where it came from:
- **[SOAP (Simple Object Access Protocol)](https://en.wikipedia.org/wiki/SOAP):** Popular in the early 2000s. XML-based, strict, and very verbose. Great for enterprise but heavy and complex.
- **[REST (Representational State Transfer)](https://en.wikipedia.org/wiki/Representational_state_transfer):** Emerged as a lighter alternative. Uses simple HTTP methods and resource-based URLs. Easy to adopt, scales with the web, and fits developer workflows.
- **[JSON](https://www.json.org/) as the data format:** Around the same time, JSON rose as the natural fit for JavaScript-heavy web development. REST+JSON became the new norm.
- **[GraphQL](https://graphql.org/) (2015+):** Introduced by [Facebook](https://about.meta.com/), GraphQL gives clients more flexibility in querying data. It's powerful but requires more tooling and isn't as widely adopted outside certain use cases.
Despite newer entrants like GraphQL or [gRPC](https://grpc.io/), **REST and JSON remain dominant** because they're simple, universal, and "good enough" for most integrations.
---
As REST APIs became the standard for integration, developers needed better ways to build and host them. This led to the rise of specialized cloud services that handle the heavy lifting of API development and deployment.
Let's look at how different backend service models support API development, and what that means for your tech stack choices.
## BaaS vs DBaaS: How They Differ in API Support
When choosing where to build your APIs, you’ll often encounter **[Backend-as-a-Service (BaaS)](https://en.wikipedia.org/wiki/Mobile_backend_as_a_service)** and **[Database-as-a-Service (DBaaS)](https://en.wikipedia.org/wiki/Cloud_database)** platforms. Both help you move faster, but they differ in what they provide out of the box.
| Category | Popular Examples | What You Get | API Support |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| **BaaS (Backend-as-a-Service)** | [Firebase](https://firebase.google.com/), [Supabase](https://supabase.com/), [Codehooks.io](https://codehooks.io/) | Full backend services: auth, database, serverless functions, hosting, queues, workflows | Rich API support, often automatic REST/GraphQL endpoints for data + custom logic APIs |
| **DBaaS (Database-as-a-Service)** | [MongoDB Atlas](https://www.mongodb.com/atlas), [AWS DynamoDB](https://aws.amazon.com/dynamodb/), [PlanetScale](https://planetscale.com/) | Managed database only: scaling, replication, backups | APIs are database-centric; usually SDKs or query APIs, REST/GraphQL APIs must be built separately |
### Key Differences:
- **BaaS:** Gives you batteries-included development. You store data and instantly get REST or GraphQL APIs to interact with it, often with integrated auth, security, and triggers. Perfect for rapid app development.
- **DBaaS:** Focuses only on providing a managed database. You still need to build and expose your own API layer using frameworks or services. Better if you want total control but slower for prototyping.
For most developers looking to build modern apps quickly, **BaaS solutions with strong API support** are the better starting point.
---
## Building REST APIs with Codehooks.io
Traditionally, building a REST API means:
- Setting up a server
- Configuring a database
- Writing boilerplate code
- Handling authentication and scaling
With **Codehooks.io**, you can skip all that.
Codehooks.io gives you:
- **Instant [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) REST APIs** backed by a serverless [NoSQL](https://en.wikipedia.org/wiki/NoSQL) datastore
- **Serverless functions** for custom business logic
- **Authentication, queues, cron jobs, and workflows** out of the box
Here’s how you can create a REST API in seconds:
```javascript
import { app } from 'codehooks-js';
// Use Crudlify to create a CRUD REST API for any database collection
app.crudlify();
export default app.init();
```
Deploy it, and you instantly have endpoints like:
- `GET /api/todos` → list todos
- `POST /api/todos` → create a new todo
- `PUT /api/todos/:id` → update a todo
- `DELETE /api/todos/:id` → delete a todo
👉 That’s a fully functional REST API without servers, frameworks, or database setup.
---
## Related guides
- [NoSQL Database REST API](/docs/database-rest-api)
- [Quickstart: CLI](/docs/quickstart-cli)
- [Core concepts: serverless functions, queues, workflows](/docs/concepts)
- [API Integration: Meaning, Tools, and Step-by-Step Guide with Examples](/blog/api-integration-made-easy)
- [API Cheat Sheet](/docs/apicheatsheet)
- [REST API Routing](/docs/rest-api-app-routes)
---
import FAQSection from '@site/src/components/FAQSection';
---
## References
- [Fielding: Architectural Styles and the Design of Network-based Software Architectures](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm)
- [IBM: What is a REST API?](https://www.ibm.com/think/topics/rest-apis)
- [MDN Web Docs: HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
---
## Summary
- **API** = general concept (how apps talk to each other).
- **REST API** = the most common type, built on REST principles.
- **REST + JSON** = the winning combo for integrations: simple, universal, and efficient.
- **History:** REST+JSON displaced SOAP and still dominates despite GraphQL/gRPC alternatives.
- **BaaS vs DBaaS:** choose BaaS for rapid API development, DBaaS if you want database-only control.
- For developers, REST APIs are the backbone of modern apps and integrations.
If you want to **experiment, prototype, or launch production-ready REST APIs fast**, try [Codehooks.io](https://account.codehooks.io/login?signup). You can deploy your first REST API in minutes—without servers, frameworks, or endless configuration.
---
## Building Webhook-Enabled LLM Workflows in JavaScript with Codehooks.io
*(Note: This content contains MDX/JSX code)*
Most AI projects don't need a fleet of orchestration tools to run a few prompt chains. The real work is state management, retries, scheduling, and simple persistence—the operational glue between LLM API calls.
This post shows you how to build a production-ready **text summarization workflow** using Codehooks.io, the Workflow API, and OpenAI. You'll learn:
- **OpenAI API integration patterns**: Error handling, retries, rate limiting, and cost optimization
- **Workflow state management**: Building reliable multi-step processes with caching and persistence
- **Webhook triggers**: Event-driven workflows that respond to GitHub issues (and other external services)
- **Programmatic access**: How to trigger and manage workflows via REST API, CLI, and webhooks
By the end, you'll have a working summarizer that caches results, stores them in a NoSQL database, and can be triggered via REST API or GitHub webhooks—all deployed with a single command.
Here's the workflow we'll build:
```mermaid
%%{init: {'themeVariables': { 'edgeLabelBackground': 'transparent'}}}%%
flowchart TD
A[POST /summaries] --> B[Start]
W[Webhook POST /webhook/summarize] --> B
B --> C[Check cache]
C -->|hit| F[Finish - cached]
C -->|miss| E[OpenAI]
E --> G[Cache]
G --> H[Store]
H --> I[Finish]
style A fill:#1565c0,stroke:#0d47a1,stroke-width:2px
style W fill:#1565c0,stroke:#0d47a1,stroke-width:2px
style B fill:#2e7d32,stroke:#1b5e20,stroke-width:2px
style C fill:#e65100,stroke:#bf360c,stroke-width:2px
style F fill:#c62828,stroke:#b71c1c,stroke-width:2px
style E fill:#7b1fa2,stroke:#4a148c,stroke-width:2px
style G fill:#00838f,stroke:#006064,stroke-width:2px
style H fill:#00838f,stroke:#006064,stroke-width:2px
style I fill:#c62828,stroke:#b71c1c,stroke-width:2px
```

## The messy reality of LLM pipelines (the problems)
If you've built more than one "simple" AI script, you've likely met these issues:
1. **State between steps** – You call an LLM, call another API, then need to combine/compare results. Where do you keep ephemeral state? How do you re-run a step without duplicating side effects?
2. **Retries and idempotency** – A single transient 500 from a model provider shouldn't break the whole chain or duplicate writes.
3. **OpenAI API integration complexity** – Rate limits, token counting, model selection, error handling, and cost management add layers of complexity beyond simple HTTP calls.
4. **Scheduling and triggers** – Many pipelines aren't one-shot. They run hourly, daily, or react to external events via webhooks from services like GitHub, Slack, or Stripe.
5. **Persistence** – You need a place to store inputs/outputs, cache expensive calls, and expose results to other services.
6. **Operational sprawl** – A script becomes a service; a service becomes a DAG; suddenly you're maintaining queues, workers, YAML, and dashboards just to run a few prompts.
7. **Language/tool drift** – Frameworks like **LangChain**, **LlamaIndex**, **Haystack**, or workflow engines like **Airflow/Prefect/Celery** are powerful, but they assume extra infrastructure and a Python-first stack. That’s perfect for some teams, but overkill for many web and product engineers who just want reliable workflows in JavaScript.
These are engineering problems, not “AI problems.” The question is: **what’s the lightest reliable runtime** that gives you state, retries, scheduling, and storage — without spinning up a new platform?
---
## The middle layer we actually want
Between prompt-chaining libraries and heavyweight workflow engines, there’s a practical middle:
- **Functions as steps**, with a small state machine to move between them
- **Built-in storage** for docs and key–value cache
- **Scheduling** (cron-like) and webhook triggers for external events
- **HTTP + webhook endpoints** to start/inspect runs
- **One deploy**
That’s the layer Codehooks provides. You write JavaScript; the platform handles routing, persistence, and workflow state. No extra orchestrator.
> Useful links: [Workflow API](/docs/workflow-api) · [AI agent setup](/docs/ai-agent-setup)
---
## OpenAI API Integration Patterns
Before diving into examples, let's establish solid patterns for OpenAI API integration in production workflows.
### Error Handling & Retries
A reusable helper with retries and exponential backoff:
```js
const callOpenAIWithRetry = async (messages, options = {}, maxRetries = 3) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(
'https://api.openai.com/v1/chat/completions',
{
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: options.model || 'gpt-4o-mini',
messages,
temperature: options.temperature ?? 0.3,
max_tokens: options.max_tokens ?? 300,
}),
}
);
if (response.status === 429) {
// Rate limit - exponential backoff and retry
const delay = Math.pow(2, attempt) * 1000;
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
}
if (!response.ok) {
const error = await response.text().catch(() => 'Unknown error');
throw new Error(`OpenAI API error ${response.status}: ${error}`);
}
const data = await response.json();
return data?.choices?.[0]?.message?.content?.trim() || '';
} catch (error) {
if (attempt === maxRetries) throw error;
// Wait before retry for network errors
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
};
```
### Cost Optimization
- **Model selection**: Use `gpt-4o-mini` for most tasks (significantly cheaper than GPT‑4 while still very capable)
- **Token management**: Set appropriate `max_tokens` limits
- **Caching**: Cache responses for identical inputs (shown in example below)
- **Batch processing**: Group similar requests when possible
### Rate Limiting
OpenAI rate limits depend on your account, model, and tier. They also change over time.
Best practices:
- Treat **HTTP 429** as a signal to back off and retry
- Implement **exponential backoff** (as in the helper above)
- Monitor limits and usage in the OpenAI dashboard
- Prefer more efficient models (like `gpt-4o-mini`) where possible
### Security
- Store API keys as encrypted environment variables
- Use different keys for different environments
- Monitor usage and set spending limits in OpenAI dashboard
---
## Example — Summarizer with cache and storage
We'll build a small workflow that accepts text, checks a cache, calls OpenAI, stores the result, and returns it. We’ll also add a webhook endpoint that can summarize GitHub issues and comments.
### Setup
```bash
npm install -g codehooks
coho create aipipeline
cd aipipeline
npm install codehooks-js node-fetch
coho set-env OPENAI_API_KEY 'sk-******' --encrypted
# Optional: for webhook signature verification (recommended for production)
coho set-env GITHUB_WEBHOOK_SECRET 'your-webhook-secret' --encrypted
```
### `index.js`
Replace the default `index.js` created by `coho create` with this workflow code (all in one file for simplicity):
```js
import { app, Datastore } from 'codehooks-js';
import fetch from 'node-fetch';
import crypto from 'crypto';
// Reusable OpenAI helper with retries and exponential backoff
async function callOpenAIWithRetry(messages, options = {}, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(
'https://api.openai.com/v1/chat/completions',
{
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: options.model || 'gpt-4o-mini',
messages,
temperature: options.temperature ?? 0.3,
max_tokens: options.max_tokens ?? 300,
}),
}
);
if (response.status === 429) {
// Rate limit - exponential backoff and retry
const delay = Math.pow(2, attempt) * 1000;
if (attempt < maxRetries) {
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
}
if (!response.ok) {
const error = await response.text().catch(() => 'Unknown error');
throw new Error(`OpenAI API error ${response.status}: ${error}`);
}
const data = await response.json();
return data?.choices?.[0]?.message?.content?.trim() || '';
} catch (error) {
if (attempt === maxRetries) throw error;
// Wait before retry for network or transient errors
await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
}
}
}
// Helper function to verify GitHub webhook signature using raw body
function verifyGitHubSignature(rawBody, signatureWithPrefix, secret) {
if (!signatureWithPrefix || !secret) return false;
if (!signatureWithPrefix.startsWith('sha256=')) return false;
const signature = signatureWithPrefix.slice('sha256='.length);
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Use timingSafeEqual to avoid timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Workflow: summarize text and store
const summarizeWorkflow = app.createWorkflow(
'summarize',
'Summarize text using OpenAI and store the result',
{
start: async (state, goto) => {
if (!state?.text) return goto(null, { error: 'Missing text input' });
goto('checkCache', { ...state, steps: ['start'] });
},
checkCache: async (state, goto) => {
const db = await Datastore.open();
// Use MD5 hash of entire text for proper cache key
const key = `summary:${crypto
.createHash('md5')
.update(state.text)
.digest('hex')}`;
const cached = await db.get(key);
const newState = {
...state,
steps: [...(state.steps || []), 'checkCache'],
};
// If cached, skip to finish without storing duplicate
if (cached) {
return goto('finish', {
...newState,
summary: cached,
cached: true,
});
}
goto('callOpenAI', newState);
},
callOpenAI: async (state, goto) => {
const newState = {
...state,
steps: [...(state.steps || []), 'callOpenAI'],
};
// OpenAI API call via the reusable helper
const summary = await callOpenAIWithRetry(
[
{ role: 'system', content: 'You are a helpful assistant.' },
{ role: 'user', content: `Summarize this:
${state.text}` },
],
{
model: 'gpt-4o-mini', // Cost-effective model for summarization
temperature: 0.3, // Lower temperature for consistent summaries
max_tokens: 300, // Limit tokens to control costs
}
);
goto('cacheAndStore', { ...newState, summary });
},
cacheAndStore: async (state, goto) => {
const db = await Datastore.open();
// Use MD5 hash for consistent cache key
const key = `summary:${crypto
.createHash('md5')
.update(state.text)
.digest('hex')}`;
// TTL is in milliseconds – 60 * 1000 = 60 seconds
await db.set(key, state.summary, { ttl: 60 * 1000 });
goto('store', {
...state,
steps: [...(state.steps || []), 'cacheAndStore'],
});
},
store: async (state, goto) => {
const db = await Datastore.open();
const doc = await db.insertOne('summaries', {
text: state.text,
summary: state.summary,
cached: !!state.cached,
source: state.source || 'api',
repository: state.repository || null,
createdAt: new Date().toISOString(),
});
goto('finish', {
...state,
...doc,
steps: [...(state.steps || []), 'store'],
});
},
finish: async (state, goto) => {
const steps = [...(state.steps || []), 'finish'];
console.log('Workflow finished');
console.log('WorkflowId:', state._id || 'N/A');
console.log('Steps executed:', steps.join(' → '));
goto(null, { ...state, steps });
},
}
);
// HTTP endpoints
// Direct API trigger for summarization
app.post('/summaries', async (req, res) => {
const result = await summarizeWorkflow.start({ text: req.body?.text });
res.json(result);
});
// Fetch stored summaries
app.get('/summaries', async (req, res) => {
const db = await Datastore.open();
const items = await db
.getMany('summaries', {}, { sort: { createdAt: -1 }, limit: 10 })
.toArray();
res.json(items);
});
// Webhook endpoint - trigger summarization from GitHub issues, comments, or generic payloads
app.post('/webhook/summarize', async (req, res) => {
// Verify webhook signature for security (GitHub-style HMAC)
if (process.env.GITHUB_WEBHOOK_SECRET) {
const signature = req.headers['x-hub-signature-256'];
const ok = verifyGitHubSignature(
req.rawBody,
signature,
process.env.GITHUB_WEBHOOK_SECRET
);
if (!ok) {
return res.status(401).json({ error: 'Invalid signature' });
}
}
// Extract text from GitHub issue webhook payload
// Supports: issue opened, issue comment, pull request, or generic text
let text = '';
let source = 'webhook';
if (req.body.issue) {
// GitHub issue or PR
text = `${req.body.issue.title}
${req.body.issue.body || ''}`;
source = `github-issue-${req.body.issue.number}`;
} else if (req.body.comment) {
// GitHub issue/PR comment
text = req.body.comment.body;
source = `github-comment-${req.body.comment.id}`;
} else {
// Generic fallback for other webhook providers
text = req.body?.content || req.body?.text || req.body?.message || '';
}
if (!text || text.trim().length === 0) {
return res
.status(400)
.json({ error: 'No text content in webhook payload' });
}
// Start workflow asynchronously
const result = await summarizeWorkflow.start({
text: text.trim(),
source,
repository: req.body.repository?.full_name || 'unknown',
});
// Return a lightweight response while the workflow runs in the background
res.json({ status: 'processing', workflowId: result._id });
});
export default app.init();
```
> In Codehooks, `req.rawBody` contains the unparsed request body, which is required for correct webhook signature verification with providers like GitHub.
### Deploy and get your endpoint
Now deploy your code and get your API endpoint:
```bash
$ coho deploy
Project: aipipeline-4cqe Space: dev
Deployed Codehook successfully! 🙌
Check your logs with: coho logs --follow
```
:::tip Finding your API endpoint and token
The `coho info` command shows your deployment details and if you add the `--examples` parameter it also provides example cURL commands. Here's what to look for:
```
$ coho info --examples
Project name: aipipeline-4cqe
Team: Amy Jones (personal account)
API endpoints:
https://funny-wizard-xyz9.codehooks.io
https://aipipeline-4cqe.api.codehooks.io/dev
https://api.example.com
Examples in cURL:
curl -H 'x-apikey: 73c53e83-c8b5-4b5f-8452-65ebca859ac1' -H 'Content-Type: application/json' https://funny-wizard-xyz9.codehooks.io/users
curl -H 'x-apikey: 73c53e83-c8b5-4b5f-8452-65ebca859ac1' -H 'Content-Type: application/json' --data-raw '{"name": "Mr. Code Hooks", "active": true }' --request POST https://funny-wizard-xyz9.codehooks.io/users
Spaces:
┌──────────────┬────────────────────────────────────────────┬───────────────┬──────┬────────────────────────────┐
│ Name │ Tokens │ Allowed Hosts │ Jwks │ Env │
├──────────────┼────────────────────────────────────────────┼───────────────┼──────┼────────────────────────────┤
│ dev (active) │ xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (RW) │ │ │ OPENAI_API_KEY=(encrypted) │
└──────────────┴────────────────────────────────────────────┴───────────────┴──────┴────────────────────────────┘
```
**API Endpoint**: Use any of the URLs listed. The first one is a unique random name for the space. The second one (`https://aipipeline-4cqe.api.codehooks.io/dev`) includes the project name and space name. The third one is a custom domain you can set up for your project.
**API Token**: Copy one of the tokens from the **Tokens** column in the Spaces table (the UUID values marked with RW for read-write access). Use this token as the value for the `x-apikey` header in your API calls.
:::
### Testing the workflow
You can test the workflow using cURL commands, a reusable shell script, or API clients like **[Bruno](https://www.usebruno.com/)**, **[Postman](https://www.postman.com/)**, or **[Insomnia](https://insomnia.rest/)**.
#### Option 1: Quick tests with cURL
Replace the URL and token with your values from `coho info`:
```bash
# Start a summarization
curl -X POST https://aipipeline-4cqe.api.codehooks.io/dev/summaries \
-H "Content-Type: application/json" \
-H "x-apikey: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
-d '{
"text": "Artificial intelligence is transforming software development. Modern AI tools help developers write code faster, debug more efficiently, and build smarter applications. From code completion to automated testing, AI is becoming an essential part of the developer toolkit."
}'
# Response example:
# {
# "_id": "abc123...",
# "text": "Artificial intelligence is transforming...",
# "summary": "AI tools are revolutionizing software development by enhancing code writing, debugging, and application intelligence.",
# "cached": false,
# "source": "api",
# "repository": null,
# "createdAt": "2025-11-15T10:30:00.000Z"
# }
# Fetch stored summaries
curl https://aipipeline-4cqe.api.codehooks.io/dev/summaries \
-H "x-apikey: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Trigger via webhook (simulating GitHub issue webhook)
curl -X POST https://aipipeline-4cqe.api.codehooks.io/dev/webhook/summarize \
-H "Content-Type: application/json" \
-d '{
"action": "opened",
"issue": {
"number": 123,
"title": "Application crashes on startup",
"body": "When I run the application, it immediately crashes with error code 500. This happens on both macOS and Linux."
},
"repository": {
"full_name": "myorg/myrepo"
}
}'
```
#### Option 2: API clients ([Bruno](https://www.usebruno.com/), [Postman](https://www.postman.com/), [Insomnia](https://insomnia.rest/))
For a more visual testing experience, use API clients. All three support importing cURL commands - just copy any of the cURL examples from Option 1 above and use the "Import from cURL" feature in your preferred tool.
#### Option 3: Reusable shell script
For easier repeated testing, create a `summarize.sh` script that handles text via argument, stdin, or uses sample text:
```bash
#!/bin/bash
PROJECTNAME=your-project-name
API_KEY="your-api-key"
# Check if text is provided as argument
if [ -n "$1" ]; then
TEXT="$1"
echo "Original text length: ${#TEXT} characters"
elif [ ! -t 0 ]; then
# Read from stdin if available (not a TTY)
TEXT=$(cat)
echo "Read from stdin, length: ${#TEXT} characters"
else
# Use a sample text
TEXT="Artificial intelligence is transforming software development..."
echo "Using sample text"
fi
# Escape the text for JSON
TEXT_ESCAPED=$(echo "$TEXT" | jq -Rs .)
curl -X POST https://${PROJECTNAME}.api.codehooks.io/dev/summaries \
-H "Content-Type: application/json" \
-H "x-apikey: $API_KEY" \
-d "{\"text\": $TEXT_ESCAPED}"
```
**Usage examples:**
```bash
# Make it executable
chmod +x summarize.sh
# Use sample text
./summarize.sh
# Provide text as argument
./summarize.sh "Your custom text here"
# Read from file via stdin
cat article.txt | ./summarize.sh
./summarize.sh < article.txt
# Test with long texts without truncation
echo "Very long text content..." | ./summarize.sh
```
:::tip Handling long texts
When testing with long texts, **always use stdin** (piping or redirection) rather than command-line arguments. Shell arguments have length limits that can truncate your text. The script automatically detects the input method and reports text length before sending.
:::
### Monitoring workflow execution
After running your workflows, you can inspect their execution status with live updates:
```bash
# One-time snapshot
coho workflow-status
# Live updates (recommended) - updates every 2 seconds
coho workflow-status --follow
```
:::info Workflow observability with live updates
The `coho workflow-status --follow` command provides **live monitoring** of your workflows, updating every 2 seconds. This is invaluable for watching workflows execute in real-time:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Workflow Status (Live) - Press Ctrl+C to exit
Last updated: 20:22:08 | Next update in 2s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Workflow Report
===============
Total workflow instances: 4
Unique steps: 6
Workflow Structure (6 steps):
1. start
2. checkCache
3. callOpenAI
4. cacheAndStore
5. store
6. finish
Current State Distribution:
start checkCache callOpenAI cacheAndStore store finish
------------ -------------- -------------- ----------------- ------------ ------------
1 0 1 0 0 2
```
**What this tells you:**
- **Live updates**: See workflows progress through steps in real-time
- **Step sequence**: The actual execution path through your workflow
- **Total instances**: How many times the workflow has been triggered
- **State distribution**: Where each workflow instance ended up (all 2 completed at `finish` in this example)
- **Debugging**: If workflows get stuck, you'll see them stopped at intermediate steps
- **Cache verification**: Watch the difference between cached (start → checkCache → finish) and uncached (full path) executions
This visibility is crucial for debugging production workflows and understanding execution patterns. Use `--follow` during development and testing to see your workflows in action!
:::
### Why this solves real problems
- **State** lives in `state` and Datastore; steps remain small and testable.
- **Retries** can be added per step (re-run from `callOpenAI` without duplicating `store`).
- **Scheduling** is one line with `app.job()` if you want periodic summaries.
- **Webhooks** let you react to external events like GitHub issues without polling.
### Key implementation details
**MD5 hash for cache keys**: Using `crypto.createHash('md5').update(state.text).digest('hex')` creates a unique, consistent cache key based on the entire text content.
**Proper cache flow**: When a cached summary is found, the workflow goes directly to the `finish` step instead of creating a duplicate database record. This prevents storing the same summary multiple times while still allowing cache hits to avoid expensive OpenAI API calls. The TTL is set to 60 seconds, so the summary will be refreshed after 60 seconds. This is a trade-off between freshness and cost. You can change the TTL to your needs.
**Finish step with tracking**: Every workflow path ends at a `finish` step that logs:
- The workflowId for debugging
- The execution path taken (e.g., `start → checkCache → finish` for cached vs `start → checkCache → callOpenAI → cacheAndStore → store → finish` for uncached)
This makes it easy to verify caching is working and debug any issues.
**Step tracking**: Each step adds itself to a `steps` array in the state, providing full visibility into the workflow execution path. This is invaluable for debugging and understanding how workflows behave in production.
### Extending to multi-step workflows
To add more LLM calls (e.g., classify → summarize → sentiment analysis), simply add more steps to the workflow. Each step receives the state from the previous step and can add new properties before calling `goto()`:
```js
classify: async (state, goto) => {
// Call OpenAI for classification
const category = await callOpenAIWithRetry(
[{ role: 'user', content: 'Classify this: ' + state.text }],
{ model: 'gpt-4o-mini' }
);
goto('summarize', {
...state,
category,
steps: [...(state.steps || []), 'classify'],
});
},
summarize: async (state, goto) => {
// Use category in the prompt
const summary = await callOpenAIWithRetry(
[
{
role: 'user',
content: `Summarize this ${state.category} text: ${state.text}`,
},
],
{ model: 'gpt-4o-mini' }
);
goto('sentiment', {
...state,
summary,
steps: [...(state.steps || []), 'summarize'],
});
},
sentiment: async (state, goto) => {
const sentiment = await callOpenAIWithRetry(
[{ role: 'user', content: 'Analyze sentiment: ' + state.text }],
{ model: 'gpt-4o-mini' }
);
goto('store', {
...state,
sentiment,
steps: [...(state.steps || []), 'sentiment'],
});
},
```
The pattern remains the same regardless of how many steps you chain together. Split steps into separate files for larger workflows.
---
## Webhook-Triggered Workflows
Webhooks enable event-driven AI workflows. External services (GitHub, Slack, Stripe) can trigger workflows automatically when events occur — no polling required.
### Key Benefits
- **Real-time processing**: Trigger AI analysis immediately when data becomes available
- **Integration-friendly**: Most SaaS platforms support outbound webhooks
- **Efficient**: Event-driven beats constant polling
### Example: GitHub Issue Triage (variation)
Here’s a variation on the previous webhook example, where a workflow triages newly opened issues. You can reuse the same `verifyGitHubSignature` helper from the main `index.js` example above.
```js
app.post('/webhook/github', async (req, res) => {
// Verify signature using req.rawBody
if (process.env.GITHUB_WEBHOOK_SECRET) {
const signature = req.headers['x-hub-signature-256'];
const ok = verifyGitHubSignature(
req.rawBody,
signature,
process.env.GITHUB_WEBHOOK_SECRET
);
if (!ok) {
return res.status(401).json({ error: 'Invalid signature' });
}
}
const event = req.body;
if (event.action === 'opened' && event.issue) {
// Classify and tag new issues automatically
await issueTriageWorkflow.start({
title: event.issue.title,
body: event.issue.body,
issueNumber: event.issue.number,
repository: event.repository.full_name,
});
}
res.json({ received: true });
});
```
### Webhook Best Practices
- **Always verify signatures** using `req.rawBody` (not parsed JSON)
- **Acknowledge fast**: Return 200 quickly, process asynchronously
- **Handle duplicates**: Use webhook IDs for idempotency
- **Security**: Store webhook secrets as encrypted environment variables
---
## Workflow triggers and access patterns
Workflows can be triggered through multiple channels, each suited to different use cases:
### Direct API Access
- **REST endpoints** for direct workflow invocation
- **Workflow API** for programmatic workflow management via the [Workflow API](/docs/workflow-api)
- Perfect for integrating with other services or AI agents
### Webhook Triggers
- **External events** from third-party services (GitHub, Slack, Stripe) trigger workflows automatically
- Built-in webhook signature verification with `req.rawBody`
- Asynchronous processing with immediate acknowledgment
- Perfect for event-driven automation
### Trigger Comparison
| Trigger Type | Use Case | Best For |
| -------------- | ----------------- | ---------------------------------------------- |
| REST API | Direct invocation | User-initiated actions, manual triggers |
| Webhooks | External events | GitHub issues, Slack messages, payment events |
| Scheduled Jobs | Time-based | Periodic summaries, daily reports, cleanup tasks |
For interactive prompting setups, see: [AI agent setup](/docs/ai-agent-setup).
---
## When you might still want heavier tooling
You might still want Airflow, Prefect, or other orchestration platforms when you have:
- Complex data pipelines with distributed processing and backfills
- Strict SLAs that require fine-grained backpressure control
- Python-native model ops where LangChain integrations are central
Codehooks can still host the **execution layer** for these systems (expose endpoints, persist results, schedule jobs), but it's not a replacement for large ETL platforms.
---
## Wrap-up
If the hard part of your AI project is **connecting steps reliably**—not training models—then a light workflow runtime is usually enough. Codehooks gives you small functions, a state machine, storage, and a single deploy target.
These **LLM workflows** can be triggered and managed through APIs, CLI, or webhooks—giving you flexible control while maintaining clear operational boundaries.
- Start with one file and one command
- Add steps as your logic grows
- Trigger via REST API, webhooks, or scheduled jobs
If you’re already calling OpenAI from Node, you’re only a few minutes away from turning your script into a resilient workflow.
---
import FAQSection from '@site/src/components/FAQSection';
---
Useful links: [Workflow API](https://codehooks.io/docs/workflow-api) · [AI agent setup](https://codehooks.io/docs/ai-agent-setup)
---
## How to Give Your OpenClaw Agent a Backend
*(Note: This content contains MDX/JSX code)*
OpenClaw is everywhere right now. People are running "Jarvis" on Mac minis 24/7, automating their entire digital lives.
But here's the thing: OpenClaw runs locally. That's great for privacy and control. It's less great when you need:
- **REST APIs and webhooks** that stay up even when your computer sleeps
- **Persistent storage** beyond local files
- **Background jobs** that run on a schedule
- **CRUD endpoints** your other apps can talk to
You *could* give your agent access to AWS. Let it wire up Lambda, API Gateway, DynamoDB, and SQS. Watch it burn through your free tier and leak credentials.
Or you could give it a backend designed for agents.

## Codehooks: A backend your agent can actually use
Codehooks.io is a serverless backend with everything wired together: REST APIs, database, queues, cron, workers. Your agent can build anything from a simple CRUD API to a multi-step workflow — and deploy it in seconds.
Here's your agent deploying a Stripe webhook handler with signature verification:
```javascript
import { app, Datastore } from 'codehooks-js';
import { verify } from 'webhook-verify';
// Allow Stripe to call this without JWT auth
app.auth('/webhook/*', (req, res, next) => next());
app.post('/webhook/stripe', async (req, res) => {
// Verify signature using rawBody (essential for HMAC validation)
const isValid = verify('stripe', req.rawBody, req.headers, process.env.STRIPE_WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const conn = await Datastore.open();
await conn.insertOne('payments', {
...req.body,
receivedAt: new Date().toISOString()
});
res.json({ received: true });
});
export default app.init();
```
```bash
coho deploy # live in seconds
```
That's a production webhook endpoint with signature verification and persistent storage. Your agent could just as easily deploy a CRUD API, a data pipeline, or a scheduled report — same workflow, same `coho deploy`.
## Your agent already knows how to use it
Run `coho prompt` and you get the complete development prompt — REST routes, CRUD patterns, database operations, queue workers, and workflow examples. Feed it into context and your OpenClaw can build any backend feature without hallucinating.
```bash
coho prompt | pbcopy # copy to clipboard on macOS
```
This is what "AI-friendly" actually means: not a buzzword, but a structured prompt that lets agents build correctly on the first try.
## Why this matters for OpenClaw
### 1. APIs and webhooks that don't depend on your device
Your OpenClaw agent runs on your Mac mini. But what happens when Stripe sends a payment webhook at 3am and your machine is updating macOS? Missed event. What about the CRUD API your mobile app depends on? Down.
With Codehooks, your endpoints live in the cloud. Your agent deploys them, and they stay up whether your Mac is awake or not.
### 2. Sandboxed integrations
Security researchers have called OpenClaw "a security nightmare" — and they're not wrong. An agent with full system access is powerful but risky.
Offloading sensitive integrations (payment webhooks, API keys, customer data) to a separate backend limits the blast radius. Your agent interacts with the backend via API, not raw credentials scattered across your machine.
### 3. Background jobs your agent doesn't have to babysit
Need a daily digest? Weekly report? Hourly data sync? Define it once:
```javascript
import { app } from 'codehooks-js';
app.job('0 9 * * *', async () => {
// runs every day at 9am, whether your agent is awake or not
});
export default app.init();
```
Need async processing too? Add a worker queue alongside your cron jobs:
```javascript
import { app, Datastore } from 'codehooks-js';
app.worker('notify', async (req, res) => {
console.log('Sending notification:', req.body.payload);
// process the task...
res.end();
});
app.post('/enqueue', async (req, res) => {
const conn = await Datastore.open();
await conn.enqueue('notify', req.body);
res.json({ queued: true });
});
export default app.init();
```
### 4. Fast enough for the agent loop
Deploys take seconds, not minutes. Your agent can:
1. Run `coho prompt` to load context
2. Write code
3. Deploy with `coho deploy`
4. Check logs with `coho log -f`
5. Iterate
Something break? Your agent reads the logs, fixes the code, and redeploys — all in the same loop. No rollback ceremony, no CloudFormation stacks. Just fix and ship.
## Get your agent set up in 2 minutes
We've published a Codehooks skill for OpenClaw that bundles everything your agent needs — context, examples, and CLI commands:
→ [codehooks-backend skill on GitHub](https://github.com/RestDB/codehooks-openclaw-skill)
**You do this once:**
```bash
npm install -g codehooks
coho login
coho create openclaw-backend
cd openclaw-backend
npm install codehooks-js
coho add-admintoken
```
Give the admin token to your agent. From here, it can deploy code, query data, and manage the backend on its own using `coho deploy --admintoken $CODEHOOKS_ADMIN_TOKEN`.
Your agent can also start from a ready-made template:
```bash
coho create my-webhooks --template stripe-webhook-handler
cd my-webhooks
npm install codehooks-js
coho deploy
```
Either way, your agent now has a live webhook URL, a database, queues, and cron — all deployable in seconds.
## What you'll have running tonight
Set aside 10 minutes. By the end you'll have:
- **REST APIs and webhooks** that stay up 24/7
- A **database** your agent can read and write to
- **Cron jobs and queues** running in the background
- An agent that can **deploy, debug, and iterate** on all of it without your help
Your Mac mini runs OpenClaw. Codehooks runs everything else.
**A note on responsibility:** Giving your agent the ability to deploy and run code on a live server is powerful — and risky. Review what your agent deploys, set appropriate permissions, and monitor usage. You're responsible for any code your agent ships. The good news: Codehooks has no pay-per-use pricing, so a busy agent won't surprise you with a bill.
→ [Get started free](https://codehooks.io)
---
### Learn more
- [CLI quickstart](/docs/quickstart-cli) — get up and running with `coho`
- [Scheduled jobs & cron](/docs/jobhooks) — the `app.job()` pattern
- [Worker queues](/docs/queuehooks) — background workers and async processing
- [AI agent setup](/docs/ai-agent-setup) — use `coho prompt` to generate agent context
- [API cheat sheet](/docs/apicheatsheet) — quick reference for the full API
---
## Vibe Coding a Todo App: From Zero to Deployed in One Session
*(Note: This content contains MDX/JSX code)*
How fast can you go from an empty folder to a fully deployed full-stack app? In this post, I'll walk through a live vibe coding session where I pair-programmed with an AI agent (Claude Code) to build a todo app using **React** for the frontend and **Codehooks.io** for the backend — all deployed to the cloud in minutes.
Every prompt I typed, every decision the agent made, and every line of code it wrote is documented here. Follow along and try it yourself.

## The Setup
- **Project:** `vibetodo-eack` on Codehooks.io (empty, freshly cleaned)
- **Stack:** React + Vite (frontend), Codehooks.io (backend API + database + static hosting)
- **Tool:** Claude Code (AI coding agent in the terminal)
### Install the Codehooks skill for Claude Code
Before starting, install the [Codehooks Claude plugin](https://github.com/RestDB/codehooks-claude-plugin) so Claude Code understands the Codehooks platform, CLI, and APIs:
```
/plugin marketplace add RestDB/codehooks-claude-plugin
/plugin install codehooks@codehooks
```
Once installed, Claude Code auto-detects Codehooks projects and can deploy, query, and manage your backend directly from the conversation. Using a different AI coding agent? Check out our [AI Agent Setup](/docs/ai-agent-setup) page for instructions on Cursor, Codex CLI, Windsurf, and more.
### Screenshots of the final Todo app
A fully functional React todo app with a complete REST API, auto-generated Swagger documentation, and cloud deployment. All created in a single vibe coding session.

## Prompt 1: The Kickoff
> *"Ok, in this session we'll vibe code the infamous todo app. We will use Codehooks for both frontend (React) and backend (CRUD API and database). The twist here is that we also will write about the process in a blogpost so other developers can read about and follow the dynamic process from zero to a finished deployed app. Document my prompts and your reasoning and actions in a blogpost.md file accordingly. Lets go, plan first, then code."*
### What happened: The Agent planned first
Before writing a single line of code, the agent entered **plan mode** — exploring the project state, reading the Codehooks API reference, and designing a 4-step approach:
1. **Initialize** — `package.json` + start the blogpost
2. **Backend** — Zod schema + `crudlify()` for instant CRUD API
3. **Frontend** — React SPA with Vite
4. **Deploy & verify** — Build, deploy in ~5 seconds, test everything
The plan was approved. Let's execute.
---
## Step 1: Project Initialization
The agent created `package.json` with all dependencies:
- **Backend:** `codehooks-js` + `zod` for validation
- **Frontend:** `react`, `react-dom`, `vite`, `@vitejs/plugin-react`
Both frontend and backend dependencies live in the same project — Codehooks handles the bundling and deployment.
---
## Step 2: The Backend (12 lines of code)
Here's the entire backend — `index.js`:
```javascript
import { app } from 'codehooks-js';
import { z } from 'zod';
const TodoSchema = z.object({
title: z.string().min(1).describe('Todo title'),
completed: z.boolean().default(false).describe('Completion status')
});
app.crudlify({ todos: TodoSchema }, { prefix: '/api' });
app.auth('/api/*', (req, res, next) => next());
app.auth('/*', (req, res, next) => next());
app.static({
route: '/',
directory: '/static',
default: 'index.html',
notFound: '/index.html'
});
export default app.init();
```
### Agent reasoning
> "The key decision here is using `crudlify()` instead of writing manual routes. One line — `app.crudlify({ todos: TodoSchema }, { prefix: '/api' })` — auto-generates a complete REST API with Zod validation. That single line gives us:
>
> - `GET /api/todos` — list all todos
> - `POST /api/todos` — create a todo (validated against the Zod schema)
> - `GET /api/todos/:id` — get one todo
> - `PUT /api/todos/:id` — replace a todo
> - `PATCH /api/todos/:id` — partial update (perfect for toggling `completed`)
> - `DELETE /api/todos/:id` — remove a todo
>
> The `app.static()` block at the end serves the React SPA. The `notFound: '/index.html'` fallback enables client-side routing."
---
## Step 3: The React Frontend
The agent created 4 files under `frontend/`:
**`frontend/vite.config.js`** — Vite config with a proxy to the live Codehooks API (for local dev) and build output to `../static`:
```javascript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: { '/api': 'https://luminous-grove-f487.codehooks.io' }
},
build: { outDir: '../static', emptyOutDir: true }
});
```
**`frontend/src/App.jsx`** — The todo app component. Key features:
- **Add** a todo via text input + form submit
- **Toggle** completed state with a checkbox (uses `PATCH` to update just the `completed` field)
- **Delete** a todo with the X button
- **Counter** showing items remaining
- Clean, minimal inline styling — no CSS framework needed
### Agent reasoning
> "The frontend is intentionally simple. Each action (`addTodo`, `toggleTodo`, `deleteTodo`) calls the Codehooks API and then re-fetches the list. This is the most straightforward pattern — no local state management needed beyond the form input. The `PATCH` method is key: instead of sending the entire todo on toggle, we only send `{ completed: !todo.completed }`, which `crudlify` handles natively."
---
## Step 4: Deploy and Verify
Three commands to go live:
```bash
npm install # Install dependencies
npx vite build frontend # Build React → /static
coho deploy # Deploy to Codehooks (~5 seconds)
```
Output:
```
Deployed Codehook successfully!
```
### Testing the API with curl
**Create:**
```bash
curl -X POST -H 'Content-Type: application/json' \
-d '{"title":"Buy milk","completed":false}' \
https://luminous-grove-f487.codehooks.io/api/todos
```
```json
{"title":"Buy milk","completed":false,"_id":"698cf5b86c156a91bb44f2ba"}
```
**List:**
```bash
curl https://luminous-grove-f487.codehooks.io/api/todos
```
```json
[{"title":"Buy milk","completed":false,"_id":"698cf5b86c156a91bb44f2ba"},
{"title":"Write blogpost","completed":false,"_id":"698cf5ba6c156a91bb44f2bc"}]
```
**Toggle completed (PATCH):**
```bash
curl -X PATCH -H 'Content-Type: application/json' \
-d '{"completed":true}' \
https://luminous-grove-f487.codehooks.io/api/todos/698cf5b86c156a91bb44f2ba
```
```json
{"title":"Buy milk","completed":true,"_id":"698cf5b86c156a91bb44f2ba"}
```
**Validation (empty title rejected):**
```bash
curl -X POST -H 'Content-Type: application/json' \
-d '{"title":""}' \
https://luminous-grove-f487.codehooks.io/api/todos
```
```json
[{"origin":"string","code":"too_small","minimum":1,"inclusive":true,
"path":["title"],"message":"Too small: expected string to have >=1 characters"}]
```
All tests passed. The React SPA loads at the base URL and is fully functional.
---
## The Result
**Live app:** [https://luminous-grove-f487.codehooks.io/](https://luminous-grove-f487.codehooks.io/)
### From zero to deployed: under 5 minutes
Let that sink in. From the moment I typed the kickoff prompt to a fully deployed, tested full-stack app live on the internet — **under 5 minutes**. Here's the breakdown:
| Phase | Time |
|---|---|
| Planning (agent explores + designs approach) | ~30 seconds |
| Backend code generation (`index.js`) | ~10 seconds |
| Frontend code generation (4 files) | ~15 seconds |
| `npm install` | ~9 seconds |
| `vite build` | ~0.4 seconds |
| `coho deploy` | ~5 seconds |
| API testing (6 curl commands) | ~15 seconds |
| **Total wall time** | **~90 seconds** |
The rest of the time was the agent writing this blogpost. The actual app was live before the ink was dry on Step 2 of the documentation.
This is the power of vibe coding with the right stack: you describe *what* you want, the agent figures out *how*, and the platform handles the *where*.
### What we built
- A full-stack todo app with **React** frontend and **Codehooks** backend
- **Zod-validated** CRUD API auto-generated from a schema
- **6 REST endpoints** from a single line of code
- Frontend and backend deployed together to the same URL
### File count
| File | Lines | Purpose |
|---|---|---|
| `index.js` | 24 | Backend: schema + API + static serving |
| `frontend/src/App.jsx` | 120 | React todo app component |
| `frontend/src/main.jsx` | 8 | React entry point |
| `frontend/vite.config.js` | 14 | Build config + dev proxy |
| `frontend/index.html` | 12 | HTML shell |
| `package.json` | 20 | Dependencies |
| **Total** | **~200** | |
---
## Prompt 2: "Add OpenAPI docs and Swagger"
> *"And finally lets add openapi docs and swagger."*
One more `app.openapi()` call — that's it. Since we used `crudlify()` with a Zod schema, Codehooks auto-generates the full OpenAPI 3.0 spec from the schema. No manual route documentation needed.
```javascript
app.openapi({
info: { title: 'Todo API', version: '1.0.0', description: 'CRUD API for todos - built with Codehooks.io' },
tags: [{ name: 'todos', description: 'Todo operations' }]
});
```
One `coho deploy` later:
- **Swagger UI:** [https://luminous-grove-f487.codehooks.io/docs](https://luminous-grove-f487.codehooks.io/docs)
- **OpenAPI spec:** [https://luminous-grove-f487.codehooks.io/openapi.json](https://luminous-grove-f487.codehooks.io/openapi.json)
All 6 CRUD endpoints are fully documented with schemas, parameters, and response types — generated automatically from the Zod `TodoSchema`. Zero extra work.
---
## Prompt 3: "Add a cron job that empties the todos collection every hour"
> *"Add a cron job that empties the todos collection every hour"*
Since this is a public demo app, we want to keep the database clean. Codehooks has built-in cron job support via `app.job()`. Two lines:
```javascript
app.job('0 * * * *', async () => {
const conn = await Datastore.open();
await conn.removeMany('todos', {});
console.log('Cron: cleared todos collection');
});
```
The cron expression `0 * * * *` fires at minute 0 of every hour. `removeMany` with an empty query `{}` wipes the entire collection. Deployed with one `coho deploy` — the job is now scheduled automatically by the platform.
---
## The vibe coding takeaway
The entire session — from empty folder to deployed, tested app — took one conversation. The agent planned before coding, made decisions transparent, and tested everything before declaring success.
The key enablers:
1. **Codehooks `crudlify()`** — turned a Zod schema into a full REST API in one line
2. **Codehooks `app.static()`** — served the React SPA from the same deployment
3. **Vite** — fast builds with zero config
4. **AI agent** — handled the boilerplate so we could focus on the architecture
No Docker. No CI/CD pipeline. No infrastructure config. Just code and deploy.
---
## Stop Building Admin Applications
*(Note: This content contains MDX/JSX code)*
# Stop Building Admin Applications.
Every developer has built some version of the same admin application. Users table, CRUD forms, list views, search, filters, auth, role management, a REST API. You know the drill. You scaffold the project, wire up the endpoints, build the forms, handle validation, add pagination, implement auth — and a week later you have something that looks like every other admin app.
What if you could skip all of that and just *describe* what you need?

See the below example screenshots of the default admin app generated from a json datamodel:

## One JSON Schema. Full-Stack App.
The [React Admin Dashboard template](https://github.com/RestDB/codehooks-io-templates/tree/main/react-admin-dashboard) for Codehooks.io takes a fundamentally different approach. Instead of writing components, routes, and API endpoints, you define your entire application in a single JSON file — your collections, field types, relationships, validation rules, and UI behavior. The template reads that schema at runtime and generates everything: the sidebar navigation, list views, search and filters, detail pages, create/edit forms, lookup fields with live search, file uploads, and a complete REST API with Swagger docs.
It's not code generation. There are no files to maintain. The UI renders dynamically from the schema stored in the database. Change a field, add a collection, reconfigure a relationship — it takes effect immediately. No rebuild, no redeploy.
## The Schema Format AI Agents Already Understand
Here's where it gets interesting. The datamodel format uses an [OpenAPI](https://www.openapis.org/)-style JSON structure — standard JSON Schema conventions that every major AI model has been trained on extensively. That means Claude, ChatGPT, Gemini, or any other AI agent can produce valid schemas on the first try.
A collection definition looks like this:
```js
{
"app": {
"title": "Veterinary Clinic",
"subtitle": "Manage patients, appointments, and treatments",
"icon": "heart-pulse"
},
"collections": {
"patients": {
"label": "Patients",
"icon": "paw-print",
"schema": {
"type": "object",
"required": ["name", "species"],
"properties": {
"name": { "type": "string", "title": "Pet Name" },
"species": {
"type": "string",
"title": "Species",
"enum": ["Dog", "Cat", "Bird", "Reptile", "Other"]
},
"breed": { "type": "string", "title": "Breed" },
"owner": {
"type": "string",
"title": "Owner",
"format": "lookup",
"collection": "owners",
"displayField": "name"
},
"dateOfBirth": { "type": "string", "title": "Date of Birth", "format": "date" },
"photo": { "type": "string", "title": "Photo", "format": "file-upload" }
}
}
}
}
}
```
Standard JSON Schema properties (`type`, `enum`, `required`, `format`) plus a handful of extensions for relationships (`format: "lookup"`) and UI behavior. If you've ever written an OpenAPI schema definition or used JSON Schema for form validation, this will feel immediately familiar.
## The AI Workflow: Prompt → Schema → App
The template ships with a built-in "Copy Prompt" button on the Datamodel Editor page. Click it, and you get a carefully crafted prompt that explains the entire schema format to an AI agent — every field type, relationship pattern, validation option, and UI configuration. Paste it into your AI of choice, describe what you need in plain language, and you get back valid JSON.
Here's the workflow:
1. **Deploy the template** — one command gets you a running app with auth, a dashboard, and the visual editor
2. **Open the Datamodel Editor** and click "Copy Prompt"
3. **Paste into any AI agent** and describe your application: "I need a CRM with companies, contacts, deals, and activities. Deals should have a pipeline with stages from Lead to Closed Won. Contacts belong to companies. Activities log calls, emails, and meetings linked to contacts and deals."
4. **Copy the AI's JSON output** into the editor
5. **Hit Save** — your CRM is live. Full CRUD, relationships, search, filters, REST API, Swagger docs. Done.
No code written. No components built. No API endpoints configured.
## What You Get Out of the Box
This isn't a toy demo. The template generates a production-grade admin interface:
**Dynamic CRUD** — List views with column sorting, full-text search, and filter buttons. Detail panels with related data from linked collections. Create/edit forms with proper validation, enum dropdowns, date pickers, markdown editors, and file uploads.
**Relationships** — Lookup fields with live search across collections (single-select and multi-select). Parent-child views on detail pages showing related records. Tree views for hierarchical data with expandable sub-items.
**Authentication** — JWT-based login with httpOnly cookie sessions. Two built-in roles (admin and user). User management UI. The sidebar dynamically hides admin-only sections.
**Visual Datamodel Editor** — Add, remove, and configure collections and fields through a form-based UI. Switch to a JSON tab with syntax highlighting for power users. Full version history with one-click rollback.
**REST API + Swagger** — Every collection automatically gets a complete CRUD API. The OpenAPI documentation at `/docs` updates dynamically when you change the schema. No code generation, no sync issues.
**Modern UI** — React 18 with Vite, Tailwind CSS v4, and shadcn/ui components. Dark mode with system preference support. Responsive layout with a collapsible sidebar that works on mobile.
## The Swiss Army Knife for Admin Apps
The reason I call this a Swiss army knife is that the same template handles wildly different use cases just by swapping the JSON:
**Project management** — tasks, milestones, team members, time tracking with status pipelines
**CRM** — companies, contacts, deals, activities with lookup relationships across collections
**Inventory system** — products, categories, suppliers, purchase orders with file uploads for product images
**Content management** — articles, authors, categories, tags with markdown editing and status workflows
**Hackathon platform** — events, participants, teams, submissions, judges, scores with cross-linked detail views
Each of these is a different `datamodel.json`. Same template, same codebase, same deployment process. The schema *is* the application.
## Getting Started
```bash
npm i -g codehooks
coho login
coho create myapp --template react-admin-dashboard
cd myapp && mv config.json backend
coho set-env JWT_ACCESS_TOKEN_SECRET $(openssl rand -hex 32)
coho set-env JWT_REFRESH_TOKEN_SECRET $(openssl rand -hex 32)
npm run install:all
npm run deploy
```
That's it. You have a running admin dashboard. Log in with `admin` / `admin`, open the Datamodel Editor, and start building.
Or skip the manual editing entirely — copy the prompt, tell an AI what you need, paste the JSON, and your app is live.
## What's Next
The template is open source and actively developed. Some directions we're exploring:
- **Clerk authentication** — plug in [Clerk](https://clerk.com) for a complete, production-ready auth experience with social logins, MFA, and user management
- **Headless CMS mode** — use the same datamodel to power static site generation with frameworks like Astro
- **Custom views and dashboards** — extend beyond CRUD with configurable dashboard widgets
- **Workflow automation** — trigger serverless functions on record changes, integrate with queues and cron jobs via Codehooks.io
The full source code is on GitHub: [react-admin-dashboard](https://github.com/RestDB/codehooks-io-templates/tree/main/react-admin-dashboard)
If you've ever spent a week building an admin app that could have been a JSON file, give this a try. Five minutes from `coho create` to a production app. No CRUD boilerplate. No auth plumbing. No form builders.
Describe what you need. Let AI write the schema. Ship it.
---
*Built with [Codehooks.io](https://codehooks.io) — the serverless backend platform designed for rapid development and AI workflows.*
---
## Build a Webhook Delivery System in 5 Minutes with Codehooks.io
*(Note: This content contains MDX/JSX code)*
# Build a Webhook Delivery System in 5 Minutes with Codehooks.io
You've built an amazing application. Users love it. Now they're asking: "Can you send webhooks when events happen?"
Maybe it's:
- An e-commerce platform where customers want order notifications and webhook delivery
- A SaaS tool where users need real-time webhook alerts
- An IoT system where devices trigger external workflows via webhooks
- A business application where events need webhook integration with other systems
**The problem?** Building a production-ready webhook delivery system from scratch takes weeks. Setting up webhook infrastructure, managing webhook queues, and handling webhook retries is complex.
**The solution?** Use this Codehooks.io webhook template and have webhook delivery running in 5 minutes.

## What Problem Does This Solve?
Imagine you're building an order management system. Your customers want to know immediately when orders are created, payments are processed, or items are shipped.
They don't want to poll your API every few seconds. They want **webhooks** - real-time HTTP callbacks with webhook delivery when events occur.
But you don't want to spend weeks building webhook infrastructure, webhook queues, and webhook retry logic. You want to focus on your core product.
**This webhook template solves that.** Deploy it once, and you have a complete webhook delivery system with automatic retries, HMAC signing, and queue-based processing.
## Why Not Build It Yourself?
Building webhooks from scratch means implementing:
- Webhook registration system with CRUD API
- Webhook URL verification (Stripe and Slack styles)
- Webhook security (HMAC signing, SSRF protection, timestamp validation)
- Webhook delivery infrastructure (queues, retries, timeouts)
- Webhook monitoring and auto-disable for failing endpoints
- Documentation and examples
**Estimated time:** 2-4 weeks for an experienced developer.
**Maintenance:** Ongoing updates, security patches, webhook scaling issues.
## Webhook Solutions Comparison
### Webhook Infrastructure Services
Several services exist for webhook delivery:
**[Webhook Relay](https://webhookrelay.com/)** - Webhook forwarding and tunneling service. Great for routing webhooks to private networks, but you still need to build the webhook delivery logic yourself. Pricing starts at $7.50/month.
**[Hookdeck](https://hookdeck.com/)** - Webhook infrastructure platform. Excellent for receiving and routing webhooks, but primarily focused on inbound webhooks. For outbound webhooks, you need their paid plans starting at $15/month.
**[Svix](https://www.svix.com/)** - Dedicated webhook sending service. Powerful but expensive ($250/month for production). Closed-source SaaS with vendor lock-in.
**[Zapier Webhooks](https://zapier.com/apps/webhooks/integrations)** - Webhook automation. Great for no-code workflows, but limited customization and can be costly at scale ($20-$50+/month).
### The Codehooks.io Advantage: Code-First + AI
**Codehooks.io takes a different approach:** Instead of a rigid SaaS platform, you get **full source code** that you can deploy, customize, and enhance with AI assistance.
**Why this matters:**
1. **Full Control** - You own the code. Modify webhook payload structure, add custom headers, implement tenant-specific logic, or integrate with your existing systems. No API limitations.
2. **AI-Powered Customization** - Use Claude, ChatGPT, or any LLM to modify the template instantly:
- "Add webhook batching for high-frequency events"
- "Implement custom retry logic based on HTTP status codes"
- "Add webhook filtering by customer tier"
- The AI understands your code and makes changes directly
3. **Cost-Effective** - Free tier for development. Production plans start at $19/month with generous API limits and predictable costs. No surprise bills as you scale.
4. **No Vendor Lock-In** - The code is yours. Move it anywhere if needed. Export your webhook data anytime.
5. **Production-Ready Template** - Don't start from scratch. Get queue-based delivery, HMAC signing, retry logic, SSRF protection, and webhook monitoring out of the box.
6. **Serverless Scaling** - Automatic scaling built into Codehooks.io. Handle 10 webhooks or 10,000 without infrastructure changes.
**The code-first approach means you get the best of both worlds:** rapid deployment like a SaaS platform, but with the flexibility and cost-effectiveness of custom code.
## Deploy Your Webhook System in 5 Minutes
Instead of weeks of development or expensive SaaS subscriptions:
```bash
coho create mywebhooks --template webhook-delivery
cd mywebhooks
npm install
coho deploy
```
**Time:** 5 minutes.
**Cost:** Free tier for development, pick a paid plan suitable for production volumes.
**Customization:** Ask an AI to modify the code for your exact needs.
**Maintenance:** Platform handles scaling, security, and infrastructure.
## Quick Start
### 1. Deploy (2 minutes)
```bash
coho create mywebhooks --template webhook-delivery
cd mywebhooks
npm install
coho deploy
```
You'll get a URL like: `https://my-webhooks-abc123.api.codehooks.io/dev`
### 2. Test (2 minutes)
```bash
export API_URL="https://my-webhooks-abc123.api.codehooks.io/dev"
export CODEHOOKS_API_KEY="your-api-key-here"
# Register a webhook (use webhook.site for testing)
curl -X POST $API_URL/webhooks \
-H "Content-Type: application/json" \
-H "x-apikey: $CODEHOOKS_API_KEY" \
-d '{
"clientId": "test-client-1",
"url": "https://webhook.site/your-unique-id",
"events": ["order.created"]
}'
# Trigger an event
curl -X POST $API_URL/events/trigger/order.created \
-H "Content-Type: application/json" \
-H "x-apikey: $CODEHOOKS_API_KEY" \
-d '{"orderId": "12345", "total": 99.99}'
```
Check webhook.site - your webhook was delivered! 🎉
## How to Use It From Your App
Once deployed, integrate webhook triggering into your application code:
### From Node.js/JavaScript
```javascript
// In your order creation code
async function createOrder(orderData) {
const order = await db.orders.insertOne(orderData);
// Trigger webhook event
fetch(`${WEBHOOK_SERVICE_URL}/events/trigger/order.created`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-apikey': process.env.CODEHOOKS_API_KEY
},
body: JSON.stringify({
orderId: order.id,
total: order.total,
createdAt: order.createdAt
})
}).catch(err => console.error('Webhook trigger failed:', err));
return order;
}
```
### From Python
```python
import requests
import os
def create_order(order_data):
order = db.orders.insert_one(order_data)
# Trigger webhook event
try:
requests.post(
f"{WEBHOOK_SERVICE_URL}/events/trigger/order.created",
headers={"x-apikey": os.environ["CODEHOOKS_API_KEY"]},
json={"orderId": str(order.inserted_id), "total": order_data["total"]},
timeout=5
)
except Exception as e:
print(f"Webhook trigger failed: {e}")
return order
```
### From Any Language
Just make an HTTP POST request:
```bash
curl -X POST https://your-webhook-service.api.codehooks.io/dev/events/trigger/order.created \
-H "Content-Type: application/json" \
-H "x-apikey: YOUR_CODEHOOKS_API_KEY" \
-d '{"orderId": "12345", "total": 99.99}'
```
**That's it!** The webhook delivery service handles:
- Finding all subscribed webhook endpoints
- Webhook payload signing with HMAC SHA-256
- Webhook delivery to each URL via queue-based system
- Automatic webhook retries on failure (exponential backoff)
- Auto-disabling failing webhook endpoints
## What You Get
### Robust & Scalable Webhook Architecture
Here’s how webhook events are triggered, queued, and delivered at scale:
```mermaid
flowchart TD
A[Your Application fires event]
B1[Event Trigger Endpoint Step 1: Store event in DB]
B2[Step 2: Mark matching webhooks with pendingEventId]
B3[Step 3: enqueueFromQuery webhooks with pendingEventId]
B4[Return 202 Accepted]
C[Message Queue 1,247 webhooks queued Response time: 45ms]
D1[Worker 1 Fetch event from DB]
D2[Worker 2 Fetch event from DB]
DN[Worker N Fetch event from DB]
E1[POST to Webhook URL 1]
E2[POST to Webhook URL 2]
EN[POST to Webhook URL N]
F1[Update webhook stats Clear pendingEventId]
F2[Update webhook stats Clear pendingEventId]
FN[Update webhook stats Clear pendingEventId]
A -->|POST /events/trigger/order.placed| B1
B1 --> B2
B2 --> B3
B3 --> B4
B4 --> C
C -->|webhook 1| D1
C -->|webhook 2| D2
C -->|webhook 1247| DN
D1 --> E1
D2 --> E2
DN --> EN
E1 --> F1
E2 --> F2
EN --> FN
style A fill:#1565c0
style B1 fill:#e65100
style B2 fill:#e65100
style B3 fill:#e65100
style B4 fill:#e65100
style C fill:#424242
style D1 fill:#2e7d32
style D2 fill:#2e7d32
style DN fill:#2e7d32
style E1 fill:#c2185b
style E2 fill:#c2185b
style EN fill:#c2185b
style F1 fill:#1565c0
style F2 fill:#1565c0
style FN fill:#1565c0
```
### Webhook Management API
Your application manages webhook subscriptions on behalf of customers:
```bash
POST /webhooks # Register webhook endpoint
GET /webhooks # List all webhook subscriptions
GET /webhooks/:id # Get webhook details
PATCH /webhooks/:id # Update webhook configuration
DELETE /webhooks/:id # Delete webhook subscription
GET /webhooks/:id/stats # Webhook delivery statistics
POST /webhooks/:id/retry # Retry failed webhook deliveries
```
**Important:** This webhook API is for **your application**, not directly exposed to customers. Build your own UI where customers configure webhook endpoints, then your backend calls this webhook service.
**Webhook Integration Flow:**
1. Customer enters webhook URL in **your UI**
2. **Your backend** calls `POST /webhooks` to register the webhook
3. When events occur, **your app** calls `POST /events/trigger/:eventType`
4. Webhook delivery service delivers to all registered webhook endpoints
### Flexible Webhook Events
Any webhook event name works - no configuration needed:
```javascript
POST /events/trigger/order.created
POST /events/trigger/payment.succeeded
POST /events/trigger/sensor.reading
POST /events/trigger/anything.you.want
```
### Webhook Security & Reliability Features
- **Webhook Signing**: HMAC SHA-256 payload signing with unique secrets per webhook
- **Webhook Verification**: URL verification (Stripe and Slack styles)
- **SSRF Protection**: Blocks webhook delivery to internal IPs
- **Queue-Based Delivery**: Scalable webhook queue system handles thousands of concurrent deliveries
- **Automatic Webhook Retries**: 3 attempts with exponential backoff for failed webhooks
- **Webhook Monitoring**: Auto-disable after 10 consecutive failures
- **Webhook Audit Trail**: 90-day event retention for debugging and replay
## Real-World Examples
### E-Commerce Platform
```javascript
// When order is created
await fetch(`${WEBHOOK_URL}/events/trigger/order.created`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-apikey': process.env.CODEHOOKS_API_KEY
},
body: JSON.stringify({ orderId, items, shippingAddress })
});
```
### SaaS Analytics
```python
# When report is ready
requests.post(
f"{webhook_url}/events/trigger/report.completed",
headers={"x-apikey": os.environ["CODEHOOKS_API_KEY"]},
json={"reportId": report_id, "downloadUrl": url}
)
```
### IoT Monitoring
```go
// When sensor detects anomaly
import (
"bytes"
"encoding/json"
"net/http"
"os"
)
payload := map[string]interface{}{
"deviceId": device.ID,
"temperature": reading.Temperature,
}
jsonData, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST",
webhookURL+"/events/trigger/sensor.alert",
bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-apikey", os.Getenv("CODEHOOKS_API_KEY"))
http.DefaultClient.Do(req)
```
import FAQSection from '@site/src/components/FAQSection';
## Get Started with Webhook Delivery
Deploy your webhook system in minutes:
```bash
coho create mywebhooks --template webhook-delivery
cd mywebhooks
coho deploy
```
Integrate webhook delivery into your app:
```javascript
await fetch(`${WEBHOOK_URL}/events/trigger/${eventType}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-apikey': process.env.CODEHOOKS_API_KEY
},
body: JSON.stringify(eventData)
});
```
The webhook delivery system handles everything else automatically.
## Why Choose Codehooks.io for Webhooks?
**vs. Building from Scratch**: Save 2-4 weeks of development time. Get production-ready webhook infrastructure immediately.
**vs. Webhook Relay / Hookdeck**: You own the webhook code. Full customization with AI assistance. Lower cost at scale.
**vs. Svix**: 10x cheaper for webhook delivery. No vendor lock-in. Modify webhook logic directly in your codebase.
**vs. Zapier**: Developer-focused webhook solution. API-first, not UI-first. Unlimited webhook customization.
**The Codehooks.io difference**: Code-first webhook platform that combines rapid deployment, AI-powered customization, and cost-effective scaling.
## Webhook Resources
- [Complete Webhook Template on GitHub](https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-delivery)
- [Easy API Integration Tutorial](/blog/api-integration-made-easy) - Learn how to integrate multiple APIs and webhooks
- [Codehooks.io Documentation](https://codehooks.io/docs)
- [Webhook API Reference](https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-delivery/CURL_EXAMPLES.md)
- [Webhook Architecture Guide](https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-delivery/ARCHITECTURE.md)
- [Webhook Quick Start Guide](https://github.com/RestDB/codehooks-io-templates/tree/main/webhook-delivery/QUICKSTART.md)
---
**Questions about webhook delivery?** Open an issue on [GitHub](https://github.com/RestDB/codehooks-io-templates/issues).
---
## Secure Your Automation Webhooks with Signature Verification (Zapier, Make, n8n, IFTTT)
*(Note: This content contains MDX/JSX code)*
# Secure Your Zapier, Make, n8n, and IFTTT Webhooks with Signature Verification
You've built a powerful automation: Stripe payment → Zapier → Slack notification + Google Sheet update + email confirmation. It works beautifully.
But there's a problem: **anyone who knows your Zapier webhook URL can trigger your automation with fake data.**
The same is true for Make (Integromat), n8n, and IFTTT. These platforms prioritize ease-of-use, but they don't verify that incoming webhooks are actually from Stripe, GitHub, or whoever you think is sending them.
**The solution?** Use Codehooks as a verified webhook gateway that sits between the webhook sender and your automation platform.
:::tip Not a coder? No problem
If you're used to no-code tools, the JavaScript examples below might look intimidating. You can use ChatGPT, Claude, or any AI assistant to generate and customize this code for you. Just share the [Codehooks AI agent setup](/docs/ai-agent-setup) or point the AI to [codehooks.io/llms.txt](https://codehooks.io/llms.txt) — it will understand how to write Codehooks code for your specific use case. AI coding agents like [Claude Code](https://claude.ai/code) can even run the CLI commands to deploy and configure everything for you.
:::

## The Security Problem
When you set up a webhook trigger in Zapier, Make, n8n, or IFTTT, you get a URL like:
```
https://hooks.zapier.com/hooks/catch/123456/abcdef/
https://hook.eu1.make.com/abc123xyz
https://your-n8n.cloud/webhook/stripe-payments
https://maker.ifttt.com/trigger/{event}/with/key/{your_key}
```
These endpoints accept **any** HTTP POST request. There's no verification that:
- The request actually came from Stripe/GitHub/etc.
- The payload hasn't been tampered with
- The request isn't a replay attack
IFTTT uses a key embedded in the URL, but this only authenticates *your account* — it doesn't verify the payload came from a legitimate source like Stripe.
### What the platforms say
**Zapier:** The [Zapier Community confirms](https://community.zapier.com/code-webhooks-52/verify-signature-of-incoming-webhook-10832) that "Webhooks by Zapier" trigger does not support signature verification.
**Make:** Users in the [Make Community](https://community.make.com/t/secure-webhooks-how-to-do-this-in-integromat/1760) discuss workarounds like header filtering and API keys, but there's no built-in signature verification for services like Stripe or GitHub.
**n8n:** There's an [active feature request](https://community.n8n.io/t/feature-proposal-hmac-signature-verification-for-webhook-node/223375) for HMAC verification. Currently, you need to implement it manually with Code nodes.
**IFTTT:** Uses a [secret key embedded in the webhook URL](https://help.ifttt.com/hc/en-us/articles/115010230347-Webhooks-service-FAQ). If someone obtains your key, they can trigger any event on your account. There's even a [GitHub project](https://github.com/jeysal/ifttt-webhook-shield) created specifically to address this security gap.
### Why this matters
An attacker who discovers your webhook URL could:
- Trigger fake payment notifications
- Create fraudulent orders in your system
- Spam your Slack channels
- Manipulate your database through automations
- Waste your automation platform credits
## The Solution: Verified Webhook Gateway
Instead of sending webhooks directly to your automation platform, route them through Codehooks:
```mermaid
flowchart LR
subgraph Sources["Webhook Sources"]
S1[Stripe]
S2[GitHub]
S3[Shopify]
end
subgraph Gateway["Codehooks Gateway"]
V[Verify Signature]
L[(Log Event)]
T[Transform Data]
end
subgraph Automation["Automation Platforms"]
Z[Zapier]
M[Make]
N[n8n]
I[IFTTT]
end
S1 --> V
S2 --> V
S3 --> V
V --> L
L --> T
T --> Z
T --> M
T --> N
T --> I
style V fill:#2e7d32,stroke:#1b5e20
style L fill:#1565c0,stroke:#0d47a1
style T fill:#e65100,stroke:#bf360c
```
Codehooks handles the complex signature verification for each service, then forwards verified events to your automation platform.
## Quick Start: Stripe to Zapier
Here's a complete example that verifies Stripe webhooks before forwarding to Zapier:
```bash
coho create webhook-gateway
cd webhook-gateway
npm install stripe
```
Replace `index.js` with:
```javascript
import { app, Datastore } from 'codehooks-js';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
// Bypass JWT for webhook endpoints
app.auth('/webhook/*', (req, res, next) => {
next();
});
// Verified Stripe → Zapier gateway
app.post('/webhook/stripe', async (req, res) => {
const sig = req.headers['stripe-signature'];
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
// Verify the webhook is actually from Stripe
event = stripe.webhooks.constructEvent(req.rawBody, sig, webhookSecret);
} catch (err) {
console.error('Stripe signature verification failed:', err.message);
return res.status(400).json({ error: 'Invalid signature' });
}
// Log verified event for audit trail
const conn = await Datastore.open();
await conn.insertOne('verified_events', {
source: 'stripe',
eventId: event.id,
type: event.type,
verified: true,
receivedAt: new Date().toISOString(),
});
console.log(`Verified Stripe event: ${event.type}`);
// Forward to Zapier (now we know it's legitimate)
const zapierUrl = process.env.ZAPIER_WEBHOOK_URL;
try {
await fetch(zapierUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verified: true,
source: 'stripe',
eventType: event.type,
eventId: event.id,
data: event.data.object,
timestamp: new Date().toISOString(),
}),
});
} catch (err) {
console.error('Failed to forward to Zapier:', err.message);
// Store for retry
await conn.enqueue('retryForward', { event, target: 'zapier' });
}
res.status(200).json({ received: true, verified: true });
});
export default app.init();
```
Deploy and configure:
```bash
coho deploy
coho set-env STRIPE_SECRET_KEY sk_live_xxxxx --encrypted
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encrypted
coho set-env ZAPIER_WEBHOOK_URL https://hooks.zapier.com/hooks/catch/xxxxx --encrypted
```
Now in your Stripe Dashboard, set the webhook URL to your Codehooks endpoint instead of Zapier directly.
## GitHub to Make (Integromat)
Verify GitHub webhooks before forwarding to Make:
```javascript
import { app, Datastore } from 'codehooks-js';
import crypto from 'crypto';
app.auth('/webhook/*', (req, res, next) => {
next();
});
// Verified GitHub → Make gateway
app.post('/webhook/github', async (req, res) => {
const signature = req.headers['x-hub-signature-256'];
const secret = process.env.GITHUB_WEBHOOK_SECRET;
// Verify GitHub signature
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)) {
console.error('GitHub signature verification failed');
return res.status(401).json({ error: 'Invalid signature' });
}
const event = req.headers['x-github-event'];
const delivery = req.headers['x-github-delivery'];
// Log verified event
const conn = await Datastore.open();
await conn.insertOne('verified_events', {
source: 'github',
eventId: delivery,
type: event,
verified: true,
receivedAt: new Date().toISOString(),
});
console.log(`Verified GitHub event: ${event}`);
// Forward to Make
const makeUrl = process.env.MAKE_WEBHOOK_URL;
await fetch(makeUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verified: true,
source: 'github',
event: event,
deliveryId: delivery,
payload: req.body,
timestamp: new Date().toISOString(),
}),
});
res.status(200).json({ received: true });
});
export default app.init();
```
## Multi-Source Gateway
Handle multiple webhook sources in one deployment:
```javascript
import { app, Datastore } from 'codehooks-js';
import Stripe from 'stripe';
import crypto from 'crypto';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.auth('/webhook/*', (req, res, next) => {
next();
});
// Stripe webhooks
app.post('/webhook/stripe', async (req, res) => {
try {
const event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
await forwardVerified('stripe', event.type, event.id, event.data.object);
res.status(200).json({ received: true });
} catch (err) {
console.error('Stripe verification failed:', err.message);
res.status(400).json({ error: 'Invalid signature' });
}
});
// GitHub webhooks
app.post('/webhook/github', async (req, res) => {
const signature = req.headers['x-hub-signature-256'];
const expected = 'sha256=' + crypto
.createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET)
.update(req.rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
await forwardVerified(
'github',
req.headers['x-github-event'],
req.headers['x-github-delivery'],
req.body
);
res.status(200).json({ received: true });
});
// Shopify webhooks
app.post('/webhook/shopify', async (req, res) => {
const hmac = req.headers['x-shopify-hmac-sha256'];
const expected = crypto
.createHmac('sha256', process.env.SHOPIFY_WEBHOOK_SECRET)
.update(req.rawBody)
.digest('base64');
if (hmac !== expected) {
return res.status(401).json({ error: 'Invalid signature' });
}
await forwardVerified(
'shopify',
req.headers['x-shopify-topic'],
req.headers['x-shopify-webhook-id'],
req.body
);
res.status(200).json({ received: true });
});
// Generic forward function
async function forwardVerified(source, eventType, eventId, data) {
const conn = await Datastore.open();
// Audit log
await conn.insertOne('verified_events', {
source,
eventType,
eventId,
verified: true,
receivedAt: new Date().toISOString(),
});
// Forward based on routing rules
const routes = {
stripe: process.env.ZAPIER_STRIPE_URL,
github: process.env.MAKE_GITHUB_URL,
shopify: process.env.N8N_SHOPIFY_URL,
// IFTTT example: https://maker.ifttt.com/trigger/{event}/with/key/{key}
paypal: process.env.IFTTT_PAYPAL_URL,
};
const targetUrl = routes[source];
if (!targetUrl) {
console.log(`No route configured for ${source}`);
return;
}
await fetch(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verified: true,
source,
eventType,
eventId,
data,
verifiedAt: new Date().toISOString(),
}),
});
console.log(`Forwarded verified ${source} event to automation`);
}
export default app.init();
```
## Advanced: Retry Failed Forwards
If your automation platform is temporarily down, queue events for retry:
```javascript
import { app, Datastore } from 'codehooks-js';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.auth('/webhook/*', (req, res, next) => {
next();
});
app.post('/webhook/stripe', async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.status(400).json({ error: 'Invalid signature' });
}
const conn = await Datastore.open();
// Store verified event
await conn.insertOne('verified_events', {
source: 'stripe',
eventId: event.id,
type: event.type,
data: event.data.object,
forwarded: false,
receivedAt: new Date().toISOString(),
});
// Queue for async forwarding (handles retries automatically)
await conn.enqueue('forwardToZapier', {
eventId: event.id,
type: event.type,
data: event.data.object,
});
// Respond immediately to Stripe
res.status(200).json({ received: true });
});
// Worker handles forwarding with retries
app.worker('forwardToZapier', async (req, res) => {
const { eventId, type, data } = req.body.payload;
const response = await fetch(process.env.ZAPIER_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
verified: true,
source: 'stripe',
eventType: type,
eventId,
data,
timestamp: new Date().toISOString(),
}),
});
if (!response.ok) {
throw new Error(`Zapier returned ${response.status}`);
}
// Mark as forwarded
const conn = await Datastore.open();
await conn.updateOne('verified_events', { eventId }, {
$set: { forwarded: true, forwardedAt: new Date().toISOString() }
});
console.log(`Forwarded event ${eventId} to Zapier`);
res.end();
});
export default app.init();
```
## Securing the Forwarding Step
The examples above verify incoming webhooks from Stripe/GitHub/etc., but what about the forwarding to your automation platform? Here are three approaches:
### 1. Secret Header Authentication
Add a shared secret as a custom header. Configure your automation platform to check for it:
```javascript
await fetch(zapierUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Gateway-Secret': process.env.GATEWAY_SECRET,
},
body: JSON.stringify({ verified: true, source, data }),
});
```
In Zapier/Make/n8n, add a filter step that checks `X-Gateway-Secret` matches your expected value. Requests without the correct header are rejected.
### 2. Callback Verification
Let your automation platform verify events by calling back to Codehooks:
```javascript
// Store verified events with a verification token
app.post('/webhook/stripe', async (req, res) => {
const event = stripe.webhooks.constructEvent(/* ... */);
const verificationToken = crypto.randomUUID();
const conn = await Datastore.open();
await conn.insertOne('verified_events', {
eventId: event.id,
token: verificationToken,
data: event.data.object,
expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 min TTL
});
// Forward with verification token
await fetch(zapierUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
eventId: event.id,
verificationToken,
verifyUrl: `https://your-app.api.codehooks.io/dev/verify/${event.id}`,
data: event.data.object,
}),
});
res.json({ received: true });
});
// Verification endpoint for automation platforms to call
app.get('/verify/:eventId', async (req, res) => {
const { eventId } = req.params;
const { token } = req.query;
const conn = await Datastore.open();
const event = await conn.getOne('verified_events', { eventId, token });
if (!event || new Date(event.expiresAt) < new Date()) {
return res.status(404).json({ verified: false });
}
res.json({ verified: true, eventId });
});
```
Your automation workflow first calls the `verifyUrl` to confirm the event is legitimate before processing.
### 3. Keep URLs Secret
At minimum, treat webhook URLs as secrets:
- Store them with `coho set-env ... --encrypted`
- Never log or expose them
- Rotate them periodically
- Use HTTPS (always encrypted in transit)
The random tokens in Zapier/Make/n8n URLs provide basic authentication — anyone with the URL can trigger your automation, so protect it accordingly.
**Which approach to use?** For notifications and logging, secret URLs are sufficient. For business workflows, add header authentication. For financial or security-sensitive operations, consider callback verification.
## Benefits of This Approach
| Feature | Direct to Zapier/Make/n8n/IFTTT | Via Codehooks Gateway |
|---------|--------------------------------|----------------------|
| Signature verification | No | Yes |
| Audit trail | No | Yes (database) |
| Retry on failure | Limited | Built-in queues |
| Event replay | No | Yes |
| Rate limiting | No | Configurable |
| Data transformation | Limited | Full code control |
| Cost | Automation credits only | Small Codehooks cost |
## When to Use This Pattern
**Use a verified gateway when:**
- Webhooks trigger financial transactions or sensitive operations
- You need an audit trail for compliance
- You want to transform data before it reaches your automation
- You need retry logic if the automation platform is down
- Security is a priority
**Skip it when:**
- The webhook source is internal/trusted
- The automation is low-stakes (e.g., logging, notifications)
- You're prototyping and will add security later
## Supported Webhook Sources
Codehooks can verify signatures from any service. Here are guides for popular platforms:
- [Stripe Webhooks](/docs/examples/webhooks/stripe) - HMAC-SHA256 signatures
- [GitHub Webhooks](/docs/examples/webhooks/github) - HMAC-SHA256 signatures
- [Shopify Webhooks](/docs/examples/webhooks/shopify) - HMAC-SHA256 signatures (Base64)
- [PayPal Webhooks](/docs/examples/webhooks/paypal) - Certificate-based verification
- [Twilio Webhooks](/docs/examples/webhooks/twilio) - HMAC-SHA1 signatures
- [SendGrid Webhooks](/docs/examples/webhooks/sendgrid) - ECDSA signatures
- [OpenAI Webhooks](/docs/examples/webhooks/openai) - Standard Webhooks (HMAC-SHA256)
import FAQSection from '@site/src/components/FAQSection';
## Get Started
Deploy your verified webhook gateway in 5 minutes:
```bash
coho create webhook-gateway
cd webhook-gateway
npm install stripe # or other SDKs you need
coho deploy
```
Configure your secrets:
```bash
coho set-env STRIPE_WEBHOOK_SECRET whsec_xxxxx --encrypted
coho set-env ZAPIER_WEBHOOK_URL https://hooks.zapier.com/xxxxx --encrypted
```
Point your webhook sources (Stripe, GitHub, etc.) to your Codehooks URL instead of directly to Zapier/Make/n8n/IFTTT.
Your automations are now protected by cryptographic signature verification.
## Related Resources
- [What are Webhooks?](/docs/examples/webhooks/what-are-webhooks) - Webhook fundamentals
- [Easy API Integration Tutorial](/blog/api-integration-made-easy) - Connect Stripe, Shopify, and multiple APIs
- [Webhook Delivery System](/blog/build-webhook-delivery-system-5-minutes-codehooks-io) - Build outgoing webhooks
- [Browse All Webhook Examples](/docs/examples/examples-overview) - Platform-specific guides
- [Codehooks Quick Start](/docs/quickstart-cli) - Get started with Codehooks
---
**Questions?** Open an issue on [GitHub](https://github.com/anthropics/claude-code/issues) or check the [Codehooks documentation](https://codehooks.io/docs).
---
## Webhook-Driven Email Automation in 5 Minutes
*(Note: This content contains MDX/JSX code)*
# Webhook-Driven Email Automation in 5 Minutes
Every modern SaaS needs to react to events: a user signs up, a payment succeeds, a trial expires. Webhooks are the glue that connects these events to your business logic—but building reliable webhook infrastructure means managing servers, queues, retries, and databases.
Email automation platforms promise to handle this, but they come with baggage: vendor lock-in, limited webhook integrations, and opaque pricing that scales against you.
What if your webhook endpoints could directly power your email automation?
## The Problem with Email Automation SaaS
Mailchimp, ConvertKit, ActiveCampaign—they all solve the same problem: sending automated email sequences. But they come with baggage:
- **Limited webhook support**: Want to trigger emails from Stripe events? Custom webhooks from your app? Good luck integrating.
- **Vendor lock-in**: Your subscribers live in their database
- **Black boxes**: Webhook failed? Email bounced? You'll never know why.
- **Scaling costs**: $50/month for 500 subscribers, $300/month for 10,000
What if you could own the entire stack—starting with your webhook endpoints?
## Enter Codehooks.io: Webhooks Made Simple
Codehooks.io is built for webhooks. Deploy endpoint handlers in seconds, connect them to databases and queues, and let the platform handle the infrastructure.
I built a drip email template that showcases this:
✅ **Webhook-first architecture** - Receive events from Stripe, signup forms, Zapier, or any service
✅ **Full control** - You own the code, subscribers, and sending logic
✅ **Any email provider** - SendGrid, Mailgun, Postmark—switch with one env var
✅ **Built-in queues & cron** - No external services to configure
✅ **Production-ready** - Scales to 100k+ subscribers with streaming architecture
And it deploys in 5 minutes.
**View the source code:** [github.com/RestDB/codehooks-io-templates/drip-email-workflow](https://github.com/RestDB/codehooks-io-templates/tree/main/drip-email-workflow)
## How It Works
Here's the webhook-driven architecture:
```mermaid
flowchart TD
A["🔔 INCOMING WEBHOOKS Stripe • Signup Forms • Your App • Any Service "]
B["🌐 Webhook Endpoints Validate & Store in Database"]
C["⏰ Cron Job - Every 15 Minutes Find & Queue Subscribers ready for next email"]
D["📧 Queue Worker Send Email via Provider SendGrid/Mailgun/Postmark"]
A --> B
B --> C
C --> D
```
Webhook endpoints receive events. Cron processes them. Queues deliver emails. That's it.
### Configure Your Workflow in JSON
```json
{
"workflowSteps": [
{
"step": 1,
"hoursAfterSignup": 24,
"template": {
"subject": "Welcome to Our Community! 🎉",
"heading": "Welcome, {{name}}!",
"body": "Thanks for signing up...",
"buttonText": "Get Started",
"buttonUrl": "https://example.com/get-started"
}
},
{
"step": 2,
"hoursAfterSignup": 96,
"template": {
"subject": "Quick Tips to Get You Started 💡",
"heading": "Here are some tips, {{name}}",
"body": "..."
}
}
]
}
```
Add 3 steps or 30—same architecture, same simplicity.
## Deploy in 5 Minutes
```bash
# 1. Create from template
coho create my-drip-campaign --template drip-email-workflow
cd my-drip-campaign
npm install
# 2. Deploy
coho deploy
# 3. Configure email provider (pick one)
coho set-env EMAIL_PROVIDER "sendgrid"
coho set-env SENDGRID_API_KEY "SG.your-key"
coho set-env FROM_EMAIL "noreply@yourdomain.com"
coho set-env FROM_NAME "Your Company"
# 4. Add a subscriber
curl -X POST https://your-project.api.codehooks.io/dev/subscribers \
-H "Content-Type: application/json" \
-H "x-apikey: YOUR_API_KEY" \
-d '{"name":"Jane Doe","email":"jane@example.com"}'
```
Done. Your drip campaign is running.
## Why Codehooks for Webhooks?
Building reliable webhook infrastructure traditionally requires stitching together:
- Serverless functions (AWS Lambda)
- API Gateway for HTTPS endpoints
- Database (MongoDB Atlas)
- Cron scheduler (CloudWatch Events)
- Queue system (SQS) for async processing
- Retry logic and dead letter queues
- Environment secrets management
That's a lot of DevOps for receiving a webhook.
Codehooks gives you all of this in **one platform**, purpose-built for webhooks:
```javascript
import { app, Datastore } from 'codehooks-js';
// Webhook endpoint - receives events from any service
app.post('/subscribers', async (req, res) => {
const conn = await Datastore.open();
await conn.insertOne('subscribers', { email: req.body.email });
res.json({ success: true });
});
// Webhook from Stripe - handle payment events
app.post('/stripe-webhook', async (req, res) => {
const event = req.body;
if (event.type === 'checkout.session.completed') {
const conn = await Datastore.open();
await conn.insertOne('subscribers', {
email: event.data.object.customer_email,
name: event.data.object.customer_details.name,
source: 'stripe'
});
}
res.json({ received: true });
});
// Built-in cron job - process subscribers periodically
app.job('*/15 * * * *', async (req, res) => {
// Find subscribers ready for next email
// Queue them for sending
});
// Built-in queue worker - handle async email delivery
app.worker('send-email', async (req, res) => {
// Send the email
});
export default app.init();
```
No Docker. No Kubernetes. No infrastructure YAML files. Just write webhook handlers and deploy.
## The Architecture Scales
Here's what makes this production-ready:
### 1. Streaming Architecture
Instead of loading all subscribers into memory:
```javascript
// ❌ Memory issues with large lists
const subscribers = await conn.getMany('subscribers').toArray();
for (const sub of subscribers) {
await processSubscriber(sub);
}
// ✅ Constant memory usage
await conn.getMany('subscribers').forEach(async (sub) => {
await processSubscriber(sub);
});
```
Handles 100k subscribers as easily as 100.
### 2. Race Condition Prevention
The cron job atomically marks emails as sent **before** queueing:
```javascript
const result = await conn.updateOne(
'subscribers',
{
_id: subscriber._id,
emailsSent: { $nin: [step] } // Only if not already sent
},
{ $push: { emailsSent: step } }
);
// Only queue if we won the race
if (result) {
await conn.enqueue('send-email', { subscriberId, step });
}
```
No duplicate emails even if cron jobs overlap.
### 3. Automatic Retry on Failure
If email sending fails, the worker removes it from the sent list:
```javascript
catch (error) {
await conn.updateOne(
'subscribers',
{ _id: subscriberId },
{ $pull: { emailsSent: step } }
);
// Next cron run will retry
}
```
Ensures delivery without manual intervention.
## Real-World Webhook Integrations
### Stripe Webhooks → Onboarding Emails
When a customer completes checkout, automatically start their onboarding sequence:
```javascript
app.post('/stripe-webhook', async (req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(req.rawBody, sig, webhookSecret);
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
const conn = await Datastore.open();
await conn.insertOne('subscribers', {
email: session.customer_email,
name: session.customer_details.name,
plan: session.metadata.plan,
source: 'stripe_checkout'
});
}
res.json({ received: true });
});
```
Point your Stripe webhook to `https://your-project.api.codehooks.io/dev/stripe-webhook` and customers automatically enter your drip sequence.
### Signup Form Webhooks
Connect any form provider—Typeform, Tally, your own frontend:
```javascript
app.post('/subscribers', async (req, res) => {
const conn = await Datastore.open();
await conn.insertOne('subscribers', {
email: req.body.email,
name: req.body.name,
source: req.body.source || 'api'
});
res.json({ success: true });
});
```
### Webhook from Your App
Add users to campaigns directly from your application:
```javascript
// In your app's signup flow
await fetch('https://your-project.api.codehooks.io/dev/subscribers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-apikey': process.env.DRIP_API_KEY
},
body: JSON.stringify({
name: user.name,
email: user.email,
source: 'app_signup'
})
});
```
The cron job handles everything else automatically.
### Test Without Sending Emails
```bash
coho set-env DRY_RUN "true"
# Watch logs to verify workflow logic
coho logs --follow
```
No redeployment needed—DRY_RUN is checked at runtime.
### Switch Email Providers
```bash
# Currently using SendGrid? Switch to Mailgun:
coho set-env EMAIL_PROVIDER "mailgun"
coho set-env MAILGUN_API_KEY "your-key"
coho set-env MAILGUN_DOMAIN "mg.yourdomain.com"
```
Your campaign keeps running. Zero downtime.
### Handle Email Provider Webhooks
Receive delivery status, bounces, and unsubscribes from your email provider:
```javascript
// SendGrid Event Webhook
app.post('/sendgrid-events', async (req, res) => {
const events = req.body;
const conn = await Datastore.open();
for (const event of events) {
if (event.event === 'bounce' || event.event === 'dropped') {
await conn.updateOne('subscribers',
{ email: event.email },
{ $set: { status: 'bounced', bouncedAt: new Date() }}
);
}
if (event.event === 'unsubscribe') {
await conn.updateOne('subscribers',
{ email: event.email },
{ $set: { status: 'unsubscribed' }}
);
}
}
res.json({ received: true });
});
```
Your email provider sends events to your webhook, and your campaign stays clean automatically.
## The Cost Breakdown
Here's what running a production drip campaign actually costs:
### For 5,000 subscribers (3-step sequence):
**Self-hosted**:
- Codehooks Pro: $19/month
- SendGrid Essentials: $19.95/month (50k emails/month)
- **Total: $467/year**
**Email automation SaaS**:
- Mailchimp Standard: ~$55-85/month = **$660-1,020/year**
- ActiveCampaign: ~$125/month = **$1,500/year**
- ConvertKit: Custom pricing, typically **$700-900/year**
**Savings: $193-1,033/year** while owning your infrastructure.
### For 50,000 subscribers:
**Self-hosted**:
- Codehooks Pro: $19/month
- SendGrid Pro: $89.95/month (up to 700k emails/month)
- **Total: $1,307/year**
**Email automation SaaS**:
- Mailchimp: ~$300-400/month = **$3,600-4,800/year**
- ActiveCampaign: ~$500+/month = **$6,000+/year**
- ConvertKit: Custom enterprise pricing = **$4,000-6,000+/year**
**Savings: $2,293-4,693/year** at scale.
The difference? You own the code, control the data, and can switch email providers anytime. No vendor lock-in, no surprise overage fees, complete transparency.
## Get Started
The drip email workflow template is open source and ready to deploy:
```bash
npm install -g codehooks
coho create my-campaign --template drip-email-workflow
cd my-campaign
coho deploy
```
**What's included:**
- Webhook endpoints for subscriber management (connect Stripe, forms, your app)
- Built-in cron jobs and queue workers
- Professional responsive email templates
- Email provider webhooks for bounces/unsubscribes
- SendGrid, Mailgun, and Postmark integrations
- Example configurations for common use cases
Browse all templates: [codehooks.io/templates](https://codehooks.io/docs/examples/examples-overview)
## When to Use This
This webhook-driven approach is perfect for:
- **Event-driven onboarding** - Stripe checkout → welcome sequence
- **Multi-source campaigns** - Aggregate subscribers from forms, apps, payment events
- **Course delivery** - Timed content drips triggered by purchase webhooks
- **Lead nurturing** - Connect your CRM webhooks to email automation
But maybe **not** ideal for:
- Transactional emails with <1min SLA (use a dedicated service)
- Teams that need no-code visual builders
- Simple newsletters without automation logic
## Why This Matters
Webhooks are the backbone of modern SaaS integrations. But building reliable webhook infrastructure—endpoints that validate, store, queue, and process events—shouldn't require a DevOps team.
With Codehooks, you get:
1. **Webhook-native platform** - Purpose-built for receiving and processing events
2. **Instant deployment** - From zero to production webhook endpoints in 5 minutes
3. **Built-in reliability** - Queues, retries, and cron jobs out of the box
4. **Proven scalability** - Streaming architecture handles 100k+ events
5. **Complete flexibility** - Connect Stripe, forms, your app—any webhook source
The template showcases what's possible: webhook endpoints, cron scheduling, queue workers, database, email delivery—all in one platform. No Docker, no Kubernetes, no infrastructure complexity.
**This is what Codehooks is built for: turning webhooks into action.**
## Future Email Provider Integrations
The drip email template currently supports SendGrid, Mailgun, and Postmark—but we're actively expanding provider support based on community feedback.
### Coming Soon
**Amazon SES** - AWS's cost-effective email service ($0.10 per 1,000 emails) is perfect for high-volume campaigns. With its proven reliability and deep AWS integration, SES is a natural fit for teams already on AWS infrastructure.
**Brevo (formerly Sendinblue)** - European privacy-focused alternative with generous free tier (300 emails/day) and advanced marketing automation features. Great for GDPR-conscious teams and startups testing their first drip campaigns.
**Postal (Open Source)** - For teams that want complete control, Postal offers a self-hosted email delivery platform. Deploy it alongside your Codehooks webhooks and own your entire email infrastructure—from webhook to inbox.
### Why Multiple Providers Matter
Email deliverability isn't one-size-fits-all:
- **Geographic optimization** - SES for US, Brevo for EU compliance
- **Failover resilience** - Switch providers if deliverability drops
- **Cost optimization** - Test providers to find your best rate
- **Exit strategy** - Never locked into one vendor
The template's provider abstraction makes adding new integrations straightforward—just implement the email sending interface and configure the environment variables.
**Want a specific provider?** [Open an issue](https://github.com/codehooks-io/templates) or contribute an integration. The template is open source and accepts pull requests.
---
## Next Steps
**New to Codehooks?** [Sign up for free](https://codehooks.io) and deploy your first webhook endpoint:
```bash
npm install -g codehooks
coho create my-drip-campaign --template drip-email-workflow
```
**Explore the code:** Browse the complete [template source code on GitHub](https://github.com/RestDB/codehooks-io-templates/tree/main/drip-email-workflow) to see how it works.
**Want to explore more?** Check out our [backend API integration guide](/blog/api-integration-made-easy), other [templates](https://codehooks.io/docs/examples/examples-overview), [webhook documentation](https://codehooks.io/docs/#api-and-webhook-development), and [queue workers](https://codehooks.io/docs/#queues).
**Questions?** Email us at [info@codehooks.io](mailto:info@codehooks.io).
---
*This template is open source and production-ready. Deploy your webhook endpoints today and take back control of your automation stack.*
---
## Auto-Generate OpenAPI Docs From Your Code
*(Note: This content contains MDX/JSX code)*
Your API is only as useful as its documentation. Without clear docs, developers waste hours reverse-engineering endpoints, partners struggle to integrate, and AI agents can't understand your API at all.
But writing OpenAPI specs by hand? That's tedious, error-prone, and constantly out of sync with your actual code.
**What if your documentation wrote itself?**
With the latest codehooks-js update, it does. Define your schemas once, and Codehooks generates complete OpenAPI 3.0 documentation—served as an interactive Swagger UI at `/docs`.
## Why API Documentation Matters
Good API documentation isn't optional—it's the difference between adoption and abandonment:
- **Developers** need clear examples to integrate quickly
- **Partners** require accurate specs for their systems
- **Customers** expect self-service API access
- **AI agents** (like ChatGPT, Claude, and custom LLMs) need structured specs to call your APIs correctly
The [OpenAPI specification](https://www.openapis.org/) has become the industry standard. It powers [Swagger UI](https://swagger.io/tools/swagger-ui/), [Postman](https://www.postman.com/), and countless code generators. But maintaining it manually creates a constant documentation burden.
## The DRY Approach
[DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) (Don't Repeat Yourself) means defining things once and reusing them everywhere. The best documentation comes from your code itself. With Codehooks, you focus on what matters:
1. **Define your schemas** (using [Zod](https://zod.dev/), [Yup](https://github.com/jquense/yup), or [JSON Schema](https://json-schema.org/))
2. **Create your API routes**
3. **Deploy**
That's it. Codehooks handles the OpenAPI generation automatically.
## Quick Start: Crudlify + Schemas
The fastest path to documented APIs is `crudlify`. Define a schema, and you get full CRUD endpoints with complete OpenAPI docs:
```javascript
import { app } from 'codehooks-js';
import { z } from 'zod';
// Define your schema once
const todoSchema = z.object({
title: z.string().min(1).max(200).describe('Task title'),
completed: z.boolean().default(false).describe('Completion status'),
priority: z.enum(['low', 'medium', 'high']).default('medium')
});
// Enable OpenAPI documentation
app.openapi({
info: {
title: 'Todo API',
version: '1.0.0',
description: 'A simple task management API'
}
});
// Generate CRUD endpoints with validation and docs
app.crudlify({ todos: todoSchema });
export default app.init();
```
Deploy with `coho deploy`, then visit:
- **`/docs`** — Interactive Swagger UI
- **`/openapi.json`** — Raw OpenAPI specification
You now have 8 fully documented endpoints:
| Method | Path | Operation |
|--------|------|-----------|
| POST | `/todos` | Create todo |
| GET | `/todos` | List todos |
| GET | `/todos/{ID}` | Get by ID |
| PUT | `/todos/{ID}` | Replace |
| PATCH | `/todos/{ID}` | Update |
| DELETE | `/todos/{ID}` | Delete |
| PATCH | `/todos/_byquery` | Batch update |
| DELETE | `/todos/_byquery` | Batch delete |
Each endpoint includes request/response schemas, validation rules, and examples—all generated from your Zod schema.

*Example: Auto-generated Swagger UI for the Todo API with all CRUD endpoints documented*
The generated Swagger UI is fully functional—not just documentation. Enter your API token in the authorize dialog, and you can test all endpoints directly from the browser.
## Custom Routes with OpenAPI Specs
Not everything fits the CRUD pattern. For custom endpoints, use the `openapi()` middleware helper:
```javascript
import { app, openapi } from 'codehooks-js';
app.get('/health',
openapi({
summary: 'Health check',
tags: ['System'],
responses: {
200: { description: 'Service is healthy' }
}
}),
(req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
}
);
app.post('/analyze',
openapi({
summary: 'Analyze text',
tags: ['AI'],
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['text'],
properties: {
text: { type: 'string', maxLength: 10000 },
language: { type: 'string', default: 'en' }
}
}
}
}
},
responses: {
200: { description: 'Analysis complete' },
400: { description: 'Invalid input' }
}
}),
async (req, res) => {
// Your analysis logic
}
);
```
The `openapi()` middleware attaches OpenAPI metadata to your route without affecting its behavior. Your documentation stays next to your code—always in sync.
## Using Zod Schemas in Custom Routes
For the best DRY experience, use your Zod schemas directly in custom routes:
```javascript
import { app, Datastore, openapi } from 'codehooks-js';
import { z } from 'zod';
const UserSchema = z.object({
name: z.string().min(1).max(100).describe('Full name'),
email: z.string().email().describe('Email address'),
role: z.enum(['admin', 'user']).default('user')
});
// Validation middleware
function validate(schema) {
return async (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
res.status(400).json({
error: 'Validation failed',
details: error.issues
});
}
};
}
// Schema used for BOTH validation AND OpenAPI docs
app.post('/users',
openapi({
summary: 'Create user',
tags: ['Users'],
requestBody: {
required: true,
content: {
'application/json': {
schema: UserSchema // Zod schema auto-converts to JSON Schema
}
}
},
responses: {
201: { description: 'User created' },
400: { description: 'Validation error' }
}
}),
validate(UserSchema),
async (req, res) => {
const db = await Datastore.open();
const user = await db.insertOne('users', req.body);
res.status(201).json(user);
}
);
```
One schema. Validation and documentation. No duplication.
## Advanced Configuration
For production APIs, you'll want more control:
```javascript
app.openapi({
info: {
title: 'Production API',
version: '2.0.0',
description: 'Full-featured API with authentication'
},
// Multiple environments
servers: [
{ url: 'https://myapi.api.codehooks.io/dev', description: 'Development' },
{ url: 'https://myapi.api.codehooks.io/prod', description: 'Production' }
],
// Organize endpoints
tags: [
{ name: 'Users', description: 'User management' },
{ name: 'Orders', description: 'Order processing' },
{ name: 'System', description: 'Health and status' }
],
// Filter what appears in docs
filter: (op) => {
// Hide internal endpoints
if (op.path.startsWith('/internal')) return false;
// Hide batch delete (dangerous)
if (op.method === 'delete' && op.path.includes('_byquery')) return false;
return true;
},
// Link to external docs
externalDocs: {
description: 'Full API Guide',
url: 'https://docs.mycompany.com/api'
}
}, '/api-docs'); // Custom Swagger UI path
```
### Filtering Operations
The `filter` function gives you precise control over what appears in your documentation:
```javascript
// Only show public endpoints
filter: (op) => op.tags.includes('Public')
// Hide all DELETE operations
filter: (op) => op.method !== 'delete'
// Exclude admin routes
filter: (op) => !op.path.startsWith('/admin')
```
## Schema Support
Codehooks supports the three most popular schema libraries:
### [Zod](https://zod.dev/)
```javascript
const schema = z.object({
email: z.string().email(),
age: z.number().min(0).max(150).optional()
});
```
### [Yup](https://github.com/jquense/yup)
```javascript
const schema = yup.object({
email: yup.string().email().required(),
age: yup.number().min(0).max(150)
});
```
### [JSON Schema](https://json-schema.org/)
```javascript
const schema = {
type: 'object',
required: ['email'],
properties: {
email: { type: 'string', format: 'email' },
age: { type: 'integer', minimum: 0, maximum: 150 }
}
};
```
All three automatically convert to OpenAPI-compatible JSON Schema in your documentation.
## Full Example
Here's a complete API with multiple collections, custom endpoints, and full OpenAPI configuration:
```javascript
import { app, Datastore, openapi } from 'codehooks-js';
import { z } from 'zod';
// Schemas
const productSchema = z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'books']),
inStock: z.boolean().default(true)
});
const orderSchema = z.object({
productId: z.string(),
quantity: z.number().int().positive(),
status: z.enum(['pending', 'shipped', 'delivered']).default('pending')
});
// OpenAPI config
app.openapi({
info: {
title: 'E-Commerce API',
version: '1.0.0',
description: 'Product catalog and order management'
},
tags: [
{ name: 'Products', description: 'Product catalog' },
{ name: 'Orders', description: 'Order management' },
{ name: 'Analytics', description: 'Business insights' }
],
filter: (op) => !op.path.includes('_byquery')
});
// Custom analytics endpoint
app.get('/analytics/sales',
openapi({
summary: 'Get sales summary',
tags: ['Analytics'],
parameters: [
{ name: 'period', in: 'query', schema: { type: 'string', enum: ['day', 'week', 'month'] } }
],
responses: {
200: { description: 'Sales data' }
}
}),
async (req, res) => {
const db = await Datastore.open();
const orders = await db.getMany('orders', { status: 'delivered' }).toArray();
res.json({
totalOrders: orders.length,
period: req.query.period || 'all'
});
}
);
// Auto-generate CRUD for both collections
app.crudlify({
products: productSchema,
orders: orderSchema
});
export default app.init();
```
Deploy and visit `/docs` to explore your complete, interactive API documentation.
## Summary
Stop treating documentation as an afterthought. With Codehooks OpenAPI support:
- **Define schemas once** — validation and docs from the same source
- **Use crudlify** — get 8 documented endpoints per collection instantly
- **Add custom routes** — the `openapi()` middleware keeps specs next to code
- **Filter and configure** — control exactly what appears in your docs
- **Deploy and share** — Swagger UI at `/docs`, spec at `/openapi.json`
Your API consumers—whether developers, partners, or AI agents—will thank you.
**Get started:**
```bash
npm install codehooks-js@latest
```
Then add `app.openapi()` to your project and deploy. Your documentation is ready.
---
*For the complete API reference, see the [OpenAPI Documentation guide](/docs/openapi-swagger-docs).*
import FAQSection from '@site/src/components/FAQSection';
op.method !== 'delete'` hides all DELETE endpoints, or `filter: (op) => !op.path.startsWith('/internal')` hides internal routes."
},
{
q: "Can I customize the Swagger UI path?",
a: "Yes! Pass a second parameter to `app.openapi()`: `app.openapi({ info: {...} }, '/api-docs')`. The Swagger UI will be available at `/api-docs` instead of the default `/docs`."
},
{
q: "Do I need to write OpenAPI specs manually for crudlify endpoints?",
a: "No! When you use `app.crudlify()` with a schema, all 8 CRUD endpoints are automatically documented with proper request/response schemas, validation rules, and examples."
},
{
q: "How do I document custom routes that aren't part of crudlify?",
a: "Use the `openapi()` middleware: `app.get('/custom', openapi({ summary: '...', responses: {...} }), handler)`. You can pass Zod schemas directly to `requestBody` for automatic schema conversion."
},
{
q: "Can AI agents use my OpenAPI documentation?",
a: "Yes! The `/openapi.json` endpoint provides a machine-readable spec that AI agents, code generators, and API clients can use to understand and interact with your API automatically."
}
]}
/>
## Further Reading
- [OpenAPI Specification](https://spec.openapis.org/oas/latest.html) — The official OpenAPI 3.0 specification
- [Swagger UI](https://swagger.io/tools/swagger-ui/) — Interactive API documentation from OpenAPI specs
- [Zod Documentation](https://zod.dev/) — TypeScript-first schema validation
- [JSON Schema](https://json-schema.org/learn/getting-started-step-by-step) — Getting started with JSON Schema
- [Codehooks REST API Routing](/docs/rest-api-app-routes) — Learn more about creating API routes
- [Codehooks Quickstart](/docs/quickstart-cli) — Get up and running with Codehooks