# 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 =
body text here
, index = -1, }) => { return (
{icon} {title}
{index > -1 ? (
{' '}
) : null}
{content}
); }; export const Feature = ({ icon, title = 'No title', content =
body text here
, nextsteps = [], }) => { return (
{icon}

{title}

{content}
{nextsteps.length > 0 && (

Learn more

    {nextsteps.map((step, i) => (
  • {step}
  • ))}
)}
); }; ## 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) :::
} title="API & Webhook Development" content="REST, CRUD, custom APIs & webhooks" /> } title="Datastore" content="NoSQL & Key-Value" /> } title="Scheduled Jobs & Events" content="CRON schedules & event triggers" /> } title="Queues" content="Process webhooks asynchronously" /> } title="Workflows" content="Chain webhooks into reliable processes" /> } title="File System" content="Serve static files" /> } title="Data Aggregation" content="Groups & sums" /> } title="Hosting" content="Let codehooks serve your frontend" />
:::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, ]} />
} title="Auto-generate OpenAPI documentation" content={
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.
{`import { app } from 'codehooks-js'; import { z } from 'zod'; const todoSchema = z.object({ title: z.string().min(1).max(200), completed: z.boolean().default(false) }); // Enable OpenAPI docs at /docs app.openapi({ info: { title: 'Todo API', version: '1.0.0' } }); app.crudlify({ todos: todoSchema }); export default app.init();`}
After deploying, visit /docs for interactive Swagger UI with full authentication support, or /openapi.json for the raw specification.
} nextsteps={[ OpenAPI documentation guide, Document custom routes, Schema library support, ]} />
## NoSQL and Key-Value Datastores
} 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.

{codex5}
} nextsteps={[ Job API, Crontab guru - interactive CRON expression builder , ]} />
## Queues
} 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.
{codex6}
} nextsteps={[Queue API]} />
## Reliable Workflows
} title="Build reliable stateful workflows" content={
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:
$ coho file-upload --projectname 'myproject-fafc' --space 'dev' --src '/somedir/' --target '/myblobs'
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. --- ## Concepts overview *(Note: This content contains MDX/JSX code)* Codehooks.io is the agent-native backend platform. A complete backend — API routes, database, queues, workers, cron, auth, and frontend hosting — that deploys in seconds from the CLI. Your coding agent creates, deploys, verifies, and iterates without ever leaving the terminal. Forget about assembling Lambda + API Gateway + DynamoDB + SQS — just focus on your business logic. Codehooks includes these main features: - **Studio** - Developer friendly web based tools for managing both data and code - **Node.js and JavaScript** - Same language for frontend and backend development (ES6 and Typescript) - **NPM** - Node.js package manager (npm) integration - **Node.js Express** - API development with Express-like API - **NoSQL** - Document database with Mongodb-like API - **Key-Value** - Key-Value database with a Redis-like API (subset) - **Worker Queues** - Persistent queues with worker functions - **Background jobs** - Cron jobs with scheduled worker functions - **CLI** - Powerful Command Line Interface (CLI) for productivity and DevOps/CI support Read on to learn more about how Codehooks.io simplifies backend development — whether you're building directly or through a coding agent. > Unless you've already have, please read the [quick start](/docs) first to learn how to install the CLI, and how to sign up/login to create your first Codehooks project. ## Codehooks main concepts ### Project A project is the top level concept in Codehooks. A project is owned by an account (private or company) and contains one or multiple "spaces" with your deployed code, data and access credentials in an isolated and secure unit. You can create multiple projects within your account and you can join/invite others to a project. An example project structure is shown below: - jane@example.com (private or corporate account owner) - **customers** (project name) - **dev** (development space) - Database instance - Security settings (API tokens, environment secrets, IP filters ...) - Source code (deployed version 1.1.12) - Developers (joe@example.com, pete@example.com) - **prod** (production space) - Database instance - Security settings (API tokens, environment secrets, IP filters ...) - Source code (deployed version 1.1.0) - Developers (joe@example.com, pete@example.com) - **salesdb** (project name for another project) - ... Projects are created with the CLI command `coho create` ([CLI docs here](cli#create)) :::info note You can also manage your projects and spaces using the account user interface at [https://account.codehooks.io](https://account.codehooks.io). ::: ### The project application source code A Codehooks application follows the familiar principles and best practices of modern JavaScript development. A project directory typically contains: - An `index.js` file for the application main entry code - A `package.json` file with library dependencies - A `config.json` file with project information We also recommend that you add source control (GIT) to manage your versions and branches. Typically this will be a git repo with branches that are deployed to various spaces. ### A minimal example The following CLI commands create a project and deploy it to the Codehooks cloud. These are the same commands a coding agent like Claude Code or Cursor would run autonomously. Create and setup a new project application: ```bash coho create example cd example npm init --yes npm install codehooks-js --save ``` After running the commands your project directory contains these files: ```bash . ├── config.json ├── index.js ├── node_modules ├── package-lock.json └── package.json ``` If you inspect the source file index.js you'll see the default generated application code: ```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(); ``` We can now deploy the application (in our case example-4g9p) to the cloud with the CLI command `coho deploy` ([CLI docs here](cli#deploy)). ```bash coho deploy # Server output example Project: example-4g9p Space: dev Deployed Codehook successfully! 🙌 ``` After deployment we can inspect the application details with the `coho info` command, in this example showing our default space `dev` with its base endpoint URL and access token(s): ```bash coho info --examples Project name: example-4g9p Team: YOUR-NAME (personal account) API endpoint: https://example-4g9p.api.codehooks.io/dev/* Spaces: ┌──────────────┬────────────────────────────────────────────┬──────┬──────┐ │ Name │ Tokens │ Jwks │ Env │ ├──────────────┼────────────────────────────────────────────┼──────┼──────┤ │ dev (active) │ a77926ca-xxx-yyyy-zzzzz-14eee564f8d5 (RW) │ │ │ └──────────────┴────────────────────────────────────────────┴──────┴──────┘ ``` After successful deployment we can test our application endpoint with curl. This minimal example and test shows that our application is deployed to a secure endpoint and returns the expected result: ```bash curl -X GET 'https://example-4g9p.api.codehooks.io/dev' \ -H 'x-apikey: a77926ca-xxx-yyyy-zzzzz-14eee564f8d5' # API output example from server CRUD server ready ``` ### Project spaces A project space is a self-contained and isolated bundle of code, datastores and security settings within a specific project. All projects have a default space called `dev` which is created automatically when you add a project. You create new spaces with the `coho add` command ([CLI docs here](cli#add)). You can easily switch between different project spaces with the `coho use` command ([CLI docs here](cli#use)). Spaces can also have restricted access (team ADMINs only), which is convenient for production deployments. For example, in your project you want to have isolated development, testing and production spaces. ``` Project-X └── dev    ├── source code (git branch dev)    ├── security settings & env variables    └── data stores └── test    ├── source code (git branch test)    ├── security settings & env variables    └── data stores └── prod (restricted)    ├── source code (git branch prod)    ├── security settings & env variables    └── data stores ``` ### Deploy your application to the Codehooks serverless cloud Each project space is created as an isolated resource group in the Codehooks cloud. For example, the `coho add prod` command creates a project space called `prod`. By switching to the space with the `coho use prod` command, this space is now the active (changed in the config.json project file). On deployment with the `coho deploy` command, the current version of your application source code is packed and distributed as a serverless runtime to the **prod** space in the Codehook cloud. Similarily, switching to another space, e.g. by using the `coho use dev`, sets the **dev** space as the active, and `coho deploy` will deploy the application to the active **dev** space. This is a powerful feature, which effectively enables you to run multiple versions of your application with separate settings, environments and data stores. ### Built in Datastores Persisting, querying and manipulating data is essential in any application. Codehooks comes with a built in datastore that supports **streaming** NoSQL and Key-Value operations. No need to think about drivers and protocols, it's built directly into the Codehooks APIs. - NoSQL object data are accessed with the standard [Database API](nosql-database-api). - Key-Value data are accessed with the standard [Key-Value API](key-value-database-api) #### Data wrangling Using the CLI command `coho import`, you can easily import any CSV or JSON file into a datastore. Furthermore, the `coho query` command lets you query and transform data in advanced ways. ```bash title="Example: import and query of Stock data" coho import -c stocks -f ~/Downloads/all_stocks_5yr.csv coho query stocks --query 'Name=GOOG&open>10' --limit 2 --table ┌───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┐ │ date │ open │ high │ low │ close │ volume │ Name │ _id │ ├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 2013-03-… │ 15.98 │ 16.36 │ 15.93 │ 16.25 │ 8383300 │ GOOG │ 18007b5f… │ ├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 2013-03-… │ 14.7 │ 14.93 │ 14.5 │ 14.82 │ 9125300 │ GOOG │ 18007b5f… │ └───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┘ ``` ### Fast development flow The development process with Codehooks is fast — whether you're working in an editor or your coding agent is driving the CLI. A typical development flow: 1. Write or generate code in `index.js` and other source files 2. Set the active space, e.g. `coho use dev` 3. Deploy with `coho deploy` 4. Check logs with `coho log --follow` 5. Fix issues and redeploy with `coho deploy` Every step is a CLI command, which means coding agents can run the full create → deploy → verify → iterate loop without human intervention. ### Command Line Interface (CLI) The Codehooks CLI is how you — or your coding agent — manage projects and applications. Every operation is available as a CLI command: creating projects, deploying code, managing secrets, querying data, and checking logs. This CLI-first design is what makes Codehooks agent-native. Codehooks also has a complete set of features for teams to collaborate. The [account UI](https://account.codehooks.io) lets you create teams and invite team members. Read more about the CLI and the full set of commands and features [here](cli). :::info note You can also manage your projects and spaces using the account user interface at [https://account.codehooks.io](https://account.codehooks.io). ::: --- ## 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: --- ## Data aggregation API *(Note: This content contains MDX/JSX code)* The JSON aggregation specification is designed to provide a structured way to aggregate JSON data streams. It offers several operators to aid in the summarization and grouping of data streams without the need for extensive code. **Aggregation Operators** Our specification currently supports the following operators: - `$max`: Determines the maximum value for the specified field(s). - `$min`: Determines the minimum value for the specified field(s). - `$avg`: Computes the average value for the specified field(s). - `$sum`: Sums up the values for the specified field(s). - `$group`: Group by specified field and perform nested aggregations. Each operator can take either a string denoting the field name or an object with additional properties such as $field (for specifying the field name) and $label (for specifying a custom label for the result). ## Aggregation data from a NoSQL query JSON data `aggregation` is a standard library that is imported in your Codehooks app. The example below shows how to use the library agains a `salesData` NoSQL data collection. ```js {5-8,12} import { app, datastore, aggregation } from 'codehooks-js'; app.post('/agg', async (req, res) => { try { const spec = { $max: 'sales', $avg: 'sales', }; const db = await datastore.open(); const dbstream = db.getMany('salesData', { active: true }); const result = await aggregation(dbstream, spec); console.log(result); res.json(result); } catch (error) { console.log(error.message); res.status(400).end(error); } }); export default app.init(); // Bind functions to the serverless runtime ``` **Example output** ```json { "count": 120500, "sales": { "max": 1234.0, "avg": 123.0, "sum_total": 123456.0 } } ``` ## Operators ### `$max` - **Description:** Determines the maximum value for the specified field(s). - **Usage:** - `$max: fieldName` for a simple field. - `$max: { $field: fieldName, $label: customLabel }` for specifying a custom label. - **Example:** ```json { "$max": { "$field": "sales", "$label": "Max Sales" } } ``` ### `$min` - **Description:** Determines the minimum value for the specified field(s). - **Usage:** - `$min: fieldName` for a simple field. - `$min: { $field: fieldName, $label: customLabel }` for specifying a custom label. - **Example:** ```json { "$min": { "$field": "sales", "$label": "Min Sales" } } ``` ### `$avg` - **Description:** Computes the average value for the specified field(s). - **Usage:** - `$avg: fieldName` for a simple field. - `$avg: { $field: fieldName, $label: customLabel }` for specifying a custom label. - **Example:** ```json { "$avg": { "$field": "sales", "$label": "Average Sales" } } ``` ### `$sum` - **Description:** Sums up the values for the specified field(s). - **Usage:** - `$sum: fieldName` for a simple field. - `$sum: { $field: fieldName, $label: customLabel }` for specifying a custom label. - **Example:** ```json { "$sum": { "$field": "sales", "$label": "Total Sales" } } ``` ### `$group` - **Description:** Group by specified field and perform nested aggregations. - **Usage:** - `$group: { $field: fieldName, ...nestedAggregations }` to group by a field and apply further aggregations. - **Example:** ```json { "$group": { "$field": "customerType", "$label": "Customer Segment", "$max": "sales" } } ``` #### Example: Complex grouping and aggregation ```js const spec = { $max: { $field: 'sales', $label: 'All time top sales' }, $group: [ { $field: 'customerType', $label: 'Segment', $max: 'sales', $min: { $field: 'sales', $label: 'Low' }, $group: { $field: 'year', $group: { $field: 'month', $max: ['sales', { $field: 'volume', $label: 'Sales volume' }], }, }, }, { $field: 'day', $max: 'sales', }, ], }; ``` Our JSON aggregation specification provides a robust tool for developers seeking powerful and streamlined data processing capabilities. Harnessing these operators allows for comprehensive insights into your data streams. --- ## AI Agent Setup > A comprehensive guide for setting up AI coding agents to build Codehooks.io backend APIs. Covers the Claude Code plugin, CLI prompt, MCP server, OpenClaw skill, and manual setup for all major LLM agents. ## What is Codehooks.io? Codehooks.io is the agent-native backend platform. It's 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. What makes it different is that 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. Your agent handles the rest. ## Getting Started Choose the setup method that matches your AI coding tool: - **[Claude Code Plugin](#claude-code-plugin-recommended)** — Recommended for Claude Code users. Auto-detection, templates, and full API context. - **[CLI Prompt](#agents-with-terminal-access)** — For any agent with terminal access (Codex CLI, Cursor, Aider, Windsurf). - **[MCP Server](#mcp-compatible-tools)** — Fallback for tools without terminal access (Claude Desktop, Cline, Zed). Prefer CLI when available. ## Setup for AI Coding Agents ### Claude Code Plugin (Recommended) **Best for:** Claude Code users who want the fastest, most integrated experience The [Codehooks Claude Plugin](https://github.com/RestDB/codehooks-claude-plugin) gives Claude Code auto-detection of Codehooks projects, built-in templates, and a `/codehooks:backend` command for scaffolding production-ready backends from natural language. **What it provides:** - **Auto-detection** — Claude Code automatically recognizes Codehooks projects and loads platform context - **CLI integration** — Deploy, query, and manage your backend directly from the conversation - **Templates** — Pre-built patterns for webhooks, CRUD APIs, cron jobs, queues, and workflows - **`/codehooks:backend` command** — Activate full backend development mode with a single slash command **Installation:** ``` /plugin marketplace add RestDB/codehooks-claude-plugin /plugin install codehooks@codehooks ``` Once installed, open any Codehooks project and use `/codehooks:backend` to start building. ### Agents with Terminal Access **Best for:** Codex CLI, Cursor, Aider, Windsurf, and other agents that can run shell commands If your AI agent has terminal access, the Codehooks CLI provides the fastest setup with the `prompt` command: ```bash # Install the CLI npm install -g codehooks # Display the prompt - pipe it to your agent or copy to clipboard coho prompt # Example: Copy to clipboard on macOS coho prompt | pbcopy ``` The CLI also gives your agent direct access to deployment, database operations, logs, and more. See the [CLI documentation](/docs/cli) for all available commands. ### MCP-Compatible Tools **Best for:** Claude Desktop, Cline, Zed, and other tools that support the Model Context Protocol but lack terminal access For tools that support MCP, the Codehooks MCP server provides deploy, query, and management capabilities: https://github.com/RestDB/codehooks-mcp-server **Note:** If your agent can run shell commands, always prefer the CLI over MCP — it's faster, more capable, and gives the agent direct access to the full `coho` toolchain. ## Agent Skills & Templates For AI agents that support skills or pre-built tool integrations, you can use the **Codehooks OpenClaw Skill** to get started even faster. The [codehooks-openclaw-skill](https://github.com/RestDB/codehooks-openclaw-skill) provides pre-built patterns for common backend tasks: - **Webhook handlers** with signature verification (Stripe, GitHub, etc.) - **CRUD REST APIs** with validation - **Cron jobs** for scheduled background work - **Worker queues** for async processing - **Multi-step workflows** with state management Install the skill in your OpenClaw agent and it can scaffold production-ready Codehooks backends from natural language descriptions. Read the [full walkthrough in the blog post](/blog/openclaw-backend) to see how OpenClaw agents use Codehooks as their serverless backend. ## Development Prompt The development prompt gives your AI agent full context on the Codehooks.io platform — API patterns, database operations, authentication, workers, cron, and more. **Access the prompt:** - **CLI:** `coho prompt` (pipe to your agent or copy to clipboard with `coho prompt | pbcopy`) - **Full docs:** [llms.txt](https://codehooks.io/llms.txt) — complete Codehooks documentation optimized for AI agents - **Source:** [codehooks-mcp-server](https://github.com/RestDB/codehooks-mcp-server) — the canonical prompt is maintained here ## Codehooks CLI for AI Agents The Codehooks CLI (`coho`) is designed for AI agents working with command-line tools. Key commands for AI development: | Command | Description | |---------|-------------| | `coho prompt` | Display the development prompt for AI assistants | | `coho docs` | Open the API cheatsheet | | `coho deploy` | Deploy code to the serverless cloud | | `coho log -f` | Stream logs in real-time | | `coho query ` | Query database collections | | `coho info --examples` | Show project info with cURL examples | | `coho doctor` | Comprehensive project diagnostics as JSON — collections, stats, deploys, and recent error logs. Useful for agents to diagnose issues. | | `coho describe` | Describe the deployed app structure — collections, schemas, queues, and files. Helps agents understand what's already deployed. | Full CLI documentation: https://codehooks.io/docs/cli For additional detailed information about the Codehooks.io platform, you can reference https://codehooks.io/llms.txt --- ## API Cheat Sheet *(Note: This content contains MDX/JSX code)* **Everything you need to build a serverless backend in one place.** This cheat sheet contains all essential backend APIs for creating complete serverless applications with Codehooks.io. Quick reference for routing, authentication, NoSQL databases, workflows, queues, key-value stores, and real-time features. Each API includes direct links to detailed documentation with examples and usage patterns. **→ [Authentication & Security](#authentication--security)** - Secure your APIs with tokens, JWT, OAuth, or custom auth methods. ## **HTTP Routing APIs** Create REST API endpoints using Express-style route handlers for all standard HTTP methods. Route handlers receive request and response objects for processing incoming HTTP requests. - **`post(path, ...middleware?, function)`** - Register POST route handlers → [Details](/docs/appeventapi#apppostroute-middleware-workerfunction) - **`get(path, ...middleware?, function)`** - Register GET route handlers → [Details](/docs/appeventapi#appgetroute-middleware-workerfunction) - **`put(path, ...middleware?, function)`** - Register PUT route handlers → [Details](/docs/appeventapi#appputroute-middleware-workerfunction) - **`patch(path, ...middleware?, function)`** - Register PATCH route handlers → [Details](/docs/appeventapi#apppatchroute-middleware-workerfunction) - **`delete(path, ...middleware?, function)`** - Register DELETE route handlers → [Details](/docs/appeventapi#appdeleteroute-middleware-workerfunction) - **`all(path, ...middleware?, function)`** - Register handlers for all HTTP methods → [Details](/docs/appeventapi#appallroute-middleware-workerfunction) ## **Middleware & Authentication APIs** Add middleware functions to process requests before they reach route handlers, and serve static or uploaded files. Authentication middleware intercepts requests without valid access tokens. - **`use(function)`** - Register global middleware → [Details](/docs/appeventapi#appuseworkerfunction) - **`auth(path, function)`** - Register authentication middleware → [Details](/docs/appeventapi#appauthroute-workerfunction) - **`static(options, function?)`** - Serve static files from source code → [Details](/docs/appeventapi#appstaticoptions-callback) - **`storage(options, function?)`** - Serve files from blob storage → [Details](/docs/appeventapi#appstorageoptions-callback) ## **Database/NoSQL APIs** Complete NoSQL document database with CRUD operations, schema validation, and MongoDB-style queries. All data is stored as JSON documents organized in collections. ### **Connection & Setup** - **`Datastore.open()`** - Connect to datastore and return API interface → [Details](/docs/nosql-database-api#datastoreopen) ### **Document Operations** - **`insertOne(collection, document)`** - Insert new document → [Details](/docs/nosql-database-api#insertonecollection-document) - **`getOne(collection, query)`** - Get single document by ID/query (throws if not found) → [Details](/docs/nosql-database-api#getonecollection-id--query) - **`findOne(collection, query)`** - Alias for getOne → [Details](/docs/nosql-database-api#findonecollection-id--query) - **`findOneOrNull(collection, query)`** - Get single document, returns null if not found → [Details](/docs/nosql-database-api#findoneornullcollection-id--query) - **`getMany(collection, query?, options?)`** - Get stream of documents → [Details](/docs/nosql-database-api#getmanycollection-query-options) - **`find(collection, query?, options?)`** - Alias for getMany → [Details](/docs/nosql-database-api#findcollection-query-options) - **`toArray(collection, query?, options?)`** - Get documents as array → [Details](/docs/nosql-database-api#toarraycollection-query-options) - **`updateOne(collection, query, document, operators?, options?)`** - Update document (patch) → [Details](/docs/nosql-database-api#updateonecollection-id--query-updateoperators-options) - **`updateMany(collection, query, document, operators?, options?)`** - Update multiple documents → [Details](/docs/nosql-database-api#updatemanycollection-query-document-options) - **`replaceOne(collection, query, document, options?)`** - Replace document completely → [Details](/docs/nosql-database-api#replaceonecollection-id--query-document-options) - **`replaceMany(collection, query, document, options?)`** - Replace multiple documents → [Details](/docs/nosql-database-api#replacemanycollection-query-document-options) - **`removeOne(collection, query)`** - Remove document → [Details](/docs/nosql-database-api#removeonecollection-id--query) - **`removeMany(collection, query, options?)`** - Remove multiple documents → [Details](/docs/nosql-database-api#removemanycollection-query-options) ### **Schema Management** - **`setSchema(collection, schema)`** - Add JSON-Schema validation → [Details](/docs/nosql-database-api#setschemacollection-schema) - **`getSchema(collection)`** - Get collection schema → [Details](/docs/nosql-database-api#getschemacollection) - **`removeSchema(collection)`** - Remove JSON-Schema validation → [Details](/docs/nosql-database-api#removeschemacollection) ### **Collection Management** - **`createCollection(collection, options?)`** - Create new collection → [Details](/docs/nosql-database-api#createcollectioncollection-options) - **`dropCollection(collection)`** - Delete collection → [Details](/docs/nosql-database-api#dropcollectioncollection) ## **Key-Value Store APIs** Fast in-memory key-value storage for caching, session management, and temporary data. Supports optional TTL (time-to-live) for automatic expiration. - **`set(key, value, options?)`** - Set key-string value pair with optional TTL → [Details](/docs/key-value-database-api#setkey-value-options) - **`setObj(key, value, options?)`** - Set key-object pair with optional TTL → [Details](/docs/key-value-database-api#setobjkey-value-options) - **`get(key, options?)`** - Get string value by key → [Details](/docs/key-value-database-api#getkey-options) - **`getObj(key, options?)`** - Get object value by key → [Details](/docs/key-value-database-api#getobjkey-options) - **`getAll(keyPattern, options?)`** - Get all key-value pairs matching pattern → [Details](/docs/key-value-database-api#getallkeypattern-options) - **`del(key, options?)`** - Delete key-value pair → [Details](/docs/key-value-database-api#delkey-options) - **`delAll(key, options?)`** - Delete all key-value pairs matching pattern → [Details](/docs/key-value-database-api#delallkey-options) - **`incr(key, value, options?)`** - Increment numeric value → [Details](/docs/key-value-database-api#incrkey-number-options) - **`decr(key, value, options?)`** - Decrement numeric value → [Details](/docs/key-value-database-api#decrkey-number-options) ## **Queue & Background Processing APIs** Offload time-consuming tasks to background worker queues for asynchronous processing. Workers can process jobs in parallel based on your plan limits. - **`worker(name, function, options?)`** - Register worker functions → [Details](/docs/appeventapi#appworkername-workerfunction-options) - **`enqueue(topic, document, options?)`** - Add job to queue → [Details](/docs/queueapi#enqueuetopic-payload-options) - **`enqueueFromQuery(collection, query, topic, options?)`** - Queue items from database query → [Details](/docs/queueapi#enqueuefromquerycollection-query-topic-options) ## **Job Scheduling APIs** Schedule recurring tasks with cron expressions or run one-time delayed jobs at specific times. Perfect for periodic maintenance, reports, and scheduled notifications. - **`job(cronExpression, worker)`** - Register scheduled cron jobs → [Details](/docs/jobhooks#cron-jobs) - **`schedule.runAt(when, data, worker)`** - Schedule one-time delayed job → [Details](/docs/jobhooks#schedule-delayed-worker-function-dynamically-with-runat) - **`schedule.run(data, worker)`** - Execute worker immediately → [Details](/docs/jobhooks#schedule-worker-function-to-run-immediate) ## **Workflow APIs** Build reliable multi-step workflows with persistent state management and automatic error recovery. Workflows can pause, resume, and handle timeouts across long-running business processes. ### **Workflow Management** - **`createWorkflow(name, description, steps, config?)`** - Create new workflow definition → [Details](/docs/workflow-api#createworkflowname-description-steps-config) - **`start(initialState)`** - Start new workflow instance → [Details](/docs/workflow-api#startinitialstate) - **`updateState(instanceId, state, options?)`** - Update workflow state → [Details](/docs/workflow-api#updatestateinstanceid-state-options) - **`setState(instanceId, stateData)`** - Set complete workflow state → [Details](/docs/workflow-api#setstateinstanceid-statedata) - **`continue(instanceId, reset?)`** - Continue paused workflow → [Details](/docs/workflow-api#continueinstanceid-reset) - **`getWorkflowStatus(id)`** - Get workflow status → [Details](/docs/workflow-api#getworkflowstatusid) - **`getInstances(filter)`** - List workflow instances → [Details](/docs/workflow-api#getinstancesfilter) - **`cancelWorkflow(id)`** - Cancel workflow instance → [Details](/docs/workflow-api#cancelworkflowid) ### **Error Recovery & Monitoring** - **`continueAllTimedOut()`** - Continue all timed out workflows → [Details](/docs/workflow-api#continuealltimedout) - **`findTimedOutSteps(filter?)`** - Find timed out workflow steps → [Details](/docs/workflow-api#findtimedoutstepsfilter) ### **Event Management** - **`on(event, listener)`** - Register event listener → [Details](/docs/workflow-api#onevent-listener) - **`once(event, listener)`** - Register one-time event listener → [Details](/docs/workflow-api#onceevent-listener) - **`off(event, listener)`** - Remove event listener → [Details](/docs/workflow-api#offevent-listener) - **`emit(event, data)`** - Emit custom event → [Details](/docs/workflow-api#emitevent-data) ### **Configuration** - Pass configuration options as the last argument to `createWorkflow()` → [Details](/docs/workflow-api#workflow-configuration) ## **File Management APIs** Store and retrieve files in blob storage with support for text, binary, and streaming operations. Ideal for user uploads, images, documents, and media files. - **`filestore.readFile(path)`** - Read file content as text → [Details](/docs/fileapi#readfilepath) - **`filestore.readFileAsBuffer(path)`** - Read file content as buffer → [Details](/docs/fileapi#readfileasbufferpath) - **`filestore.getReadStream(path)`** - Read file as binary stream → [Details](/docs/fileapi#getreadstreampath) - **`filestore.saveFile(path, filestream)`** - Write binary stream to file → [Details](/docs/fileapi#savefilepath-filestream) - **`filestore.deleteFile(path)`** - Delete file → [Details](/docs/fileapi#deletefilepath) - **`filestore.list(path)`** - List files in directory → [Details](/docs/fileapi#listpath-options) ## **Real-time Communication APIs** Push live updates to connected clients using server-sent events (SSE). Build real-time features like chat, notifications, and live data feeds. - **`realtime.createChannel(channel)`** - Create real-time channel → [Details](/docs/realtimeapi#realtimecreatechannelchannel) - **`realtime.publishEvent(channel, data, query?)`** - Publish event to channel → [Details](/docs/realtimeapi#realtimepublisheventchannel-data-query) - **`realtime.createListener(channel, data)`** - Create listener for channel → [Details](/docs/realtimeapi#realtimecreatelistenerchannel-data) - **`realtime.getListener(channel, listenerID)`** - Get specific listener → [Details](/docs/realtimeapi#realtimegetlistenerchannel-listenerid) - **`realtime.getListeners(channel)`** - Get all listeners for channel → [Details](/docs/realtimeapi#realtimegetlistenerschannel) - **`realtime.removeListener(channel, listenerID)`** - Remove listener → [Details](/docs/realtimeapi#realtimeremovelistenerchannel-listenerid) ## **Inter-Service Communication APIs** Make high-speed authenticated requests between Codehooks applications for microservices architecture. Optimized for internal app-to-app communication within the platform. - **`internalFetch(url, options?)`** - Make authenticated requests to other Codehooks APIs → [Details](/docs/appeventapi#appinternalfetchurl-options) ## **Template & Configuration APIs** Configure application settings and auto-generate complete CRUD REST APIs with a single function call. Use crudlify to instantly create database endpoints without writing route handlers. - **`set(key, val)`** - Set application configuration → [Details](/docs/appeventapi#appsetkey-val) - **`crudlify(schema?, options?)`** - Auto-generate CRUD REST API → [Details](/docs/appeventapi#appcrudlifyschema-options) ## **OpenAPI Documentation APIs** Auto-generate OpenAPI 3.0 specifications and interactive Swagger UI from your schemas and routes. Supports Zod, Yup, and JSON Schema for automatic schema conversion. - **`app.openapi(config, swaggerPath?)`** - Enable OpenAPI docs generation and Swagger UI → [Details](/docs/openapi-swagger-docs#appopenapiconfig-swaggerpath) - **`openapi(spec)`** - Middleware to add OpenAPI metadata to custom routes → [Details](/docs/openapi-swagger-docs#openapispec-middleware) ## **Application Lifecycle APIs** Initialize and bind your application to the serverless runtime. Must be called and exported as the default export from your main application file. - **`init(function?)`** - Initialize application and return manifest → [Details](/docs/appeventapi#appinit) - **`start(function?)`** - Alias for init() method → [Details](/docs/appeventapi#appinit) ## **Database REST API Endpoints** Auto-generated RESTful endpoints for database operations when using crudlify(). All standard CRUD operations are available with query support and pagination. When using `crudlify()`, these endpoints are automatically created: | Operation | Method | Endpoint | Description | | --------- | -------- | -------------------- | ---------------------------------------------------------------------------------- | | List all | `GET` | `/:collection` | Get all documents → [Details](/docs/database-rest-api#get-list-of-documents) | | Query | `GET` | `/:collection?query` | Get documents by query → [Details](/docs/database-rest-api#get-documents-by-query) | | Get by ID | `GET` | `/:collection/:id` | Get single document → [Details](/docs/database-rest-api#get-document-by-id) | | Create | `POST` | `/:collection` | Create new document → [Details](/docs/database-rest-api#create-a-new-document) | | Update | `PATCH` | `/:collection/:id` | Update document → [Details](/docs/database-rest-api#update-a-document-by-id) | | Replace | `PUT` | `/:collection/:id` | Replace document → [Details](/docs/database-rest-api#replace-a-document-by-id) | | Delete | `DELETE` | `/:collection/:id` | Delete document → [Details](/docs/database-rest-api#delete-a-document-by-id) | ## **Authentication & Security** Secure your API endpoints and authenticate users with multiple authentication methods. Choose the approach that fits your use case: API tokens for service-to-service calls, JWT for user authentication, or custom authentication logic. ### **Authentication Methods** - **API Tokens** - Add `x-apikey` header for app-to-app authentication. Create tokens with `coho add-token` or via web console → [Details](/docs/authentication#app-to-app-authentication-with-api-tokens) - **JWT/JWKS** - User authentication via Auth0, Clerk, or Stack-Auth. Configure JWKS endpoint with `coho jwks ` → [Details](/docs/authentication#authenticate-users-with-jwt-using-jwks) - **Admin Tokens** - For CLI automation and CI/CD. Create with `coho add-admintoken` and use with `--admintoken` flag → [Details](/docs/authentication#use-admin-tokens-to-authenticate-with-the-cli) - **IP Whitelisting** - Restrict API access by IP address using `coho whitelist ` or web console → [Details](/docs/authentication#use-ip-address-to-limit-api-access) - **Custom Auth** - Full control with `codehooks-auth` package. Includes OAuth (Google, GitHub), email verification, and OTP → [Details](/docs/authentication#custom-authentication-with-codehooks-auth) ### **Quick Examples** ```bash # Create read-only API token coho add-token --readonly # Configure JWT authentication coho jwks https://your-tenant.auth0.com/.well-known/jwks.json # Use API token in requests curl https://your-app.api.codehooks.io/api \ -H 'x-apikey: your-token-here' ``` ## **Query Syntax Examples** MongoDB-style query syntax for filtering database documents with comparison, logical, and array operators. Queries can be written in JavaScript objects or URL parameters. ### NoSQL Queries ```javascript // Simple equality { status: 'active'; } // Comparison operators { age: { $gt: 18; } } { price: { $lte: 100; } } // Logical operators { $and: [{ status: 'active' }, { age: { $gte: 18 } }]; } // Array operations { tags: { $in: ['javascript', 'nodejs']; } } // Regular expressions { name: { $regex: /john/i; } } ``` ### URL Query Parameters ```bash # Simple query ?status=active&limit=10 # Advanced query ?q={"age":{"$gt":18}}&sort=name&fields=name,email ``` ## **Common Usage Patterns** Real-world code examples showing typical patterns for building APIs, database operations, background processing, and workflows. Copy and adapt these patterns for your own applications. ### Basic API Setup ```javascript import { app } from 'codehooks-js'; // Auto-generate CRUD API app.crudlify(); // Custom routes app.get('/hello', (req, res) => { res.json({ message: 'Hello World' }); }); export default app.init(); ``` ### Database Operations ```javascript import { Datastore } from 'codehooks-js'; async function myFunc() { const conn = await Datastore.open(); // Insert document const doc = await conn.insertOne('users', { name: 'John' }); // Query documents const users = await conn.getMany('users', { active: true }).toArray(); // Key-value operations await conn.set('session:123', { userId: 'abc' }, { ttl: 3600000 }); } ... ``` ### Background Processing ```javascript // worker options, use 5 parallel workers const workerOpt = {workers: 5}; // Register worker app.worker('emailWorker', async (req, res) => { console.log('Processing email:', req.body.payload); res.end(); }, workerOpt); // Queue job async function myFunc() { const conn = await Datastore.open(); await conn.enqueue('emailWorker', { email: 'user@example.com' }); } ... ``` ### Workflow Processing ```javascript // Create a workflow with persistent state const workflow = app.createWorkflow( 'orderProcessing', 'Process customer orders', { start: async (state, goto) => { state.orderId = state.orderId || generateOrderId(); goto('validatePayment', state); }, validatePayment: async (state, goto) => { // Validate payment logic here if (state.paymentValid) { goto('fulfillOrder', state); } else { goto('handlePaymentError', state); } }, fulfillOrder: async (state, goto) => { state.status = 'fulfilled'; goto(null, state); // Complete workflow }, } ); // Start workflow instance const orderWorkflow = await workflow.start({ customerId: '123', amount: 99.99, }); ``` --- **Links:** - [Complete Documentation](/docs) - [Getting Started Guide](/docs/quickstart-cli) - [Webhook Templates](/docs/examples/examples-overview) - [CLI Reference](/docs/cli) - [Prompt for development using ChatGPT, Claude, Cursor etc](/docs/ai-agent-setup) - [Claude Code Plugin](https://github.com/RestDB/codehooks-claude-plugin) — Auto-detection, templates, and `/codehooks:backend` command for Claude Code - [MCP Server for Agent based development](https://github.com/RestDB/codehooks-mcp-server) --- ## The Application Object *(Note: This content contains MDX/JSX code)* import TOCInline from '@theme/TOCInline'; The Application object creates events that lets you hook your serverless functions accordingly. ```mermaid flowchart LR event("REST API events") & job("Background events") --> hook("App hook") --> func("Function") -.- db[(Database)] ``` Available event types are: - REST API route events - Background Job cron and scheduled events - Worker Queue events - Authentication events The event hooks is an Node.js Express style JavaScript library in a [public npm library](https://www.npmjs.com/package/codehooks-js) that you install as a npm package in your project directory. You can check for updates in the npm version. Install the CLI. ```bash npm i codehooks-js ``` Deploy your app with the CLI command. ```bash coho deploy ``` **API quick overview** ## Complete code example Import the `app` library in your application and hook into the various events. ```js import { app, openapi } from 'codehooks-js'; // generic serverless function function fooFunc(req, res) { res.end(); } // generic middleware function function fooMiddleware(req, res, next) { next(); } /* * Hook into events */ app.use(fooMiddleware); // global middleware app.use('/foo', fooMiddleware); // global middleware on routes app.get('/foo', fooFunc); // GET app.post('/foo', fooFunc); // POST app.put('/foo', fooFunc); // PUT app.patch('/foo', fooFunc); // PATCH app.delete('/foo', fooFunc); // DELETE app.all('/foo', fooFunc); // GET, POST, PUT, PATCH, DELETE app.auth('/*', fooMiddleware); // Called before outher routes without access tokens app.job('* * * * *', fooFunc); // subscribe to cron events app.worker('workername', fooFunc); // subscribe to worker events app.static(options); // serve deployed static files and content app.storage(options); // serve dynamic uploaded files and content app.openapi(config); // OpenAPI docs and Swagger UI app.crudlify(options); // Database CRUD REST API // Bind events and functions to the serverless runtime export default app.init(); ``` ## app.init() Mandatory call to bind your functions to events from the serverless runtime. **Parameters** none ## app.use(workerFunction) Adds global middleware to all route API events (get, put, post, patch, delete). **Parameters** - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object), [next](rest-api-app-routes#middleware-functions)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; import cookieParser from 'cookie-parser'; // external npm lib middleware app.use(cookieParser()); app.get('/foo', function (req, res) { // Cookies that have not been signed console.log('Cookies: ', req.cookies); // Cookies that have been signed console.log('Signed Cookies: ', req.signedCookies); // Custom middleware result console.log('Foo: ', req.foo); res.send('Done'); }); export default app.init(); ``` ## app.use(route, workerFunction) Adds global middleware to a specific route API events (get, put, post, patch, delete). **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object), [next](rest-api-app-routes#middleware-functions)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; // custom app.use('/foo', (req, res, next) => { console.log('Called before any other route handlers'); req.foo = 'Foo was here!'; next(); // must be called to continue }); app.get('/foo', function (req, res) { // Custom middleware result console.log('Foo: ', req.foo); res.send('Done'); }); export default app.init(); ``` ## app.get(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP GET method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.get('/foo', function (req, res) { res.send('Done'); }); export default app.init(); ``` ## app.post(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP POST method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.post('/foo', function (req, res) { console.log('Data from client is', req.body); res.send('Done'); }); export default app.init(); ``` ## app.put(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP PUT method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.put('/foo', function (req, res) { console.log('Data from client is', req.body); res.send('Done'); }); export default app.init(); ``` ## app.patch(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP PATCH method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.patch('/foo', function (req, res) { console.log('Data from client is', req.body); res.send('Done'); }); export default app.init(); ``` ## app.delete(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP DELETE method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.delete('/foo/:ID', function (req, res) { const { ID } = req.params; console.log('Delete this', ID); res.send('Done'); }); export default app.init(); ``` ## app.all(route, ...middleware?, workerFunction) If no other routes matched before this, then execute the workerFunction function on all HTTP methods and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.get('/foo', function (req, res) { res.send('Done GET'); }); app.all('/*', function (req, res) { res.send( `None of the above routes matched but this ${req.method} ${req.originalUrl}` ); }); export default app.init(); ``` ## app.auth(route, workerFunction) Execute workerFunction function when no authentication token (x-apikey or JWT) is present. Works for all HTTP methods and route matches. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** [See Auth hooks docs here.](authhooks) ## app.worker(name, workerFunction, options) Register a named workerFunction function that can be used to process queued events or scheduled events. **Parameters** - **name**: unique worker name - **workerFunction**: function(queueData, responseFunction) - **options**: (optional) Object with configuration settings - **workers**: number - Number of parallel workers to process queue items concurrently. Limited by your plan quota. - **timeout**: number (milliseconds) - Paid plans can set the worker function execution time up to 10 minutes. **Returns** void **Code example for worker queues** [See Queue docs here.](queuehooks) **Code example for scheduled workers** [See scheduled jobs docs here.](jobhooks#schedule-delayed-worker-function-dynamically-with-runat) ## app.job(cronExpression, workerFunction) Execute workerFunction function when cron job are scheduled. **Parameters** - **cronExpression**: a valid cron expression string - **workerFunction**: function(jobData, responseFunction) [See Job hook docs here.](jobhooks) ## app.static(options, callback) Serve static files from deployed source directory. For example: `app.static({ route: '/', directory: '/dist', default: 'index.html', notFound: '/index.html' })` **Options** - **route**: URL sub route for clients - **directory**: path to file upload directory - **default**: (optional) default file to serve when requesting a directory (e.g., 'index.html') - **notFound**: (optional) file to serve for 404 errors (essential for client-side routing in SPAs) **callback(req, res, next)** Provide a middleware function to control the request. For example, add client side cache headers instructions for static assets. ```js // serve static assets/images, and cache for 1 hour app.static({ route: '/assets', directory: '/assets' }, (req, res, next) => { console.log( 'If you see this, the client cache is invalidated or called for the first time' ); const ONE_HOUR = 1000 * 60 * 60; res.set('Cache-Control', `public, max-age=${ONE_HOUR}, s-maxage=${ONE_HOUR}`); res.setHeader('Expires', new Date(Date.now() + ONE_HOUR).toUTCString()); res.removeHeader('Pragma'); next(); }); ``` ## app.storage(options, callback) Serve files from the blob storage file system. You can upload files to the blob storage with the CLI. Also see the [Filesystem API docs](/docs/fileapi). ```bash $ coho file-upload --projectname 'myproject-fafc' --space 'dev' --src '/mylocaldir/' --target '/mydocuments' ``` For example: `app.storage({ route: '/docs', directory: '/mydocuments' })` **Options** - **directory**: path to file upload directory - **route**: URL sub route for clients **callback(req, res, next)** Provide a middleware function to control the request. ## app.crudlify(schema, options) Creates a complete Database REST API ([detailed docs here](/docs/database-rest-api)). Example: REST API for two collection `customer` and `product`, any schema is allowed: ```js app.crudlify({ customer: {}, product: {} }, { prefix: '/crudapi' }); // Example valid URL's // https://myproject-ff00.api.codehooks.io/dev/customer?name=Bill // https://myproject-ff00.api.codehooks.io/dev/product?price>42 ``` **Schema** -- **collection**: schema, for specific collections, e.g. `{"collection": someSchema}` **Options** - **prefix**: serve REST routes under another route. `{"prefix": "/api"}` **Returns** Promise to `before` `after` functions. Example: ```js crudlify(app, {product, customer}).then((hooks) => { hooks.beforePOST('customer', async (data) => { // do something here data.foo = 'Was here before saving!' }) hooks.afterPOST('product', async (data) => { console.log("Data as saved to the database", data) // do something here }) ``` ## app.openapi(config, swaggerPath?) Enable automatic OpenAPI 3.0 documentation generation and serve an interactive Swagger UI. When combined with `crudlify`, schemas are automatically converted to OpenAPI specifications. See the [complete OpenAPI documentation](/docs/openapi-swagger-docs) for full details. **Parameters** - **config** (object): OpenAPI configuration object - **info** (object, required): API metadata with `title`, `version`, and optional `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` - **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** ```js import { app } from 'codehooks-js'; import { z } from 'zod'; const todoSchema = z.object({ title: z.string().min(1).max(200), completed: z.boolean().default(false) }); // Enable OpenAPI documentation app.openapi({ info: { title: 'Todo API', version: '1.0.0', description: 'Task management API' }, tags: [ { name: 'Todos', description: 'Todo operations' } ], filter: (op) => op.method !== 'delete' // Hide DELETE endpoints from docs }, '/api-docs'); // Swagger UI at /api-docs app.crudlify({ todos: todoSchema }); export default app.init(); ``` After deployment, visit: - `/docs` (or custom path) — Interactive Swagger UI with authentication support - `/openapi.json` — Raw OpenAPI 3.0 specification ## openapi(spec) middleware Add OpenAPI metadata to custom routes. Import the `openapi` middleware helper and use it to document individual endpoints. **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 schemas directly) - **parameters** (array, optional): Query/path parameters - **responses** (object): Response definitions - **security** (array, optional): Security requirements (use `[]` for public endpoints) **Returns** Middleware function **Code example** ```js import { app, openapi } from 'codehooks-js'; import { z } from 'zod'; const UserSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email() }); app.openapi({ info: { title: 'User API', version: '1.0.0' } }); // Document a custom POST endpoint with Zod schema app.post('/users', openapi({ summary: 'Create a new 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) => { // Handler logic res.status(201).json({ id: '123', ...req.body }); } ); // Document a GET endpoint with query parameters app.get('/users', openapi({ summary: 'List users', tags: ['Users'], parameters: [ { name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } }, { name: 'role', in: 'query', schema: { type: 'string', enum: ['admin', 'user'] } } ], responses: { 200: { description: 'List of users' } } }), async (req, res) => { res.json([]); } ); export default app.init(); ``` ## app.createWorkflow(name, desc, workflowJSON, config?) Create reliable stateful workflows using plain JavaScript. **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 including `collectionName`, `queuePrefix`, `timeout`, `maxStepCount`, `workers`, and step-specific settings **Returns**: Workflow instance for managing the workflow **Code example**: ```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(); ``` ## app.set(key, val) Set configuration settings for the application. This allows you to configure various aspects of your application behavior. **Parameters** - **key**: String - the configuration key - **val**: any - the configuration value **Returns** void **Code example** ```js import { app } from 'codehooks-js'; import handlebars from 'handlebars'; // Set views directory app.set('views', '/views'); // Set view engine app.set('view engine', { hbs: handlebars }); // Set other configuration options app.set('foo', 'bar'); export default app.init(); ``` ## app.internalFetch(url, options) Fetch data from another Codehooks API. This method is designed for high speed app-to-app communication within the Codehooks platform, allowing you to make authenticated API calls to other Codehooks applications. **Authentication**: Must include `x-apikey` header for authentication if the target API is not public. **Parameters** - **url**: String - URL to fetch from (e.g., `http://myapi-ffee.codehooks.io/dev/api/myroute`) - **options**: Object (optional) - Fetch options including method, headers, body, etc. **Returns** `Promise` - Promise that resolves with the fetched data **Code example** ```js import { app } from 'codehooks-js'; app.get('/fetch-data', async (req, res) => { try { // Fetch data from another Codehooks API const data = await app.internalFetch( 'http://myapi-ffee.codehooks.io/dev/api/users', { method: 'GET', headers: { 'Content-Type': 'application/json', 'x-apikey': 'your-target-api-key-here' } } ); res.json({ success: true, data }); } catch (error) { res.status(500).json({ error: 'Failed to fetch data' }); } }); app.post('/sync-user', async (req, res) => { try { // POST data to another Codehooks API const result = await app.internalFetch( 'http://userservice-1234.codehooks.io/dev/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-apikey': 'your-target-api-key-here' }, body: JSON.stringify(req.body) } ); res.json({ success: true, result }); } catch (error) { res.status(500).json({ error: 'Failed to sync user' }); } }); export default app.init(); ``` Read more in the [detailed workflow API docs](/docs/workflow-api) --- ## Authentication *(Note: This content contains MDX/JSX code)* Application APIs and data can be accessed with a secure token. Tokens can be JWT tokens created from a provider such as Auth0.com or API tokens created by yourself (or any project user with ADMIN rights). In addition, some CLI commands can use a special _admin token_ (se below). ## App-to-app authentication with API tokens ### Create an API token with the CLI API tokens can be created i two ways. 1. Using the CLI command `coho add-token` 2. Using the admin application at https://account.codehooks.io The example below shows how to use the CLI to create a new `READ` access API token. ```bash coho add-token --readonly Created new token 5f1ce782-5798-4295-bdd0-51568390a59c with access READ ``` The API token can now be used to access your application API. Add the `x-apikey` as a HTTP header in the request. ```bash curl https://myproject-ff00.api.codehooks.io/dev/myapi \ -H 'x-apikey: 5f1ce782-5798-4295-bdd0-51568390a59c' ``` ### Using the API key as a URL parameter Some integration platforms (e.g. Zapier, Make, Power Automate) only support URL-based API calls and don't allow setting custom HTTP headers. For these cases, you can pass the API key as a query parameter using `apikey`. ```bash curl 'https://myproject-ff00.api.codehooks.io/dev/myapi?apikey=5f1ce782-5798-4295-bdd0-51568390a59c' ``` :::tip Using with crudlify When using the `apikey` URL parameter together with [crudlify](/docs/appeventapi#appcrudlifyschema-options), you should use the `q` query parameter for your database queries. This prevents the `apikey` parameter from being interpreted as a database query filter. ```bash # Correct: use q parameter for the query, apikey for authentication curl 'https://myproject-ff00.api.codehooks.io/dev/customers?q={"city":"New York"}&apikey=5f1ce782-5798-4295-bdd0-51568390a59c' ``` ::: :::warning note The secure token should not be used from web pages. We recommend that you only use it server side. ::: ### Using the web application to add an API token In your account at https://account.codehooks.io, find the project and the space settings to add a new token. ![add API token using web](/img/api-token-using-ui.png) ## Authenticate users with JWT (using JWKS) ### Set up JWKS with the CLI If you have set up JWT authentication using [Auth0.com (Okta)](https://auth0.com), [Clerk](https://clerk.com/), [Stack-Auth](https://stack-auth.com) or similar services, you can easily set up a JWKS endpoint in the space settings. All API calls will then be verified using this mechanism. ```bash # add JWKS endpoint $ coho jwks https://.auth0.com/.well-known/jwks.json Set JWKS url to space 'dev' ``` | Auth provider | JWKS URL | | -------------- | ----------------------------------------------------------------------------------------------- | | Auth0.com | `https://.auth0.com/.well-known/jwks.json` | | Clerk.com | `https:///.well-known/jwks.json` | | Stack-auth.com | `https://api.stack-auth.com/api/v1/projects//.well-known/jwks.json` | ### Using the web application to set a JWKS endpoint In your account at https://account.codehooks.io, find the project and the space settings to set the JWKS endpoint URL. ![set JWKS endpoint using web](/img/add-jwks-endpoint.png) :::tip You can also roll your own authentication using code. Read more about using [authhooks](/docs/authhooks) for this. ::: :::info Verifying JWT tokens - RS256 vs HS256 By default, to verify login tokens (JWT), Auth0 example applications now use the asymmetric RS256 algorithm instead of the HS256 symmetric one. What is the difference? HS256 requires you to use a secret key to verify the token but RS256 let you use a public key which can be fetched from an URL on the web (JWKS URL). You can read more about it in [this blogpost by Auth0.](https://auth0.com/blog/navigating-rs256-and-jwks/) ::: ## Use admin tokens to authenticate with the CLI For use with the CLI, you can create admin tokens instead of having to log in to your account as a user with ADMIN rights. Admin tokens belong to the personal account or a team and applies to the following codehooks CLI commands: deploy, undeploy, createindex, removeindex, backup, restore, import, export, query. Admin tokens are ideal for use in shell scripts and for CI purposes. ```bash # add admin token $ coho add-admintoken ? Select personal account or team to add admin token to Personal New admin token added: 0882e570d8fc7fe97ae958ae8df3d7ba-7f5c5eb7177c Please copy this now and keep it secret. It will never be shown in full again. ``` ```bash # use the admin token $ coho deploy --admintoken 0882e570d8fc7fe97ae958ae8df3d7ba-7f5c5eb7177c Deploying to Project: students-3bfe Space: dev Deployed Codehook successfully! 🙌 ``` ## Use IP address to limit API access From your serverless functions you can inspect the client IP address from the request header fields. The example headers listen below shows that the client IP address is `11.22.33.44`. ```js headers: { host: 'myapp-f0gt.api.codehooks.io', 'x-request-id': 'ba5582e3bafd12602b3f70a5af5ab20f', 'x-real-ip': '11.22.33.44', 'x-forwarded-for': '11.22.33.44', 'x-forwarded-host': 'myapp-f0gt.api.codehooks.io', 'x-forwarded-port': '443', 'x-forwarded-proto': 'https', 'x-forwarded-scheme': 'https', 'x-scheme': 'https', accept: '*/*', 'user-agent': 'Thunder Client (https://www.thunderclient.com)', 'x-api-key': '07ee0bba-xxxx-yyy-zzzz-dfd0b991fa0a' } ``` Use the [CLI command `coho whitelist`](/docs/cli.md#whitelist) or the admin web application to give exclusive IP addresses API access, e.g. `coho whitelist 11.22.33.44` You can use a [authentication hook](/docs/authhooks) to intercept API requests. ## Custom Authentication with codehooks-auth For complete control over authentication, you can use the open-source [`codehooks-auth`](https://github.com/RestDB/codehooks-auth) package. This solution provides: - JWT-based authentication with access and refresh tokens - Built-in user management with OAuth support (Google and GitHub) - Email verification and OTP (One-Time Password) authentication - Customizable templates and email notifications ![codehooks-auth](https://raw.githubusercontent.com/RestDB/codehooks-auth/refs/heads/main/examples/images/auth-lock-screen.png) ### Setup Instructions 1. Install the required packages: ```bash npm install codehooks-auth codehooks-js ``` 2. Set up JWT secrets: ```bash coho set-env JWT_ACCESS_TOKEN_SECRET 'your_access_token_secret' --encrypted coho set-env JWT_REFRESH_TOKEN_SECRET 'your_refresh_token_secret' --encrypted ``` 3. Configure authentication in your backend code: ```js import { app } from 'codehooks-js'; import { initAuth } from 'codehooks-auth'; const settings = { JWT_ACCESS_TOKEN_SECRET: process.env.JWT_ACCESS_TOKEN_SECRET, JWT_REFRESH_TOKEN_SECRET: process.env.JWT_REFRESH_TOKEN_SECRET, redirectSuccessUrl: '/dashboard.html', // where to redirect after successful login baseAPIRoutes: '/api', // protected routes // Optional: Configure OAuth providers google: { CLIENT_ID: process.env.CLIENT_ID, CLIENT_SECRET: process.env.CLIENT_SECRET, REDIRECT_URI: 'https://{YOUR_APP_URL}.codehooks.io/auth/oauthcallback/google', }, github: { CLIENT_ID: process.env.GITHUB_CLIENT_ID, CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, REDIRECT_URI: 'https://{YOUR_APP_URL}.codehooks.io/auth/oauthcallback/github', }, }; // Initialize auth with your settings initAuth(app, settings); export default app.init(); ``` 4. Access protected routes in your client code: ```js fetch('/api/protected-route', { credentials: 'include', headers: { 'Content-Type': 'application/json', }, }); ``` The authentication system will automatically handle: - User registration and login flows - JWT token management (access and refresh tokens) - OAuth authentication with supported providers - Email verification and password reset functionality - Protected API route authorization See the [codehooks-auth documentation](https://github.com/RestDB/codehooks-auth) for complete setup instructions, template customization options, and advanced configuration. --- ## Auth hooks *(Note: This content contains MDX/JSX code)* Auth hooks lets you override the default security behaviour on specific routes. If an auth hook route matches a route hook, and no authentication token is present in the headers, the auth function is called as a last resort to allow or dismiss the request. The auth hook function logic calls `next()` to allow, or `response.end()` to block a client request. Use the auth hook to create public routes and custom rules and overrides to fit any use case. ## Example auth hook ```javascript {9} title="index.js" import app from 'codehooks-js'; // Standard JS lib for express style code // REST API routes app.get('/specialroute/frags', (req, res) => { res.end('You have the correct secret header value'); }); // Auth hook app.auth('/specialroute/*', (req, res, next) => { // call some auth function here, e.g. myLookup myLookup(req.headers['X-challenge'], (err, data) => { if (err) { res.status(401); // Unauthorized res.end(); } else { // allow API call next(); } }); }); function myLookup(challenge, callback) { if (challenge === 'SOMESECRET') { callback(null); } else { callback('Sorry'); } } export default app.init(); // Bind functions to the serverless runtime ``` [See docs for route matching](rest-api-app-routes#route-matching) [See docs for middleware functions and the next() function](rest-api-app-routes#middleware-functions) --- ## Codehooks CLI tool *(Note: This content contains MDX/JSX code)* # Codehooks CLI Documentation **Version**: `1.3.2` > Command-line interface for [Codehooks.io](https://codehooks.io) - A serverless backend platform ## Quick Start ### Installation ```bash npm install -g codehooks ``` ### Basic Usage ```bash # Login to your account codehooks login # Create a new project codehooks create my-project # Deploy your code codehooks deploy ``` --- ## Commands by Category ### Getting Started | Command | Description | |---------|-------------| | [docs](#docs) | Open the API cheatsheet - routes, database, key-value, queues, jobs, workflows, and more | | [prompt](#prompt) | Display the Codehooks.io development prompt for AI assistants | | [login](#login) | Authenticate CLI - sign up and/or sign in | | [logout](#logout) | Log out of the CLI | | [account](#account) | Show info about account, projects and invitations | ### Project Management | Command | Description | |---------|-------------| | [create](#create) | Create a new codehooks project | | [init](#init) | Link an existing Codehooks project to the current folder | | [info](#info) | Show info about project and spaces | | [doctor](#doctor) | Show comprehensive project status (agent-friendly) | | [describe](#describe) | Describe the deployed app (collections, schemas, queues, files) | | [invite](#invite) | Invite user to project | | [join](#join) | Join project | | [leave](#leave) | Leave project | | [remove-project](#remove-project) | Remove the project | ### Space Management | Command | Description | |---------|-------------| | [add](#add) | Add new space to project | | [use](#use) | Set active space | | [remove-space](#remove-space) | Remove space and data | ### Deployment | Command | Description | |---------|-------------| | [verify](#verify) (compile, ver, comp) | Compile code (defaults to current dir) | | [deploy](#deploy) (de, dep) | Deploys current codehook folder | | [undeploy](#undeploy) (unde, undep) | Undeploy current codehook folder | | [install](#install) (inst, i) | Install a template application | ### Database & Collections | Command | Description | |---------|-------------| | [collection](#collection) (coll, col, ls) | Show collections for space | | [createcollection](#createcollection) (createcoll, add-collection) | Create a new collection | | [dropcollection](#dropcollection) (dropcoll, rmcoll, deletecoll) | Delete all data in collection and remove collection name | | [add-schema](#add-schema) (schema, create-schema) | Add a JSON schema to a collection | | [remove-schema](#remove-schema) (delete-schema, del-schema) | Remove JSON schema for a collection | | [cap-collection](#cap-collection) (cap, cap-coll, capcoll) | Cap a collection | | [uncap-collection](#uncap-collection) (uncap) | Remove cap on a collection | ### Data Operations | Command | Description | |---------|-------------| | [query](#query) (q) | Run query on collection in current space | | [count](#count) (co) | Count objects in a collection in current space | | [import](#import) (imp) | Import JSON or CSV data from file | | [export](#export) | Export JSON or CSV data | | [createindex](#createindex) (index, idx, create-index) | Add field(s) to a query index | | [dropindex](#dropindex) (removeindex, remove-index, rmindex, delete-index) | Remove field(s) from a query index | ### Key-Value Store | Command | Description | |---------|-------------| | [get](#get) | Retrieve key-value pair(s) from a space | | [set](#set) | Set key-value pair in a space | | [del](#del) | Delete key-value pair in a space | ### File Management | Command | Description | |---------|-------------| | [file-upload](#file-upload) (up, upload) | Upload files to server | | [file-delete](#file-delete) (delete) | Delete a file from server | | [file-list](#file-list) (files) | List files from server | ### Monitoring & Logs | Command | Description | |---------|-------------| | [log](#log) (logs) | Show system logs for a space. | | [stats](#stats) | Show usage metrics for project spaces | | [workflow-status](#workflow-status) (wfstat) | Visualize workflow data from workflowdata collection | | [queue-status](#queue-status) (qs, queues) | Visualize queue status with item counts and workers | ### Security & Access | Command | Description | |---------|-------------| | [add-token](#add-token) | Add token to space | | [remove-token](#remove-token) | Remove token from space | | [set-env](#set-env) | Set environment variable for space | | [remove-env](#remove-env) | Remove environment variable from space | | [jwks](#jwks) | Set/replace JWKS endpoint for OAuth2 authentication. Set to "" (empty string) to remove. | | [whitelist](#whitelist) | Add host to whitelist (use to restrict space access) | | [whitelist-remove](#whitelist-remove) | Remove host from whitelist | ### Admin Tools | Command | Description | |---------|-------------| | [admin](#admin) | Open the codehooks.io admin account UI at account.codehooks.io | | [admintokens](#admintokens) | Show active admintokens for account or team | | [add-admintoken](#add-admintoken) | Add admin token to account or team (for use with CI) | | [remove-admintoken](#remove-admintoken) | Remove admin token from account or team | ### Shell Integration | Command | Description | |---------|-------------| | [completion](#completion) | Generate command completion script. Just add this to your .bashrc, .bash_profile, .zshrc (or similar) on *nix machines | --- ## Global Options These options are available for all commands: | Option | Description | |--------|-------------| | -d, --debug | Show debug (verbose) information | | -h, --help | Show help | | -v, --version | Show version number | --- ## Command Reference ### Getting Started ### docs Open the API cheatsheet - routes, database, key-value, queues, jobs, workflows, and more --- ### prompt Display the Codehooks.io development prompt for AI assistants --- ### login Authenticate CLI - sign up and/or sign in --- ### logout Log out of the CLI **Options:** | Option | Description | |--------|-------------| | `--dir` | [default: "."] | | `--json` | Output as JSON [boolean] | --- ### account Show info about account, projects and invitations **Options:** | Option | Description | |--------|-------------| | `--json` | Output as JSON [boolean] | --- ### Project Management ### create Create a new codehooks project **Options:** | Option | Description | |--------|-------------| | `-n, --projectname` | Project name | | `--description` | A few words about this project | | `-t, --teamid` | Add project to team with this id | | `--template` | Template to use (e.g., "slack-memory-bot") or "default" for CRUD template [string] | | `--here` | Create project in current folder instead of new subfolder [boolean] [default: false] | | `-e, --empty` | Only create config.json, skip generating code files [boolean] [default: false] | | `--no-deploy` | Skip automatic deployment of default template [boolean] [default: false] | | `-g, --ga-deploy` | Add Github Action for automatic deploy [boolean] | | `-y, --yes` | Skip all prompts and use defaults (non-interactive mode) [boolean] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks create myapp Create a new project interactively codehooks create myapp --yes Create with defaults, no prompts codehooks create myapp --here Create in current folder codehooks create myapp --empty Create config only, no code files codehooks create myapp --here --empty Create config in current folder codehooks create myapp --template slack-memory-bot Create with a specific template codehooks create myapp --teamid abc123 --yes Create in team, non-interactive codehooks create myapp --no-deploy Create without deploying ``` --- ### init Link an existing Codehooks project to the current folder **Options:** | Option | Description | |--------|-------------| | `-n, --projectname` | Project name | | `-s, --space` | Space (environment) name [default: "dev"] | | `-e, --empty` | Only create the project config file [boolean] | | `--download` | Download source code from project [boolean] [default: true] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks init codehooks init --empty codehooks init --download ``` --- ### info Show info about project and spaces **Options:** | Option | Description | |--------|-------------| | `--projectname` | Active project [required] | | `--json` | Output info as JSON (not table) [boolean] | | `--space` | Only show info for this space [string] | | `-e, --examples` | Include cURL examples [boolean] | --- ### doctor Show comprehensive project status (agent-friendly) **Options:** | Option | Description | |--------|-------------| | `-p, --projectname` | Select which project name to use [required] | | `-s, --space` | Select which space to inspect | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON (default: true) [boolean] [default: true] | | `--collections` | Include collections info [boolean] [default: true] | | `--stats` | Include usage stats [boolean] [default: true] | | `--deploys` | Include deployment history [boolean] [default: true] | | `--logs` | Include recent error logs [boolean] [default: true] | **Examples:** ```bash codehooks doctor Full project status as JSON codehooks doctor --no-json Human-readable summary codehooks doctor --no-logs --no-stats Skip logs and stats codehooks doctor --admintoken CI/CD usage ``` --- ### describe Describe the deployed app (collections, schemas, queues, files) **Options:** | Option | Description | |--------|-------------| | `-p, --projectname` | Select which project name to use [required] | | `-s, --space` | Select which space to inspect | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON (default: true) [boolean] [default: true] | | `--files` | Include uploaded files list [boolean] [default: true] | | `--schemas` | Include full schema objects (use --no-schemas for summary only) [boolean] [default: true] | **Examples:** ```bash codehooks describe Full app description as JSON codehooks describe --no-json Human-readable summary codehooks describe --no-files Skip file listing codehooks describe --no-schemas Compact collection info without schemas ``` --- ### invite Invite user to project **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `-t, --email` | [string] [required] | | `--role` | [string] [default: "ADMIN"] | | `--remove` | remove invitation [boolean] | | `--json` | Output as JSON [boolean] | --- ### join Join project **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--json` | Output as JSON [boolean] | --- ### leave Leave project **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--json` | Output as JSON [boolean] | --- ### remove-project Remove the project **Options:** | Option | Description | |--------|-------------| | `-p, --projectname` | Project name [string] [required] | --- ### Space Management ### add Add new space to project **Options:** | Option | Description | |--------|-------------| | `--projectname` | [required] | | `-n, --space` | space name [required] | | `--restricted` | only team admins or owner can use if restricted [boolean] | | `--json` | Output as JSON [boolean] | --- ### use Set active space **Options:** | Option | Description | |--------|-------------| | `--projectname` | [required] | | `-n, --name` | | | `--json` | Output as JSON [boolean] | --- ### remove-space Remove space and data **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name | | `--space` | A space in your project [required] | | `--json` | Output as JSON [boolean] | --- ### Deployment ### verify **Aliases:** compile, ver, comp Compile code (defaults to current dir) **Options:** | Option | Description | |--------|-------------| | `--dir` | [default: "."] | | `--space` | Select which space to access | | `-p, --projectname` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `-e, --main` | Application main file, default is codehooks [default: "index"] | | `--tofile` | Output file path, writes to specified file when provided | --- ### deploy **Aliases:** de, dep Deploys current codehook folder **Options:** | Option | Description | |--------|-------------| | `--dir` | [default: "."] | | `-s, --space` | Select which space to access | | `-p, --projectname` | Select which project name to use | | `--history` | Show deployment history | | `--rollback` | Undo last deployment, set previous as active | | `--json` | Output JSON format | | `--admintoken` | Use admin token authentication (use for CI) | | `--template` | Deploy a pre defined code template | | `--vibemode` | Fast deploy, skip source code upload [boolean] [default: false] | | `--upload` | Upload source code assets to codehooks.io this projects environment [boolean] [default: true] | | `--minify` | Minify source code assets before upload [boolean] [default: true] | | `--compress` | Upload source as tar.gz archive instead of file-by-file [boolean] [default: false] | | `-e, --main` | Application main file, default is codehooks [default: "index"] | --- ### undeploy **Aliases:** unde, undep Undeploy current codehook folder **Options:** | Option | Description | |--------|-------------| | `--dir` | [default: "."] | | `-s, --space` | Select which space to access | | `-p, --projectname` | Select which project name to use | | `--json` | Output JSON format | | `--admintoken` | Use admin token authentication (use for CI) | --- ### install **Aliases:** inst, i Install a template application **Options:** | Option | Description | |--------|-------------| | `-t, --template` | Template directory from Github repo with templates [string] | | `--space` | Select which space (environment) to access | | `-p, --projectname` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks install Interactive template selection codehooks install static-website-tailwindcss Install specific template codehooks install --template slack-memory-bot Install with --template flag ``` --- ### Database & Collections ### collection **Aliases:** coll, col, ls Show collections for space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `--json` | JSON output format | | `--sys` | Show system collections [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | --- ### createcollection **Aliases:** createcoll, add-collection Create a new collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--cap` | Cap collection to max documents | | `--capdelay` | Delay capping to millis | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks createcollection pets codehooks createcollection logs --cap 5000 ``` --- ### dropcollection **Aliases:** dropcoll, rmcoll, deletecoll Delete all data in collection and remove collection name **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks dropcollection pets ``` --- ### add-schema **Aliases:** schema, create-schema Add a JSON schema to a collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--schema` | Path to file with JSON schema for collection [required] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks add-schema --collection 'person' --schema './personSchema.json' ``` --- ### remove-schema **Aliases:** delete-schema, del-schema Remove JSON schema for a collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks remove-schema --collection 'person' ``` --- ### cap-collection **Aliases:** cap, cap-coll, capcoll Cap a collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--cap` | Cap collection to max documents [default: 1000] | | `--capdelay` | Delay capping to max millis [default: 1000] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks capcollection --collection 'temperature' --cap 10000 ``` --- ### uncap-collection **Aliases:** uncap Remove cap on a collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks uncap-collection --collection 'temperature' ``` --- ### Data Operations ### query **Aliases:** q Run query on collection in current space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `-q, --query` | Limit selection with a query expression or JSON string '\{...\}' [default: ""] | | `-n, --count` | Count query results | | `-c, --collection` | Collection name [required] | | `--delete` | Delete all items from query result | | `--update` | Patch all items from query result with JSON string '\{...\}' [string] | | `--replace` | Replace all items from query result with JSON string '\{...\}' [string] | | `--useindex` | Use an indexed field to scan data in query [string] | | `--start` | Start value for index scan [string] | | `--end` | End value for index scan [string] | | `--limit` | Limit query result [number] | | `--fields` | Comma separated list of fields to include [string] | | `--sort` | Comma separated list of fields to sort by [string] | | `--offset` | Skip items before returning data in query result [number] | | `--enqueue` | Add query result to queue topic [string] | | `--pretty` | Output data with formatting and colors | | `--reverse` | Scan index in reverse order | | `--table` | Output data as formatted table (not JSON) [boolean] | | `--csv` | Output data in CSV format [boolean] | | `--jsonl` | Output as JSON Lines (one JSON object per line) [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | **Examples:** ```bash codehooks query pets name=Polly codehooks query --collection pets --query 'name=Polly&type=Parrot' codehooks query --collection pets --query 'name=/^po/' codehooks query --collection pets --query 'name=/^po/' --sort 'age,name' codehooks query pets 'name=Polly' --useindex name --fields 'name,type' codehooks q pets 'name=Polly&type=Parrot' --update '{"name": "Zilla"}' codehooks q pets 'type=Fish' --delete codehooks q pets 'type=Snake' --enqueue 'mytopic' codehooks q pets --jsonl codehooks query pets --q '{"type":"Dog"}' --project petsdb-5544 --space dev ``` --- ### count **Aliases:** co Count objects in a collection in current space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `-c, --collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | --- ### import **Aliases:** imp Import JSON or CSV data from file **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Project name [string] [required] | | `-s, --space` | A space in your project [string] | | `-c, --collection` | A collection in a Space [string] [required] | | `-f, --filepath` | File to import [string] [required] | | `--separator` | Field separator character, default is ',', also normal with '\t' or ';' [string] [default: ","] | | `--dryrun` | Test only, will not import any data [boolean] [default: false] | | `--rowcount` | Add a row count field to each imported record [boolean] [default: false] | | `--admintoken` | Use admin token authentication (use for CI) | | `--local` | Import data to local development server on port parameter [number] | | `--json` | Output as JSON [boolean] | | `--encoding` | String encoding to use, latin1, utf8, ascii, hex, ucs2 [string] [default: "utf8"] | **Examples:** ```bash codehooks import ./myfile.csv mycollection codehooks import --filepath ./myfile.csv --collection mycollection codehooks import -f ./myfile.json -c mycollection codehooks import -f ./myfile.csv -c mycollection --separator ';' --encoding 'latin1' ``` --- ### export Export JSON or CSV data **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Project name [string] [required] | | `-s, --space` | a space name in your project [string] | | `--collection` | A collection in the space [string] [required] | | `-f, --filepath` | Filename to save export data [string] | | `--csv` | Export to CSV [boolean] | | `--jsonl` | Export to JSONL (JSON Lines) [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | --- ### createindex **Aliases:** index, idx, create-index Add field(s) to a query index **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `-c, --collection` | Collection with indexes [required] | | `-i, --index` | Field to index [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks createindex --collection pets --index name codehooks idx pets name codehooks idx pets -i name -i type -i 'price-in-dollar' ``` --- ### dropindex **Aliases:** removeindex, remove-index, rmindex, delete-index Remove field(s) from a query index **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `-c, --collection` | Collection with indexes [required] | | `-i, --index` | Field to remove index for [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks removeindex pets name codehooks removeindex --collection pets --index 'name' --index 'type' ``` --- ### Key-Value Store ### get Retrieve key-value pair(s) from a space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to query | | `-k, --key` | Key to match, or key* to fetch list [default: "*"] | | `--keyspace` | Keyspace to scan | | `--text` | Output info as text line [boolean] | | `--jsonl` | Output as JSON Lines (one JSON object per line) [boolean] | | `--onlycount` | Output only the count of keys [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | **Examples:** ```bash codehooks get 'my-value-one' codehooks get 'my*' codehooks get '*' --keyspace spacex codehooks get 'my*' --text codehooks get 'my*' --jsonl codehooks get 'my*' --onlycount ``` --- ### set Set key-value pair in a space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use [string] | | `-s, --space` | Select which space to use [string] | | `--key` | Key to set [string] [required] | | `--val` | Value to set [string] [required] | | `--keyspace` | Keyspace to use [string] | | `--ttl` | Time to live in millis for value [number] | | `--json` | Output info as JSON [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | **Examples:** ```bash codehooks set 'my-value-one' 'foo' codehooks set 'my-value-two' 'bar' codehooks set 'session-4f51-9bed' 'OK' --keyspace 'spacex' --ttl 60000 ``` --- ### del Delete key-value pair in a space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--key` | Key to delete [required] | | `--keyspace` | Keyspace to use | | `--json` | Output info as JSON [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | **Examples:** ```bash codehooks del 'my-value-one' codehooks del 'my-value-two' --keyspace 'spacex' ``` --- ### File Management ### file-upload **Aliases:** up, upload Upload files to server **Options:** | Option | Description | |--------|-------------| | `-f, --src` | Path to directory or file [default: "."] | | `-t, --target` | Target directory on server, same as local if not set | | `-e, --ext` | Include files matching the extension [string] | | `--space` | Select which space (environment) to access | | `-p, --projectname` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `--dryrun` | Output files to upload without performing the action [boolean] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks file-upload './static' codehooks file-upload --src './local' --target '/remote' codehooks file-upload --src './local' --target '/remote' --ext 'png' codehooks file-upload --src './local' --target '/remote' --ext 'png|jpg|jpeg|gif' codehooks file-upload afile.txt ``` --- ### file-delete **Aliases:** delete Delete a file from server **Options:** | Option | Description | |--------|-------------| | `-f, --filename` | Delete file with match on absolute path/filename | | `-r, --match` | Delete multiple file that matches regular expression to a file path | | `--space` | Select which space (environment) to access | | `-p, --projectname` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `--dryrun` | Output files to upload without performing the action [boolean] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks file-delete --filename '/static/myfile.txt' codehooks file-delete --match '/static/*' codehooks file-delete --match '.css$' ``` --- ### file-list **Aliases:** files List files from server **Options:** | Option | Description | |--------|-------------| | `-f, --path` | Path to directory or file [default: "."] | | `--space` | Select which space (environment) to access | | `-p, --project` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `--reverse` | Scan index in reverse order | | `--table` | Output data as formatted table (not JSON) [boolean] | | `--json` | Output data as formatted JSON [boolean] | **Examples:** ```bash codehooks file-list '/static/' ``` --- ### Monitoring & Logs ### log **Aliases:** logs Show system logs for a space. **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project name to use | | `-s, --space` | Select which space to log | | `-t, --tail` | Chop log to n lines [default: 100] | | `-f, --follow` | Keep log stream open | | `-c, --context` | Filter log on: jobhooks, queuehooks, routehooks, datahooks, auth | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output log lines as JSON [boolean] | **Examples:** ```bash codehooks log codehooks log -f codehooks log --tail 10 codehooks log --project 'pets-ff00' --space prod ``` --- ### stats Show usage metrics for project spaces **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `--space` | Only show info for this space [string] | | `--json` | Output info as json [boolean] | --- ### workflow-status **Aliases:** wfstat Visualize workflow data from workflowdata collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `-f, --format` | Output format [string] [choices: "json", "markdown", "html", "ascii"] [default: "ascii"] | | `-a, --active` | Show only active (running) workflows [boolean] [default: false] | | `-i, --detailed` | Show detailed Instance Status table (default is summary only) [boolean] [default: false] | | `-v, --verbose` | Show version number [boolean] [default: false] | | `--follow` | Continuously update workflow status (Ctrl+C to exit) [boolean] [default: false] | | `--interval` | Refresh interval in milliseconds when using --follow (min: 1000) [number] [default: 2000] | --- ### queue-status **Aliases:** qs, queues Visualize queue status with item counts and workers **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `-f, --format` | Output format [string] [choices: "table", "compact", "json"] [default: "table"] | | `--follow` | Continuously update queue status (Ctrl+C to exit) [boolean] [default: false] | | `--interval` | Refresh interval in milliseconds when using --follow (min: 1000) [number] [default: 2000] | | `--admintoken` | Use admin token authentication (use for CI) | --- ### Security & Access ### add-token Add token to space **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--readonly` | [boolean] [default: false] | | `--description` | [string] [default: ""] | | `--json` | Output as JSON [boolean] | --- ### remove-token Remove token from space **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `-t, --token` | [string] [required] | | `--json` | Output as JSON [boolean] | --- ### set-env Set environment variable for space **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--encrypted` | [boolean] [default: false] | | `--key` | [string] [required] | | `--value` | [string] [required] | | `--json` | Output as JSON [boolean] | --- ### remove-env Remove environment variable from space **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `-k, --key` | [string] [required] | | `--json` | Output as JSON [boolean] | --- ### jwks Set/replace JWKS endpoint for OAuth2 authentication. Set to "" (empty string) to remove. **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--url` | URL of JWKS endpoint (must be https) [string] [required] | | `--json` | Output as JSON [boolean] | --- ### whitelist Add host to whitelist (use to restrict space access) **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--ip` | IP address which should be allowed access to space [string] [required] | | `--json` | Output as JSON [boolean] | --- ### whitelist-remove Remove host from whitelist **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--ip` | The IP address to remove from the whitelist. [required] | | `--json` | Output as JSON [boolean] | --- ### Admin Tools ### admin Open the codehooks.io admin account UI at account.codehooks.io --- ### admintokens Show active admintokens for account or team **Options:** | Option | Description | |--------|-------------| | `--json` | Output as JSON [boolean] | | `--team` | Team ID (for non-interactive use with --json, use "personal" for personal account) [string] | --- ### add-admintoken Add admin token to account or team (for use with CI) **Options:** | Option | Description | |--------|-------------| | `--json` | Output as JSON [boolean] | | `--team` | Team ID (for non-interactive use with --json, use "personal" for personal account) [string] | --- ### remove-admintoken Remove admin token from account or team **Options:** | Option | Description | |--------|-------------| | `--json` | Output as JSON [boolean] | | `--team` | Team ID (for non-interactive use with --json, use "personal" for personal account) [string] | | `--token` | Admin token ID to remove (for non-interactive use with --json) [string] | --- ### Shell Integration ### completion Generate command completion script. Just add this to your .bashrc, .bash_profile, .zshrc (or similar) on *nix machines --- ## Additional Resources - **Documentation:** [Overview](/docs/) - **API Reference:** [API Cheat Sheet](/docs/apicheatsheet) - **Examples:** [https://github.com/RestDB/codehooks-io-examples](https://github.com/RestDB/codehooks-io-examples) - **MCP Server:** [https://github.com/RestDB/codehooks-mcp-server](https://github.com/RestDB/codehooks-mcp-server) - **Templates:** [https://github.com/RestDB/codehooks-io-templates](https://github.com/RestDB/codehooks-io-templates) - **Support:** [info@codehooks.io](mailto:info@codehooks.io) ## License MIT License - Copyright © Codehooks AS --- ## Client code examples *(Note: This content contains MDX/JSX code)* import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Client applications communicate with a Codehooks API and database via a secure REST API. You can interface with Codehooks from most popular programming languages and platforms, such as: cURL, JavaScript, Python, PHP, Java, C#, Kotlin, R and Swift. In the code examples shown below we use `mystore-fafb` as an example Codehooks project name and `dev` as a project data space. Replace this with your own project name, and use a valid API token (`x-apikey`) or a JWT/JWKS integration (`Bearer `). ## Code examples for popular programming languages `GET` documents from the salesorder collection. ```bash curl --location 'https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` `POST` a new document to the salesorder collection. ```bash curl --location 'https://mystore-fafb.api.codehooks.io/dev/salesorder' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "customerID": 1234, "productID": 21435465, "quantity": 3, "payment": "subscription" }' ``` `PATCH` (update) an existing document in the salesorder collection. ```bash curl --location --request PATCH 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "quantity": 2, "payment": "card" }' ``` `DELETE` a document in the salesorder collection. ```bash curl --location --request DELETE 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` `GET` documents from the salesorder collection. ```js var myHeaders = new Headers(); myHeaders.append('x-apikey', '3c932310-3fab-4ba3-8102-b75ba0f05149'); var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow', }; fetch( 'https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234', requestOptions ) .then((response) => response.json()) .then((result) => console.log(result)) .catch((error) => console.log('error', error)); ``` `POST` a new document to the salesorder collection. ```js var myHeaders = new Headers(); myHeaders.append('x-apikey', '3c932310-3fab-4ba3-8102-b75ba0f05149'); myHeaders.append('Content-Type', 'application/json'); var raw = JSON.stringify({ customerID: 1234, productID: 21435465, quantity: 3, payment: 'subscription', }); var requestOptions = { method: 'POST', headers: myHeaders, body: raw, redirect: 'follow', }; fetch('https://mystore-fafb.api.codehooks.io/dev/salesorder', requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.log('error', error)); ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```js var myHeaders = new Headers(); myHeaders.append('x-apikey', '3c932310-3fab-4ba3-8102-b75ba0f05149'); myHeaders.append('Content-Type', 'application/json'); var raw = JSON.stringify({ quantity: 2, payment: 'card', }); var requestOptions = { method: 'PATCH', headers: myHeaders, body: raw, redirect: 'follow', }; fetch( 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876', requestOptions ) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.log('error', error)); ``` `DELETE` a document in the salesorder collection. ```js var myHeaders = new Headers(); myHeaders.append('x-apikey', '3c932310-3fab-4ba3-8102-b75ba0f05149'); var requestOptions = { method: 'DELETE', headers: myHeaders, redirect: 'follow', }; fetch( 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876', requestOptions ) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.log('error', error)); ``` `GET` documents from the salesorder collection. ```py title="Using http.client" import requests url = "https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234" payload = {} headers = { 'x-apikey': '3c932310-3fab-4ba3-8102-b75ba0f05149' } response = requests.request("GET", url, headers=headers, data=payload) print(response.text) ``` `POST` a new document to the salesorder collection. ```py import http.client import json conn = http.client.HTTPSConnection("mystore-fafb.api.codehooks.io") payload = json.dumps({ "customerID": 1234, "productID": 21435465, "quantity": 3, "payment": "subscription" }) headers = { 'x-apikey': '3c932310-3fab-4ba3-8102-b75ba0f05149', 'Content-Type': 'application/json' } conn.request("POST", "/dev/salesorder", payload, headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```py import http.client import json conn = http.client.HTTPSConnection("mystore-fafb.api.codehooks.io") payload = json.dumps({ "quantity": 2, "payment": "card" }) headers = { 'x-apikey': '3c932310-3fab-4ba3-8102-b75ba0f05149', 'Content-Type': 'application/json' } conn.request("PATCH", "/dev/salesorder/9876", payload, headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` `DELETE` a document in the salesorder collection. ```py import http.client conn = http.client.HTTPSConnection("mystore-fafb.api.codehooks.io") payload = '' headers = { 'x-apikey': '3c932310-3fab-4ba3-8102-b75ba0f05149' } conn.request("DELETE", "/dev/salesorder/9876", payload, headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` `GET` documents from the salesorder collection. ```cs title="Using HttpClient" var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Get, "https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234"); request.Headers.Add("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` `POST` (create) a new document to the salesorder collection. ```cs var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Post, "https://mystore-fafb.api.codehooks.io/dev/salesorder"); request.Headers.Add("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149"); var content = new StringContent("{\n \"customerID\": 1234,\n \"productID\": 21435465,\n \"quantity\": 3,\n \"payment\": \"subscription\"\n}", null, "application/json"); request.Content = content; var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```cs var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Patch, "https://mystore-fafb.api.codehooks.io/dev/salesorder/9876"); request.Headers.Add("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149"); var content = new StringContent("{\n \"quantity\": 2,\n \"payment\": \"card\"\n}", null, "application/json"); request.Content = content; var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` `DELETE` a document in the salesorder collection. ```cs var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Delete, "https://mystore-fafb.api.codehooks.io/dev/salesorder/9876"); request.Headers.Add("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` `GET` documents from the salesorder collection. ```php title="Using Guzzle" '3c932310-3fab-4ba3-8102-b75ba0f05149' ]; $request = new Request('GET', 'https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234', $headers); $res = $client->sendAsync($request)->wait(); echo $res->getBody(); ``` `POST` (create) a new document to the salesorder collection. ```php '3c932310-3fab-4ba3-8102-b75ba0f05149', 'Content-Type' => 'application/json' ]; $body = '{ "customerID": 1234, "productID": 21435465, "quantity": 3, "payment": "subscription" }'; $request = new Request('POST', 'https://mystore-fafb.api.codehooks.io/dev/salesorder', $headers, $body); $res = $client->sendAsync($request)->wait(); echo $res->getBody(); ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```php '3c932310-3fab-4ba3-8102-b75ba0f05149', 'Content-Type' => 'application/json' ]; $body = '{ "quantity": 2, "payment": "card" }'; $request = new Request('PATCH', 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876', $headers, $body); $res = $client->sendAsync($request)->wait(); echo $res->getBody(); ``` `DELETE` a document in the salesorder collection. ```php '3c932310-3fab-4ba3-8102-b75ba0f05149' ]; $request = new Request('DELETE', 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876', $headers); $res = $client->sendAsync($request)->wait(); echo $res->getBody(); ``` `GET` documents from the salesorder collection. ```java title="Using Unirest" Unirest.setTimeouts(0, 0); HttpResponse response = Unirest.get("https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234") .header("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .asString(); ``` `POST` (create) a new document to the salesorder collection. ```java Unirest.setTimeouts(0, 0); HttpResponse response = Unirest.post("https://mystore-fafb.api.codehooks.io/dev/salesorder") .header("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .header("Content-Type", "application/json") .body("{\n \"customerID\": 1234,\n \"productID\": 21435465,\n \"quantity\": 3,\n \"payment\": \"subscription\"\n}") .asString(); ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```java Unirest.setTimeouts(0, 0); HttpResponse response = Unirest.patch("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876") .header("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .header("Content-Type", "application/json") .body("{\n \"quantity\": 2,\n \"payment\": \"card\"\n}") .asString(); ``` `DELETE` a document in the salesorder collection. ```java Unirest.setTimeouts(0, 0); HttpResponse response = Unirest.delete("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876") .header("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .asString(); ``` `GET` documents from the salesorder collection. ```swift title="Using URLSession" var request = URLRequest(url: URL(string: "https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234")!,timeoutInterval: Double.infinity) request.addValue("3c932310-3fab-4ba3-8102-b75ba0f05149", forHTTPHeaderField: "x-apikey") request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() ``` `POST` (create) a new document to the salesorder collection. ```swift let parameters = "{\n \"customerID\": 1234,\n \"productID\": 21435465,\n \"quantity\": 3,\n \"payment\": \"subscription\"\n}" let postData = parameters.data(using: .utf8) var request = URLRequest(url: URL(string: "https://mystore-fafb.api.codehooks.io/dev/salesorder")!,timeoutInterval: Double.infinity) request.addValue("3c932310-3fab-4ba3-8102-b75ba0f05149", forHTTPHeaderField: "x-apikey") request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" request.httpBody = postData let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```swift let parameters = "{\n \"quantity\": 2,\n \"payment\": \"card\"\n}" let postData = parameters.data(using: .utf8) var request = URLRequest(url: URL(string: "https://mystore-fafb.api.codehooks.io/dev/salesorder/9876")!,timeoutInterval: Double.infinity) request.addValue("3c932310-3fab-4ba3-8102-b75ba0f05149", forHTTPHeaderField: "x-apikey") request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpMethod = "PATCH" request.httpBody = postData let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() ``` `DELETE` a document in the salesorder collection. ```swift var request = URLRequest(url: URL(string: "https://mystore-fafb.api.codehooks.io/dev/salesorder/9876")!,timeoutInterval: Double.infinity) request.addValue("3c932310-3fab-4ba3-8102-b75ba0f05149", forHTTPHeaderField: "x-apikey") request.httpMethod = "DELETE" let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() ``` `GET` documents from the salesorder collection. ```kotlin title="Using Okhttp" val client = OkHttpClient() val request = Request.Builder() .url("https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234") .addHeader("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .build() val response = client.newCall(request).execute() ``` `POST` (create) a new document to the salesorder collection. ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = "{\n \"customerID\": 1234,\n \"productID\": 21435465,\n \"quantity\": 3,\n \"payment\": \"subscription\"\n}".toRequestBody(mediaType) val request = Request.Builder() .url("https://mystore-fafb.api.codehooks.io/dev/salesorder") .post(body) .addHeader("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = "{\n \"quantity\": 2,\n \"payment\": \"card\"\n}".toRequestBody(mediaType) val request = Request.Builder() .url("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876") .patch(body) .addHeader("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() ``` `DELETE` a document in the salesorder collection. ```kotlin val client = OkHttpClient() val mediaType = "text/plain".toMediaType() val body = "".toRequestBody(mediaType) val request = Request.Builder() .url("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876") .method("DELETE", body) .addHeader("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .build() val response = client.newCall(request).execute() ``` `GET` documents from the salesorder collection. ```r title="Using RCurl" library(RCurl) headers = c( "x-apikey" = "3c932310-3fab-4ba3-8102-b75ba0f05149" ) res <- getURL("https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234", .opts=list(httpheader = headers, followlocation = TRUE)) cat(res) ``` `POST` (create) a new document to the salesorder collection. ```r library(RCurl) headers = c( "x-apikey" = "3c932310-3fab-4ba3-8102-b75ba0f05149", "Content-Type" = "application/json" ) params = "{ \"customerID\": 1234, \"productID\": 21435465, \"quantity\": 3, \"payment\": \"subscription\" }" res <- postForm("https://mystore-fafb.api.codehooks.io/dev/salesorder", .opts=list(postfields = params, httpheader = headers, followlocation = TRUE), style = "httppost") cat(res) ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```r library(RCurl) headers = c( "x-apikey" = "3c932310-3fab-4ba3-8102-b75ba0f05149", "Content-Type" = "application/json" ) params = "{ \"quantity\": 2, \"payment\": \"card\" }" res <- getURLContent("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876", customrequest = "PATCH", postfields = params, httpheader = headers, followlocation = TRUE) cat(res) ``` `DELETE` a document in the salesorder collection. ```r library(RCurl) headers = c( "x-apikey" = "3c932310-3fab-4ba3-8102-b75ba0f05149" ) res <- httpDELETE("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876", httpheader = headers, followlocation = TRUE) cat(res) ``` ## Links for further reading - [Database REST API](/docs/database-rest-api) - [Database query language](/docs/nosql-database-query-language) - [Serverless function NoSQL Database API](/docs/nosql-database-api) --- ## Concepts overview *(Note: This content contains MDX/JSX code)* Codehooks.io is the agent-native backend platform. A complete backend — API routes, database, queues, workers, cron, auth, and frontend hosting — that deploys in seconds from the CLI. Your coding agent creates, deploys, verifies, and iterates without ever leaving the terminal. Forget about assembling Lambda + API Gateway + DynamoDB + SQS — just focus on your business logic. Codehooks includes these main features: - **Studio** - Developer friendly web based tools for managing both data and code - **Node.js and JavaScript** - Same language for frontend and backend development (ES6 and Typescript) - **NPM** - Node.js package manager (npm) integration - **Node.js Express** - API development with Express-like API - **NoSQL** - Document database with Mongodb-like API - **Key-Value** - Key-Value database with a Redis-like API (subset) - **Worker Queues** - Persistent queues with worker functions - **Background jobs** - Cron jobs with scheduled worker functions - **CLI** - Powerful Command Line Interface (CLI) for productivity and DevOps/CI support Read on to learn more about how Codehooks.io simplifies backend development — whether you're building directly or through a coding agent. > Unless you've already have, please read the [quick start](/docs) first to learn how to install the CLI, and how to sign up/login to create your first Codehooks project. ## Codehooks main concepts ### Project A project is the top level concept in Codehooks. A project is owned by an account (private or company) and contains one or multiple "spaces" with your deployed code, data and access credentials in an isolated and secure unit. You can create multiple projects within your account and you can join/invite others to a project. An example project structure is shown below: - jane@example.com (private or corporate account owner) - **customers** (project name) - **dev** (development space) - Database instance - Security settings (API tokens, environment secrets, IP filters ...) - Source code (deployed version 1.1.12) - Developers (joe@example.com, pete@example.com) - **prod** (production space) - Database instance - Security settings (API tokens, environment secrets, IP filters ...) - Source code (deployed version 1.1.0) - Developers (joe@example.com, pete@example.com) - **salesdb** (project name for another project) - ... Projects are created with the CLI command `coho create` ([CLI docs here](cli#create)) :::info note You can also manage your projects and spaces using the account user interface at [https://account.codehooks.io](https://account.codehooks.io). ::: ### The project application source code A Codehooks application follows the familiar principles and best practices of modern JavaScript development. A project directory typically contains: - An `index.js` file for the application main entry code - A `package.json` file with library dependencies - A `config.json` file with project information We also recommend that you add source control (GIT) to manage your versions and branches. Typically this will be a git repo with branches that are deployed to various spaces. ### A minimal example The following CLI commands create a project and deploy it to the Codehooks cloud. These are the same commands a coding agent like Claude Code or Cursor would run autonomously. Create and setup a new project application: ```bash coho create example cd example npm init --yes npm install codehooks-js --save ``` After running the commands your project directory contains these files: ```bash . ├── config.json ├── index.js ├── node_modules ├── package-lock.json └── package.json ``` If you inspect the source file index.js you'll see the default generated application code: ```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(); ``` We can now deploy the application (in our case example-4g9p) to the cloud with the CLI command `coho deploy` ([CLI docs here](cli#deploy)). ```bash coho deploy # Server output example Project: example-4g9p Space: dev Deployed Codehook successfully! 🙌 ``` After deployment we can inspect the application details with the `coho info` command, in this example showing our default space `dev` with its base endpoint URL and access token(s): ```bash coho info --examples Project name: example-4g9p Team: YOUR-NAME (personal account) API endpoint: https://example-4g9p.api.codehooks.io/dev/* Spaces: ┌──────────────┬────────────────────────────────────────────┬──────┬──────┐ │ Name │ Tokens │ Jwks │ Env │ ├──────────────┼────────────────────────────────────────────┼──────┼──────┤ │ dev (active) │ a77926ca-xxx-yyyy-zzzzz-14eee564f8d5 (RW) │ │ │ └──────────────┴────────────────────────────────────────────┴──────┴──────┘ ``` After successful deployment we can test our application endpoint with curl. This minimal example and test shows that our application is deployed to a secure endpoint and returns the expected result: ```bash curl -X GET 'https://example-4g9p.api.codehooks.io/dev' \ -H 'x-apikey: a77926ca-xxx-yyyy-zzzzz-14eee564f8d5' # API output example from server CRUD server ready ``` ### Project spaces A project space is a self-contained and isolated bundle of code, datastores and security settings within a specific project. All projects have a default space called `dev` which is created automatically when you add a project. You create new spaces with the `coho add` command ([CLI docs here](cli#add)). You can easily switch between different project spaces with the `coho use` command ([CLI docs here](cli#use)). Spaces can also have restricted access (team ADMINs only), which is convenient for production deployments. For example, in your project you want to have isolated development, testing and production spaces. ``` Project-X └── dev    ├── source code (git branch dev)    ├── security settings & env variables    └── data stores └── test    ├── source code (git branch test)    ├── security settings & env variables    └── data stores └── prod (restricted)    ├── source code (git branch prod)    ├── security settings & env variables    └── data stores ``` ### Deploy your application to the Codehooks serverless cloud Each project space is created as an isolated resource group in the Codehooks cloud. For example, the `coho add prod` command creates a project space called `prod`. By switching to the space with the `coho use prod` command, this space is now the active (changed in the config.json project file). On deployment with the `coho deploy` command, the current version of your application source code is packed and distributed as a serverless runtime to the **prod** space in the Codehook cloud. Similarily, switching to another space, e.g. by using the `coho use dev`, sets the **dev** space as the active, and `coho deploy` will deploy the application to the active **dev** space. This is a powerful feature, which effectively enables you to run multiple versions of your application with separate settings, environments and data stores. ### Built in Datastores Persisting, querying and manipulating data is essential in any application. Codehooks comes with a built in datastore that supports **streaming** NoSQL and Key-Value operations. No need to think about drivers and protocols, it's built directly into the Codehooks APIs. - NoSQL object data are accessed with the standard [Database API](nosql-database-api). - Key-Value data are accessed with the standard [Key-Value API](key-value-database-api) #### Data wrangling Using the CLI command `coho import`, you can easily import any CSV or JSON file into a datastore. Furthermore, the `coho query` command lets you query and transform data in advanced ways. ```bash title="Example: import and query of Stock data" coho import -c stocks -f ~/Downloads/all_stocks_5yr.csv coho query stocks --query 'Name=GOOG&open>10' --limit 2 --table ┌───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┐ │ date │ open │ high │ low │ close │ volume │ Name │ _id │ ├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 2013-03-… │ 15.98 │ 16.36 │ 15.93 │ 16.25 │ 8383300 │ GOOG │ 18007b5f… │ ├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 2013-03-… │ 14.7 │ 14.93 │ 14.5 │ 14.82 │ 9125300 │ GOOG │ 18007b5f… │ └───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┘ ``` ### Fast development flow The development process with Codehooks is fast — whether you're working in an editor or your coding agent is driving the CLI. A typical development flow: 1. Write or generate code in `index.js` and other source files 2. Set the active space, e.g. `coho use dev` 3. Deploy with `coho deploy` 4. Check logs with `coho log --follow` 5. Fix issues and redeploy with `coho deploy` Every step is a CLI command, which means coding agents can run the full create → deploy → verify → iterate loop without human intervention. ### Command Line Interface (CLI) The Codehooks CLI is how you — or your coding agent — manage projects and applications. Every operation is available as a CLI command: creating projects, deploying code, managing secrets, querying data, and checking logs. This CLI-first design is what makes Codehooks agent-native. Codehooks also has a complete set of features for teams to collaborate. The [account UI](https://account.codehooks.io) lets you create teams and invite team members. Read more about the CLI and the full set of commands and features [here](cli). :::info note You can also manage your projects and spaces using the account user interface at [https://account.codehooks.io](https://account.codehooks.io). ::: --- ## REST API query and Database CRUD API *(Note: This content contains MDX/JSX code)* A new Codehooks.io application has a complete and secure REST API for basic database CRUD (Create, Read, Update, Delete) operations using REST API queries. The CRUD REST API is implemented by bundling the deployed application with the NPM package [codehooks-js](https://www.npmjs.com/package/codehooks-js) and the [crudlify API](/docs/appeventapi#appcrudlifyschema-options). When you create a new project a default auto generated application is deployed, this application implements a full database CRUD REST API with built-in NoSQL query functionality. The code for this is shown below. ```js import { app } from 'codehooks-js'; app.crudlify(); // REST CRUD API export default app.init(); ``` All database URL endpoints are prefixed with the project ID, Codehooks API endpoint, and the database space name. For example, if your project ID is `myproject` and the database space is `dev`, then the full URL endpoint for the REST API is: `https://myproject-ff00.api.codehooks.io/dev/people?name=Ally`. ```mermaid graph TD A[REST API] --> B[CRUD Operations] B --> C[Create=POST] B --> D[Read/query=GET] B --> E[Update=PUT/PATCH] B --> F[Delete=DELETE] C --> G[(Database)] D --> G[(Database)] E --> G[(Database)] F --> G[(Database)] accTitle: Database REST API query and CRUD accDescr: You use the REST API to update the database using CRUD operations and REST API query ``` ## Overview of Database REST API operations and queries | API description | HTTP verb | Route | | :------------------------------------------------------------------------ | --------- | --------------------------- | | [Get list of documents](#get-list-of-documents) | `GET` | `/:collection` | | [Get documents by query](#get-documents-by-query) | `GET` | `/:collection?query` | | [Get document by ID](#get-document-by-id) | `GET` | `/:collection/:ID` | | [Create a new document](#create-a-new-document) | `POST` | `/:collection` | | [Update a document by ID](#update-a-document-by-id) | `PATCH` | `/:collection/:ID` | | [Replace a document by ID](#replace-a-document-by-id) | `PUT` | `/:collection/:ID` | | [Delete a document by ID](#delete-a-document-by-id) | `DELETE` | `/:collection/:ID` | | [Update multiple documents by query](#update-multiple-documents-by-query) | `PATCH` | `/:collection/:ID/_byquery` | | [Delete multiple documents by query](#delete-multiple-documents-by-query) | `DELETE` | `/:collection/:ID/_byquery` | :::note Database collections Create and manage collections with the Codehooks CLI command [`coho createcollection`](/docs/cli#createcollection) or using the Codehooks Studio [➕ Create collection] menu. ::: :::note Security All databases are protected with encrypted HTTPS protocol, secure API tokens or JWT tokens. You can also add IP filters and much more, read more about [Authentication here](/docs/authentication). **API tokens**: List your database API tokens with the CLI command [`coho info`](/docs/cli.md#info). Create new API tokens with the CLI command [`coho add-token`](/docs/cli.md#add-token). ::: ## Database REST API ### Get list of documents Retrieve all collection data. **URL:** `/:collection[?options]` **Method:** `GET` **Parameters:** - **collection**: a valid collection name, e.g. `people` **Options:** - **limit**: limit result, e.g. `?limit=2` - **offset**: skip forward in data result set - **fields**: comma separated list of fields to show, e.g. `?limit=2&fields=Last Name,Sex` - **sort**: comma separated list of fields to sort result by, e.g. `?limit=2&sort=-Sex,Last Name` **Returns** Array of JSON documents **Code example** ```bash curl 'https://myproject-ff00.api.codehooks.io/dev/people?limit=2' \ -H 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json [ { "Index": 54901, "User Id": "d088FCEC6EDEF20", "First Name": "Michaela", "Last Name": "Callahan", "Sex": "Male", "Email": "yesenia31@example.net", "Phone": "108-950-4850x6836", "Date of birth": "2010-11-07", "Job Title": "Research scientist (maths)", "_id": "64bb88b51ed9a3057ac99d9c" }, { "Index": 54902, "User Id": "40Fd72E5AaC5b67", "First Name": "Lacey", "Last Name": "Saunders", "Sex": "Male", "Email": "vcarey@example.net", "Phone": "623.506.2528x932", "Date of birth": "1946-06-28", "Job Title": "Insurance claims handler", "_id": "64bb88b51ed9a3057ac99d9d" } ] ``` **Error response** `401 no access` ```html 401 no access ``` ### REST API query - get documents by query Retrieve collection data filtered by a query parameter. :::tip For a complete overview of the NoSQL query language and how to perform the REST API queries, check out the [NoSQL and REST API Query language](nosql-database-query-language) documentation. ::: **URL:** `/:collection[?query&options]` **Method:** `GET` **Parameters:** - **collection**: a valid collection name, e.g. `people` **Query:** - Simple REST API queries using URL parameters: - **name=value** - e.g. `?First Name=Michaela` - Advanced MongoDB query using URL JSON: - **q=\{\.\.\.\}** - e.g. `?q={"First Name": {"$regex": "en"}, "Last Name": {"$in": ["Saunders", "Massey"]}}` **Options:** - **limit**: limit result, e.g. `?limit=2` - **offset**: skip forward in data result set - **fields**: comma separated list of fields to show, e.g. `?limit=2&fields=Last Name,Sex` - **sort**: comma separated list of fields to sort result by, e.g. `?limit=2&sort=-Sex,Last Name` **Returns** Array of JSON documents **Code example:** simple query ```bash curl --location 'https://myproject-ff00.api.codehooks.io/dev/people?First%20Name=Michaela&fields=First%20Name%2CEmail%2C_id' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json [ { "First Name": "Michaela", "Email": "yesenia31@example.net", "_id": "64bb88b51ed9a3057ac99d9c" } ] ``` **Code example:** advanced query :::tip URL encoding Notice that advanced query parameter `q={...}` should be programmatically URL encoded before sent to the server. E.g. the query: ```js let query = { 'First Name': { $regex: 'en' }, 'Last Name': { $in: ['Saunders', 'Massey'] }, }; ``` For example, you can use JavaScript's `query = encodeURIComponent(JSON.stringify(query))`: Then the encoded query equals: ```html {%22First%20Name%22%3A{%22%24regex%22%3A%22en%22}%2C%22Last%20Name%22%3A{%22%24in%22%3A[%22Saunders%22%2C%22Massey%22]}} ``` ::: ```bash curl --location --globoff 'https://myproject-ff00.api.codehooks.io/dev/people?q={%22First%20Name%22%3A{%22%24regex%22%3A%22en%22}%2C%22Last%20Name%22%3A{%22%24in%22%3A[%22Saunders%22%2C%22Massey%22]}}' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json [ { "Index": 54907, "User Id": "18E57F5a6aaC1ab", "First Name": "Darlene", "Last Name": "Saunders", "Sex": "Female", "Email": "noah55@example.net", "Phone": "001-158-700-3226", "Date of birth": "1970-08-23", "Job Title": "Scientist, forensic", "_id": "64bb88b51ed9a3057ac99da2" }, { "Index": 54933, "User Id": "aeD915eA429Fb02", "First Name": "Lauren", "Last Name": "Massey", "Sex": "Male", "Email": "stuart55@example.net", "Phone": "+1-716-581-5746x2442", "Date of birth": "2001-10-21", "Job Title": "Information systems manager", "_id": "64bb88b51ed9a3057ac99dbc" } ] ``` **Error response** `401 no access` ```html 401 no access ``` **Error response** `400 Bad Request` ```html Unexpected token } in JSON at position 5 ``` ### Get document by ID Retrieve a document from a collection. **URL:** `/:collection/:ID` **Method:** `GET` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **ID**: an existing document `_id` value **Returns** A JSON document **Code example** ```bash curl --location 'https://myproject-ff00.api.codehooks.io/dev/people/64bb88b51ed9a3057ac99d9c' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json { "Index": 54901, "User Id": "d088FCEC6EDEF20", "First Name": "Michaela", "Last Name": "Callahan", "Sex": "Male", "Email": "yesenia31@example.net", "Phone": "108-950-4850x6836", "Date of birth": "2010-11-07", "Job Title": "Research scientist (maths)", "_id": "64bb88b51ed9a3057ac99d9c" } ``` **Error response** `401 no access` ``` 401 no access ``` **Error response** `404 Not Found` ``` 3 INVALID_ARGUMENT: NotFoundError: Key not found in database [64bb88b51ed9a3057ac99d9cc] ``` ### Create a new document Insert a new JSON document in a collection. **URL:** `/:collection` **Method:** `POST` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **body**: a valid JSON document **Returns** The created JSON document **Code example** ```bash curl --location 'https://myproject-ff00.api.codehooks.io/dev/people' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data-raw '{ "First Name": "Jim", "Last Name": "Callahan", "Email": "jim@example.net", "Phone": "108-950-4850x6836", "Date of birth": "1995-11-07", "Job Title": "Software tester" }' ``` **Success response** `201 Created` ```js { "First Name": "Jim", "Last Name": "Callahan", "Email": "jim@example.net", "Phone": "108-950-4850x6836", "Date of birth": "1995-11-07", "Job Title": "Software tester", "_id": "64bbc861f13609074a5d981a" } ``` **Error response** `401 no access` ``` 401 no access ``` **Error response** `400 Bad Request` E.g. error when POST an invalid JSON document or a JSON-schema `schemaError` error occurs. ``` { "schemaError": [ { "instancePath": "", "schemaPath": "#/required", "keyword": "required", "params": { "missingProperty": "firstName" }, "message": "must have required property 'firstName'" } ] } ``` ### Update a document by ID Update a JSON document in a collection. **URL:** `/:collection/:ID` **Method:** `PATCH` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **ID**: a valid document `_id` - **body**: a valid JSON document **Returns** The updated JSON document **Code example** ```bash curl --location --request PATCH 'https://myproject-ff00.api.codehooks.io/dev/people/64bbc861f13609074a5d981a' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "Phone": "123-345-12332x6836", "Job Title": "Bug master" }' ``` **Success response** `200 OK` ```js { "First Name": "Jim", "Last Name": "Callahan", "Email": "jim@example.net", "Phone": "123-345-12332x6836", "Date of birth": "1995-11-07", "Job Title": "Bug master", "_id": "64bbc861f13609074a5d981a" } ``` **Error response** `401 no access` ``` 401 no access ``` **Error response** `400 Bad Request` E.g. invalid JSON document or `schemaError`. ``` { "schemaError": [ ... ] } ``` ### Replace a document by ID Replace a JSON document in a collection. **URL:** `/:collection/:ID` **Method:** `PUT` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **ID**: a valid document `_id` - **body**: a valid JSON document **Returns** The replaced JSON document **Code example** ```bash curl --location --request PUT 'https://myproject-ff00.api.codehooks.io/dev/people/64bbc861f13609074a5d981a' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "First Name": "Re", "Last Name": "Placed", "Job Title": "Replacer" }' ``` **Success response** `200 OK` ```json { "First Name": "Re", "Last Name": "Placed", "Job Title": "Replacer", "_id": "64bbc861f13609074a5d981a" } ``` **Error response** `401 no access` ``` 401 no access ``` E.g. invalid JSON document or `schemaError`. ``` { "schemaError": [ ... ] } ``` ### Delete a document by ID Delete a JSON document in a collection. **URL:** `/:collection/:ID` **Method:** `DELETE` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **ID**: a valid document `_id` **Returns** The deleted document \_id **Code example** ```bash curl --location --request DELETE 'https://myproject-ff00.api.codehooks.io/dev/people/64bbc861f13609074a5d981a' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json { "_id": "64bbc861f13609074a5d981a" } ``` **Error response** `401 no access` ``` 401 no access ``` ### Update multiple documents by query Update multiple JSON document by query in a collection. **URL:** `/:collection/_byquery[?query&options]` **Method:** `PATCH` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **body**: a valid JSON document to update on all matches **Query:** - Simple REST API queries using URL parameters: - **name=value** - e.g. `?First Name=Michaela` - Advanced MongoDB query using URL JSON: - **q=\{\.\.\.\}** - e.g. `?q={"First Name": {"$regex": "en"}, "Last Name": {"$in": ["Saunders", "Massey"]}}` **Returns** Count of updated documents **Code example** ```bash curl --location --request PATCH 'https://myproject-ff00.api.codehooks.io/dev/people/_byquery?Job%20Title=%22Scientist%2C%20forensic%22' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "$set": { "salary": "high" }, "$inc": { "visited": 1 } }' ``` **Success response** `200 OK` ```json { "count": 2 } ``` **Error response** `401 no access` ``` 401 no access ``` **Error response** `400 Bad Request` E.g. invalid JSON document or `schemaError`. ``` { "schemaError": [ ... ] } ``` ### Delete multiple documents by query Delete multiple JSON document by query in a collection. :::warning Erases all matched documents by query Please ensure that the applied query is correct. An empty query parameter will erase all documents in collection. ::: **URL:** `/:collection/_byquery[?query&options]` **Method:** `DELETE` **Parameters:** - **collection**: a valid collection name, e.g. `people` **Query:** - Simple REST API queries using URL parameters: - **name=value** - e.g. `?First Name=Michaela` - Advanced MongoDB query using URL JSON: - **q=\{\.\.\.\}** - e.g. `?q={"First Name": {"$regex": "en"}, "Last Name": {"$in": ["Saunders", "Massey"]}}` **Returns** Count of deleted documents **Code example** ```bash curl --location --request DELETE 'http://myproject-ff00.api.codehooks.io/dev/people/_byquery?Job%20Title=%22Scientist%2C%20forensic%22' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json { "count": 2 } ``` **Error response** `401 no access` ``` 401 no access ``` ## Data Schema support We recommend to add JSON-schema 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. ![json-schema](./images/studio/json-schema.webp) However, you can also take full control over the data validation in your code using these popular technologies described in the following sections. Codehooks supports these popular data and validation schemas: - [Yup](https://www.npmjs.com/package/yup) - Dead simple Object schema validation - [Zod](https://www.npmjs.com/package/zod) - TypeScript-first schema validation with static type inference - [JSON.schema](https://www.npmjs.com/package/z-schema) - Standard declarative language that allows you to annotate and validate JSON documents. ### Data validation with Yup The code example below shows how easy it is to validate data with a [Yup](https://www.npmjs.com/package/yup) data schema. Run `npm i yup` in project folder. ```js import { app } from 'codehooks-js'; import { object, string, number, date } from 'yup'; // database schema const userSchema = object({ name: string().required(), age: number().required().positive().integer(), email: string().email(), website: string().url().nullable(), createdOn: date().default(() => new Date()), }); app.crudlify({ user: userSchema }); export default app.init(); // export app to a runtime server engine ``` ### Data validation with JSON-schema The next code example uses JSON-schema to validate data. Run `npm i z-schema` in project folder. ```js import { app } from 'codehooks-js'; import ZSchema from 'z-schema'; const userSchema = { id: 'personDetails', type: 'object', properties: { firstName: { type: 'string' }, lastName: { type: 'string' }, }, required: ['firstName', 'lastName'], }; app.crudlify({ user: userSchema }); // bind to serverless runtime export default app.init(); ``` ### Data validation with Zod Zod is another popular data validation library. Run `npm i zod` in project folder. A simple code example using Zod is show below. ```js import { app } from 'codehooks-js'; // Standard Codehooks.io lib import { z } from 'zod'; const userSchema = z .object({ username: z.string(), email: z.string().email(), status: z.boolean().default(true), }) .required({ username: true, email: true }); app.crudlify({ user: userSchema }); export default app.init(); // Bind functions to the serverless cloud ``` ## Database event hooks middleware To provide additional CRUD logic, events are triggered before and after a database operation. ```js hooks.before(, handlerFunction) hooks.after(, handlerFunction) ``` Example event hooks are shown in the code example below. ```js ... // collapsed code app.crudlify({user: userSchemaYup}, options).then((hooks) => { hooks.beforePOST('user', async (data) => { console.log("User data before saving", data) // abort operation with a throw, cases 404 status code // E.g. throw new Error(`BAD post for ${data}`) // mutate data before saved to the database data.foo = 'Was here!' }) hooks.afterPOST('user', async (data) => { console.log("User data after saved to the database", data) }) }) ... // collapsed code ``` ## Examples ### Insert a new `user` to the database POST a new user using curl. ```bash curl -X POST \ 'https://myproject-fef0.api.codehooks.io/dev/user' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Ally", "email": "ally@example.com" }' ``` ### Validate data against a Yup data schema Check that the data schema validates correctly by sending an invalid email address. ```bash curl -X POST \ 'https://myproject-fef0.api.codehooks.io/dev/user' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Sally", "email": "sally.example.com" }' ``` Validation error shows that Yup works. ```bash 400 Bad Request { "value": { "active": true, "email": "sally.example.com", "name": "Sally" }, "path": "email", "type": "email", "errors": [ "email must be a valid email" ], ... # chopped error message } ``` ### Run a database REST API query ```bash curl -X GET \ 'https://myproject-fef0.api.codehooks.io/dev/user?name=Ally' \ --header 'Content-Type: application/json' ``` Example output. ```json [ { "_id": "63fb97825f624f479034eb08", "active": true, "email": "ally@example.com", "name": "Ally" } ] ``` ### Update a record in the database ```bash curl -X PATCH \ 'https://myproject-fef0.api.codehooks.io/dev/user/63fb97825f624f479034eb08' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Ally Mc. Beal" }' ``` ### Querying the database using the REST API You can query the database REST API in two different ways. **Simple** to use and quick queries with the URL: ```url https://myproject-fef0.api.codehooks.io/dev/user?name=jane&age>23&limit=2&offset=5&sort=email&fields=name,age ``` Which actually produces this query object: ```js { name: 'Jane', age: { '$gt': 23 } }, { fields: { name: 1, age: 1 }, sort: { email: 1 }, skip: 5, limit: 2, projection: { name: 1, age: 1 } } ``` For **advanced** use, and programmatic approach, pass inn the full NoSQL/JSON REST API query and hints as URL parameters: ```url https://myproject-fef0.api.codehooks.io/dev/user?q={"name": "Jane", "age": {"$gt": 23}}&h={"fields": { "name": 1, "age": 1 },"sort": {"email": 1 },"skip": 5,"limit": 2,"projection": { "name": 1, "age": 1 }} ``` The last option would probably use `JSON.stringify(query)` etc. to produce a valid query in the URL. --- ## Data operators *(Note: This content contains MDX/JSX code)* import TOCInline from '@theme/TOCInline'; Data objects in a datastore can be manipulated in any update operation. To manipulate one or many data object(s) a range of special operators can be applied using the [`updateOne(...)`](nosql-database-api#updateonecollection-id-document) or [`updateMany(...)`](nosql-database-api#updatemanycollection-document-options) API. **Operators quick overview** ## **$inc** operator Increase or decrease a field value on an existing datastore object. **Syntax**: `{ $inc: { : , : , ... } }` Example: ```js // before {..., "followers": 42} const operator = {$inc: {"followers": 1}} // Call update API, e.g. updateMany or updateOne await conn.updateMany('mycollection', {"status": "ACTIVE"}, operator) // after {..., "followers": 43} ``` 👉 _Use negative values to decrement field._ ## **$push** operator Append an element to a sub array on an existing datastore object. **Syntax**: `{ $push: { : , ... } }` Example: ```js // before {"followers": 42, "comments": ["one", "three"]} const operator = {$push: {"comments": "two"}} // E.g.: await conn.updateOne('customer', ID, operator); // after {"followers": 42, "comments": ["one", "three", "two"]} ``` ## **$pull** operator Remove an element from a sub array on an existing datastore object. Pulls all matches from array. **Syntax**: `{ $pull: { : |, : |, ... } }` Example: ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$pull: {"comments": "two"}} // E.g.: await conn.updateOne('customer', ID, operator); // after {"followers": 42, "comments": ["one", "three"]} ``` ## **$pop** operator Removes last element of a sub array on an existing datastore object. **Syntax**: `{ $pop: { : -1 | 1, ... } }` -1 pop first item of array, 1 pop last item of array. Example: ```js // before {"followers": 42, "comments": ["one", "three", "two"]} const operator = {$pop: {"comments": 1} // E.g.: await conn.updateOne('customer', ID, operator); // after {"followers": 42, "comments": ["one", "three"]} ``` ## **$addToSet** operator Insert an unique element to a sub array on an existing datastore object. Nothing is inserted if item already exists in set. **Syntax**: `{ $addToSet: { : , ... } }` Example: ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$addToSet: {"items": {"name": "one", "price": 1}}} // E.g.: await conn.updateOne('customer', ID, operator); // after { "followers": 42, "comments": ["one", "two", "three"], "items": [{"name": "one", "price": 1}] } ``` ## **$set** operator Set any object value. **Syntax**: `{ $set: { : , ... } }` Example: ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$set: {"address": {"street": "Main street", "ZIP": 1123}}} // E.g.: await conn.updateOne('customer', ID, operator); // after { "followers": 42, "comments": ["one", "two", "three"], "address": {"street": "Main street", "ZIP": 1123} } ``` ## **$unset** operator Delete an object property. **Syntax**: `{ $unset: { : "", ... } }` ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$unset: {"comments": ""}} // E.g.: await conn.updateOne('customer', ID, operator); // after { "followers": 42 } ``` ## **$rename** operator Rename an object property. **Syntax**: `{$rename: { : , : , ... } }` ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$rename: {"comments": "claps"}} // E.g.: await conn.updateOne('customer', ID, operator); // after { "followers": 42, "claps": ["one", "two", "three"] } ``` --- ## 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 ![upload](./images/fileapi/upload.png) ### Download a PDF file ![download](./images/fileapi/download.png) --- ## 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. ![json-schema](./images/studio/json-schema.webp) ### 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. ![real-time web chat](./images/webchat.webp) [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: ![Workflow data in Codehooks Studio](images/workflowdata.webp) 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 =
body text here
, index = -1, }) => { return (
{icon} {title}
{index > -1 ? (
{' '}
) : null}
{content}
); }; export const Feature = ({ icon, title = 'No title', content =
body text here
, nextsteps = [], }) => { return (
{icon}

{title}

{content}
{nextsteps.length > 0 && (

Learn more

    {nextsteps.map((step, i) => (
  • {step}
  • ))}
)}
); }; ## 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) :::
} title="API & Webhook Development" content="REST, CRUD, custom APIs & webhooks" /> } title="Datastore" content="NoSQL & Key-Value" /> } title="Scheduled Jobs & Events" content="CRON schedules & event triggers" /> } title="Queues" content="Process webhooks asynchronously" /> } title="Workflows" content="Chain webhooks into reliable processes" /> } title="File System" content="Serve static files" /> } title="Data Aggregation" content="Groups & sums" /> } title="Hosting" content="Let codehooks serve your frontend" />
:::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, ]} />
} title="Auto-generate OpenAPI documentation" content={
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.
{`import { app } from 'codehooks-js'; import { z } from 'zod'; const todoSchema = z.object({ title: z.string().min(1).max(200), completed: z.boolean().default(false) }); // Enable OpenAPI docs at /docs app.openapi({ info: { title: 'Todo API', version: '1.0.0' } }); app.crudlify({ todos: todoSchema }); export default app.init();`}
After deploying, visit /docs for interactive Swagger UI with full authentication support, or /openapi.json for the raw specification.
} nextsteps={[ OpenAPI documentation guide, Document custom routes, Schema library support, ]} />
## NoSQL and Key-Value Datastores
} 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.

{codex5}
} nextsteps={[ Job API, Crontab guru - interactive CRON expression builder , ]} />
## Queues
} 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.
{codex6}
} nextsteps={[Queue API]} />
## Reliable Workflows
} title="Build reliable stateful workflows" content={
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:
$ coho file-upload --projectname 'myproject-fafc' --space 'dev' --src '/somedir/' --target '/myblobs'
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. --- ## Data aggregation API *(Note: This content contains MDX/JSX code)* The JSON aggregation specification is designed to provide a structured way to aggregate JSON data streams. It offers several operators to aid in the summarization and grouping of data streams without the need for extensive code. **Aggregation Operators** Our specification currently supports the following operators: - `$max`: Determines the maximum value for the specified field(s). - `$min`: Determines the minimum value for the specified field(s). - `$avg`: Computes the average value for the specified field(s). - `$sum`: Sums up the values for the specified field(s). - `$group`: Group by specified field and perform nested aggregations. Each operator can take either a string denoting the field name or an object with additional properties such as $field (for specifying the field name) and $label (for specifying a custom label for the result). ## Aggregation data from a NoSQL query JSON data `aggregation` is a standard library that is imported in your Codehooks app. The example below shows how to use the library agains a `salesData` NoSQL data collection. ```js {5-8,12} import { app, datastore, aggregation } from 'codehooks-js'; app.post('/agg', async (req, res) => { try { const spec = { $max: 'sales', $avg: 'sales', }; const db = await datastore.open(); const dbstream = db.getMany('salesData', { active: true }); const result = await aggregation(dbstream, spec); console.log(result); res.json(result); } catch (error) { console.log(error.message); res.status(400).end(error); } }); export default app.init(); // Bind functions to the serverless runtime ``` **Example output** ```json { "count": 120500, "sales": { "max": 1234.0, "avg": 123.0, "sum_total": 123456.0 } } ``` ## Operators ### `$max` - **Description:** Determines the maximum value for the specified field(s). - **Usage:** - `$max: fieldName` for a simple field. - `$max: { $field: fieldName, $label: customLabel }` for specifying a custom label. - **Example:** ```json { "$max": { "$field": "sales", "$label": "Max Sales" } } ``` ### `$min` - **Description:** Determines the minimum value for the specified field(s). - **Usage:** - `$min: fieldName` for a simple field. - `$min: { $field: fieldName, $label: customLabel }` for specifying a custom label. - **Example:** ```json { "$min": { "$field": "sales", "$label": "Min Sales" } } ``` ### `$avg` - **Description:** Computes the average value for the specified field(s). - **Usage:** - `$avg: fieldName` for a simple field. - `$avg: { $field: fieldName, $label: customLabel }` for specifying a custom label. - **Example:** ```json { "$avg": { "$field": "sales", "$label": "Average Sales" } } ``` ### `$sum` - **Description:** Sums up the values for the specified field(s). - **Usage:** - `$sum: fieldName` for a simple field. - `$sum: { $field: fieldName, $label: customLabel }` for specifying a custom label. - **Example:** ```json { "$sum": { "$field": "sales", "$label": "Total Sales" } } ``` ### `$group` - **Description:** Group by specified field and perform nested aggregations. - **Usage:** - `$group: { $field: fieldName, ...nestedAggregations }` to group by a field and apply further aggregations. - **Example:** ```json { "$group": { "$field": "customerType", "$label": "Customer Segment", "$max": "sales" } } ``` #### Example: Complex grouping and aggregation ```js const spec = { $max: { $field: 'sales', $label: 'All time top sales' }, $group: [ { $field: 'customerType', $label: 'Segment', $max: 'sales', $min: { $field: 'sales', $label: 'Low' }, $group: { $field: 'year', $group: { $field: 'month', $max: ['sales', { $field: 'volume', $label: 'Sales volume' }], }, }, }, { $field: 'day', $max: 'sales', }, ], }; ``` Our JSON aggregation specification provides a robust tool for developers seeking powerful and streamlined data processing capabilities. Harnessing these operators allows for comprehensive insights into your data streams. --- ## AI Agent Setup > A comprehensive guide for setting up AI coding agents to build Codehooks.io backend APIs. Covers the Claude Code plugin, CLI prompt, MCP server, OpenClaw skill, and manual setup for all major LLM agents. ## What is Codehooks.io? Codehooks.io is the agent-native backend platform. It's 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. What makes it different is that 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. Your agent handles the rest. ## Getting Started Choose the setup method that matches your AI coding tool: - **[Claude Code Plugin](#claude-code-plugin-recommended)** — Recommended for Claude Code users. Auto-detection, templates, and full API context. - **[CLI Prompt](#agents-with-terminal-access)** — For any agent with terminal access (Codex CLI, Cursor, Aider, Windsurf). - **[MCP Server](#mcp-compatible-tools)** — Fallback for tools without terminal access (Claude Desktop, Cline, Zed). Prefer CLI when available. ## Setup for AI Coding Agents ### Claude Code Plugin (Recommended) **Best for:** Claude Code users who want the fastest, most integrated experience The [Codehooks Claude Plugin](https://github.com/RestDB/codehooks-claude-plugin) gives Claude Code auto-detection of Codehooks projects, built-in templates, and a `/codehooks:backend` command for scaffolding production-ready backends from natural language. **What it provides:** - **Auto-detection** — Claude Code automatically recognizes Codehooks projects and loads platform context - **CLI integration** — Deploy, query, and manage your backend directly from the conversation - **Templates** — Pre-built patterns for webhooks, CRUD APIs, cron jobs, queues, and workflows - **`/codehooks:backend` command** — Activate full backend development mode with a single slash command **Installation:** ``` /plugin marketplace add RestDB/codehooks-claude-plugin /plugin install codehooks@codehooks ``` Once installed, open any Codehooks project and use `/codehooks:backend` to start building. ### Agents with Terminal Access **Best for:** Codex CLI, Cursor, Aider, Windsurf, and other agents that can run shell commands If your AI agent has terminal access, the Codehooks CLI provides the fastest setup with the `prompt` command: ```bash # Install the CLI npm install -g codehooks # Display the prompt - pipe it to your agent or copy to clipboard coho prompt # Example: Copy to clipboard on macOS coho prompt | pbcopy ``` The CLI also gives your agent direct access to deployment, database operations, logs, and more. See the [CLI documentation](/docs/cli) for all available commands. ### MCP-Compatible Tools **Best for:** Claude Desktop, Cline, Zed, and other tools that support the Model Context Protocol but lack terminal access For tools that support MCP, the Codehooks MCP server provides deploy, query, and management capabilities: https://github.com/RestDB/codehooks-mcp-server **Note:** If your agent can run shell commands, always prefer the CLI over MCP — it's faster, more capable, and gives the agent direct access to the full `coho` toolchain. ## Agent Skills & Templates For AI agents that support skills or pre-built tool integrations, you can use the **Codehooks OpenClaw Skill** to get started even faster. The [codehooks-openclaw-skill](https://github.com/RestDB/codehooks-openclaw-skill) provides pre-built patterns for common backend tasks: - **Webhook handlers** with signature verification (Stripe, GitHub, etc.) - **CRUD REST APIs** with validation - **Cron jobs** for scheduled background work - **Worker queues** for async processing - **Multi-step workflows** with state management Install the skill in your OpenClaw agent and it can scaffold production-ready Codehooks backends from natural language descriptions. Read the [full walkthrough in the blog post](/blog/openclaw-backend) to see how OpenClaw agents use Codehooks as their serverless backend. ## Development Prompt The development prompt gives your AI agent full context on the Codehooks.io platform — API patterns, database operations, authentication, workers, cron, and more. **Access the prompt:** - **CLI:** `coho prompt` (pipe to your agent or copy to clipboard with `coho prompt | pbcopy`) - **Full docs:** [llms.txt](https://codehooks.io/llms.txt) — complete Codehooks documentation optimized for AI agents - **Source:** [codehooks-mcp-server](https://github.com/RestDB/codehooks-mcp-server) — the canonical prompt is maintained here ## Codehooks CLI for AI Agents The Codehooks CLI (`coho`) is designed for AI agents working with command-line tools. Key commands for AI development: | Command | Description | |---------|-------------| | `coho prompt` | Display the development prompt for AI assistants | | `coho docs` | Open the API cheatsheet | | `coho deploy` | Deploy code to the serverless cloud | | `coho log -f` | Stream logs in real-time | | `coho query ` | Query database collections | | `coho info --examples` | Show project info with cURL examples | | `coho doctor` | Comprehensive project diagnostics as JSON — collections, stats, deploys, and recent error logs. Useful for agents to diagnose issues. | | `coho describe` | Describe the deployed app structure — collections, schemas, queues, and files. Helps agents understand what's already deployed. | Full CLI documentation: https://codehooks.io/docs/cli For additional detailed information about the Codehooks.io platform, you can reference https://codehooks.io/llms.txt --- ## AWS Lambda Alternative for AI Agents, Webhooks & APIs (2026) *(Note: This content contains MDX/JSX code)* import FAQSection from '@site/src/components/FAQSection'; # AWS Lambda Alternative for AI Agents, Webhooks & APIs ## Why Developers Look for AWS Lambda Alternatives ![AWS Lambda vs Codehooks comparison](./img/aws-lambda-vs-codehooks.webp) AWS Lambda is powerful, but building webhook handlers and APIs requires assembling multiple services. Many developers search for AWS Lambda alternatives when they face: - **Complex service orchestration** — Lambda alone isn't enough; you need API Gateway + DynamoDB + SQS + IAM roles - **Steep learning curve** — CloudFormation, SAM, or Terraform just to deploy a simple endpoint - **Cold start latency** — Webhook providers may timeout waiting for your function to wake up - **Unpredictable costs** — Multiple billing dimensions across Lambda, API Gateway, DynamoDB, and data transfer - **No built-in database** — Every project requires setting up DynamoDB or RDS separately ## Codehooks: All-in-One Serverless Backend Codehooks is purpose-built for quick APIs, webhooks, and backend automation — delivering what takes 5+ AWS services in a single platform. ### Key Differences | Feature | AWS Lambda | Codehooks | |---------|------------|-----------| | **Setup Complexity** | Lambda + API Gateway + DynamoDB + SQS + IAM | Single platform, deploy in seconds | | **Deployment Time** | Minutes (SAM/CloudFormation) | ~5 seconds (`coho deploy`) | | **Document Database** | Separate service (DynamoDB/RDS) | Built-in NoSQL database | | **Key-Value Store** | Requires ElastiCache/DynamoDB | Built-in with TTL support | | **Queue System** | Requires SQS setup | Built-in workers and queues | | **Cron Jobs** | Requires EventBridge/CloudWatch | Built-in with simple syntax | | **Cold Starts** | 100ms-several seconds | No cold starts | | **Pricing Model** | Pay per invocation × 6 services | Flat monthly rate | | **Vendor Lock-in** | Deep AWS ecosystem (SDK, IAM, CloudFormation) | Light (codehooks-js, but standard Node.js) | ## The AWS Lambda Setup Problem To build a webhook handler with AWS Lambda, you need to configure and connect multiple services: ```mermaid flowchart TB subgraph AWS[" "] direction TB WH1[Webhook] --> AG[API Gateway] AG --> L[Lambda] L --> DB[(DynamoDB)] AG --> IAM[IAM Roles] L --> CW[CloudWatch Logs] L --> SQS[SQS Queue] L --> EB[EventBridge] end ``` **AWS Architecture (6+ services)** ```mermaid flowchart TB subgraph CH[" "] direction TB WH2[Webhook] --> CO[Codehooks
API + DB + Queue + Cron] end ``` **Codehooks Architecture (1 platform)** ## Pricing Comparison: AWS Lambda vs Codehooks ### AWS Lambda Pricing (Multiple Services) Building a webhook handler on AWS involves costs across multiple services (US East region, as of 2026): - **Lambda**: $0.20 per 1M requests + $0.0000166667/GB-second compute - **API Gateway**: $1.00 per million HTTP API requests - **DynamoDB**: $1.25 per million write requests, $0.25 per million reads - **SQS**: $0.40 per million requests - **Data Transfer**: $0.09/GB after 100GB free - **CloudWatch Logs**: $0.50/GB ingested *Prices vary by region. Check [AWS Pricing](https://aws.amazon.com/pricing/) for current rates.* **Example scenario**: A Stripe webhook handler processing 100K events/month with database writes: - Lambda: ~$2-5 - API Gateway: ~$0.10 - DynamoDB: ~$5-15 (depending on read/write patterns) - CloudWatch: ~$1-3 - **Total: $10-25/month** (and significant setup time) ### Codehooks Pricing (All-Inclusive) | Plan | Price | API Calls | Database | Compute | |------|-------|-----------|----------|---------| | Development | Free | 60/min | 150 MB | Included | | Pro | $19/mo | 3,600/min | 15 GB | **Unlimited** | | Team | $99/mo | 6,000/min | 25 GB | **Unlimited** | **Same scenario**: 100K webhook events/month = **$19/mo flat** on Pro plan. Database, queues, and cron jobs included. ## Code Comparison: Stripe Webhook Handler ### AWS Lambda Implementation First, create your Lambda function: ```javascript // index.mjs - AWS Lambda function import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb"; import Stripe from "stripe"; const client = new DynamoDBClient({}); const dynamo = DynamoDBDocumentClient.from(client); const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); export const handler = async (event) => { const sig = event.headers["stripe-signature"]; let stripeEvent; try { // Note: For API Gateway, you may need to handle base64 encoding const body = event.isBase64Encoded ? Buffer.from(event.body, "base64").toString("utf8") : event.body; stripeEvent = stripe.webhooks.constructEvent( body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return { statusCode: 400, body: JSON.stringify({ error: `Webhook Error: ${err.message}` }), }; } if (stripeEvent.type === "payment_intent.succeeded") { const paymentIntent = stripeEvent.data.object; await dynamo.send( new PutCommand({ TableName: process.env.DYNAMODB_TABLE, Item: { id: paymentIntent.id, amount: paymentIntent.amount, status: "succeeded", createdAt: new Date().toISOString(), }, }) ); } return { statusCode: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ received: true }), }; }; ``` Then, set up infrastructure (SAM template): ```yaml # template.yaml - AWS SAM AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Resources: StripeWebhookFunction: Type: AWS::Serverless::Function Properties: Handler: index.handler Runtime: nodejs20.x Timeout: 30 Environment: Variables: STRIPE_SECRET_KEY: !Ref StripeSecretKey STRIPE_WEBHOOK_SECRET: !Ref StripeWebhookSecret DYNAMODB_TABLE: !Ref PaymentsTable Policies: - DynamoDBCrudPolicy: TableName: !Ref PaymentsTable Events: WebhookApi: Type: HttpApi Properties: Path: /stripe-webhook Method: POST PaymentsTable: Type: AWS::DynamoDB::Table Properties: TableName: payments AttributeDefinitions: - AttributeName: id AttributeType: S KeySchema: - AttributeName: id KeyType: HASH BillingMode: PAY_PER_REQUEST Parameters: StripeSecretKey: Type: String NoEcho: true StripeWebhookSecret: Type: String NoEcho: true ``` Deployment: ```bash sam build sam deploy --guided # Takes 2-5 minutes ``` ### Codehooks Implementation ```javascript // index.js - Codehooks import { app, Datastore } from 'codehooks-js'; import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // Bypass JWT auth for webhook endpoint app.auth('/stripe-webhook', (req, res, next) => next()); app.post('/stripe-webhook', async (req, res) => { const sig = req.headers['stripe-signature']; try { const event = stripe.webhooks.constructEvent( req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET ); if (event.type === 'payment_intent.succeeded') { const paymentIntent = event.data.object; const conn = await Datastore.open(); await conn.insertOne('payments', { stripeId: paymentIntent.id, amount: paymentIntent.amount, status: 'succeeded', createdAt: new Date().toISOString(), }); } res.json({ received: true }); } catch (err) { res.status(400).json({ error: `Webhook Error: ${err.message}` }); } }); export default app.init(); ``` Deployment: ```bash coho deploy # Takes ~5 seconds ``` **Your endpoint is live**: `https://yourproject-xxxx.api.codehooks.io/stripe-webhook` ## Adding Background Processing ### AWS Lambda + SQS ```javascript // Producer function import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs"; const sqs = new SQSClient({}); export const handler = async (event) => { await sqs.send( new SendMessageCommand({ QueueUrl: process.env.QUEUE_URL, MessageBody: JSON.stringify({ orderId: "123", action: "process" }), }) ); return { statusCode: 200, body: "Queued" }; }; ``` Plus additional SAM configuration for the queue and consumer Lambda... ### Codehooks Built-in Queues ```javascript import { app, Datastore } from 'codehooks-js'; // Define the worker app.worker('processOrder', async (req, res) => { const { orderId, action } = req.body.payload; console.log(`Processing order ${orderId}: ${action}`); // Do the work... res.end(); }); // Enqueue from anywhere app.post('/orders', async (req, res) => { const conn = await Datastore.open(); await conn.enqueue('processOrder', { orderId: '123', action: 'process' }); res.json({ status: 'queued' }); }); export default app.init(); ``` No additional infrastructure. Just works. ## Adding Scheduled Jobs ### AWS Lambda + EventBridge Requires CloudFormation/SAM configuration: ```yaml # Add to template.yaml DailyCleanupFunction: Type: AWS::Serverless::Function Properties: Handler: cleanup.handler Runtime: nodejs20.x Events: ScheduleEvent: Type: Schedule Properties: Schedule: cron(0 0 * * ? *) # Daily at midnight ``` ### Codehooks Built-in Jobs ```javascript import { app, Datastore } from 'codehooks-js'; // Daily cleanup job - runs at midnight app.job('0 0 * * *', async (req, res) => { const conn = await Datastore.open(); const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); await conn.removeMany('logs', { createdAt: { $lt: thirtyDaysAgo.toISOString() } }); console.log('Cleanup completed'); res.end(); }); export default app.init(); ``` One line. No extra services. ## When to Choose Codehooks Over AWS Lambda ### Choose Codehooks When You Need: **Webhook & API Projects** - Stripe, Shopify, GitHub webhook handlers - Third-party API integrations - Backend APIs for web/mobile apps - Bot backends (Slack, Discord) **Rapid Development** - Deploy in seconds, not minutes - No infrastructure configuration - Built-in database and queues - CLI-first workflow **Predictable Costs** - Flat monthly pricing - No multi-service billing complexity - No surprise data transfer charges **Simplicity** - Standard JavaScript/Node.js - No AWS-specific SDKs to learn - No IAM role configuration - No CloudFormation/SAM/Terraform **Agent-Native Development** - **CLI-first** — every operation is a terminal command (`coho create`, `coho deploy`, `coho log -f`) - **90 seconds prompt to production** — your agent scaffolds, deploys, and verifies in one loop - **Agent context tools** — `coho prompt` generates platform context, `coho describe` summarizes your project - **Agent diagnostics** — `coho doctor` checks project health - [Claude Code plugin](https://github.com/RestDB/codehooks-claude-plugin) for auto-detection, templates, and full API context - [MCP server](https://github.com/RestDB/codehooks-mcp-server) for direct agent integration - [LLM-optimized documentation](https://codehooks.io/llms.txt) for accurate code generation - **Flat-rate pricing** — your agent can deploy 50 iterations while debugging, same $19/month Compare this to AWS: an agent deploying to Lambda needs to orchestrate API Gateway, DynamoDB, IAM roles, and CloudFormation — multiple services that require complex configuration your agent has to understand and manage. ### Keep Using AWS Lambda When You Need: - **Deep AWS ecosystem integration** (S3 triggers, Kinesis, Step Functions) - **Multi-region deployment** with AWS Global Accelerator - **VPC integration** with private AWS resources - **Existing AWS infrastructure** and IAM setup - **GPU workloads** or custom runtimes - **Enterprise compliance** requirements tied to AWS ## Migration from AWS Lambda to Codehooks ### Step 1: Create Codehooks Project ```bash npm install -g codehooks coho create myproject cd myproject ``` ### Step 2: Convert Lambda Handler | AWS Lambda Pattern | Codehooks Equivalent | |--------------------|----------------------| | `export const handler = async (event) => {}` | `app.post('/path', async (req, res) => {})` | | `event.body` | `req.body` (auto-parsed) | | `event.headers` | `req.headers` | | `event.pathParameters.id` | `req.params.id` | | `event.queryStringParameters` | `req.query` | | `return { statusCode, body }` | `res.status(code).json(data)` | | DynamoDB PutCommand | `conn.insertOne(collection, doc)` | | DynamoDB GetCommand | `conn.getOne(collection, id)` | | SQS SendMessage | `conn.enqueue(queue, payload)` | ### Step 3: Set Environment Variables ```bash coho set-env STRIPE_SECRET_KEY sk_live_xxx coho set-env STRIPE_WEBHOOK_SECRET whsec_xxx ``` ### Step 4: Deploy ```bash coho deploy ``` Your endpoint is live. Update your webhook provider with the new URL. ## Conclusion **Choose Codehooks over AWS Lambda when:** - You're building webhooks, APIs, or automation workflows - You want to deploy in seconds, not minutes - You need built-in database, queues, and cron jobs - You prefer flat, predictable pricing - You don't want to manage 5+ AWS services - Your AI agent needs a CLI-first backend it can deploy to with a single command **Keep AWS Lambda when:** - You're deeply invested in the AWS ecosystem - You need S3 triggers, Step Functions, or Kinesis - You have enterprise compliance tied to AWS - You need custom runtimes or GPU workloads For most webhook and API projects, Codehooks delivers what Lambda promises — serverless backend development — without the infrastructure complexity. --- > **Ready to simplify your serverless backend?** Deploy your first webhook handler in under a minute: > ```bash > npm install -g codehooks > coho create myproject > coho deploy > ``` --- ## Firebase Alternative for AI Agents: Webhooks & Automations (2026) *(Note: This content contains MDX/JSX code)* import FAQSection from '@site/src/components/FAQSection'; # Firebase Alternative for AI Agents: Webhooks & Automations ## Why Developers Look for Firebase Alternatives ![Firebase vs Codehooks comparison](./img/firebase-vs-codehooks.webp) Firebase is a powerful platform, but it's not optimized for every use case. Many developers search for Firebase alternatives when they need: - **Webhook-first architecture** — Firebase requires Cloud Functions setup for webhooks - **Predictable pricing** — Firebase's pay-per-invocation model creates unpredictable bills - **Faster deployment** — Firebase deployments can take 1-2 minutes - **Simpler architecture** — Firebase requires combining multiple services (Functions + Firestore + Auth) - **CLI-first workflows for AI agents** — Firebase's dashboard-heavy setup creates friction for agents that operate through the terminal ## Codehooks: Built for Webhooks, Automations & AI Agents Codehooks is an agent-native backend platform designed for quick APIs, webhook handling, and automation workflows — areas where Firebase requires significant setup. ### Key Differences | Feature | Firebase | Codehooks | |---------|----------|-----------| | **Deployment Time** | 1-2 minutes | ~5 seconds | | **Webhook Handling** | Requires Cloud Functions setup | Built-in, production-ready templates | | **Pricing Model** | Pay-per-invocation + egress + storage | Flat-rate with unlimited compute | | **Database** | Firestore (document) or Realtime DB | NoSQL + Key-Value store built-in | | **Queue/Workers** | Requires Pub/Sub or Cloud Tasks | Built-in queue and worker system | | **Cron Jobs** | Requires Cloud Scheduler | Built-in with simple syntax | | **Cold Starts** | Common on free tier | No cold starts | | **AI Agent Support** | MCP | MCP, Claude Code plugin, CLI-first | | **Primary Interface** | Web dashboard | CLI (terminal-native) | ## Pricing Comparison: Firebase vs Codehooks ### Firebase Pricing (Pay-as-you-go) Firebase's pricing can be unpredictable due to multiple billing dimensions: - **Cloud Functions**: $0.40 per million invocations + compute time + memory - **Firestore**: $0.18 per 100K reads, $0.18 per 100K writes, $0.02 per 100K deletes - **Bandwidth**: $0.12 per GB after 10GB free - **Storage**: $0.026 per GB/month **Example scenario**: A webhook handler processing 100K events/month with database writes could cost $50-200+ depending on compute time and data volume. And every iteration your agent deploys adds to the bill. ### Codehooks Pricing (Flat-rate) | Plan | Price | API Calls | Database | Compute | |------|-------|-----------|----------|---------| | Development | Free | 60/min | 150 MB | Included | | Pro | $19/mo | 3,600/min | 15 GB | **Unlimited** | | Team | $99/mo | 6,000/min | 25 GB | **Unlimited** | **Same scenario**: 100K webhook events/month = $19/mo flat on Pro plan. No surprises. Your agent can deploy 50 iterations while debugging — same price. ## When to Choose Codehooks Over Firebase ### Choose Codehooks When You Need: **Webhook & Integration Projects** - Stripe payment webhooks - Shopify order processing - GitHub CI/CD webhooks - Slack/Discord bot backends - Third-party API integrations **Automation & Background Jobs** - Scheduled tasks and cron jobs - Queue-based processing - Data synchronization workflows - Event-driven pipelines **Agent-Native Development** - CLI-first — your agent deploys with `coho deploy`, no dashboard needed - Sub-5-second deploys for rapid iteration - `coho prompt` generates platform context for any AI agent - [Claude Code plugin](https://github.com/RestDB/codehooks-claude-plugin) for auto-detection and templates - [MCP server](https://github.com/RestDB/codehooks-mcp-server) for direct agent integration **Predictable Costs** - Flat monthly pricing - No per-invocation charges - No egress fees on compute - No surprise bills — your agent can iterate aggressively at the same price ### Keep Using Firebase When You Need: - **Firebase Authentication** with deep mobile SDK integration - **Firestore's offline sync** for mobile apps - **Firebase Hosting** for static sites with CDN - **Firebase Analytics** and Crashlytics integration - **Google Cloud ecosystem** integration ## Code Comparison: Stripe Webhook Handler ### Firebase Implementation ```javascript // Firebase Cloud Functions const functions = require('firebase-functions'); const admin = require('firebase-admin'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); admin.initializeApp(); exports.stripeWebhook = functions.https.onRequest(async (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent( req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return res.status(400).send(`Webhook Error: ${err.message}`); } if (event.type === 'payment_intent.succeeded') { const paymentIntent = event.data.object; await admin.firestore().collection('payments').add({ stripeId: paymentIntent.id, amount: paymentIntent.amount, status: 'succeeded', createdAt: admin.firestore.FieldValue.serverTimestamp() }); } res.json({ received: true }); }); ``` Deployment: `firebase deploy --only functions` (1-2 minutes) ### Codehooks Implementation ```javascript // Codehooks import { app, Datastore } from 'codehooks-js'; import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); app.post('/stripe-webhook', async (req, res) => { const sig = req.headers['stripe-signature']; const event = stripe.webhooks.constructEvent( req.rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET ); if (event.type === 'payment_intent.succeeded') { const conn = await Datastore.open(); await conn.insertOne('payments', { stripeId: event.data.object.id, amount: event.data.object.amount, status: 'succeeded', createdAt: new Date().toISOString() }); } res.json({ received: true }); }); export default app.init(); ``` Deployment: `coho deploy` (~5 seconds). An AI agent can scaffold this, deploy it, test it, and iterate — all from the terminal. **Or use the template**: `coho create --template stripe-webhook-handler && coho deploy` ## Migration from Firebase to Codehooks ### Step 1: Export Your Data ```bash # Export Firestore collection to JSON firebase firestore:export ./backup ``` ### Step 2: Create Codehooks Project ```bash npm install -g codehooks coho create myproject cd myproject ``` ### Step 3: Import Data ```bash coho import --collection payments --file ./payments.json ``` ### Step 4: Migrate Cloud Functions Convert your Firebase Cloud Functions to Codehooks route handlers. The API is similar: | Firebase | Codehooks | |----------|-----------| | `functions.https.onRequest` | `app.post('/path', handler)` | | `admin.firestore().collection()` | `Datastore.open()` then `conn.insertOne()` | | `functions.pubsub.schedule()` | `app.job('cron', handler)` | | Cloud Tasks | `app.queue('name', handler)` | ### Step 5: Deploy ```bash coho deploy ``` Your webhook endpoint is live at `https://yourproject-xxxx.api.codehooks.io/stripe-webhook` ## Conclusion **Choose Codehooks over Firebase when:** - You need quick APIs with built-in database and auth - You're building webhook handlers and integrations - You need predictable, flat-rate pricing - Fast deployment (seconds vs minutes) matters - You want built-in queues, workers, and cron jobs - Your AI agent needs a CLI-first backend it can deploy to autonomously - You prefer terminal-native development over dashboard configuration **Keep Firebase when:** - You're building mobile apps with offline sync - You need Firebase Auth's mobile SDKs - You're deep in the Google Cloud ecosystem - You need Firebase Analytics/Crashlytics Many teams use both: Firebase for mobile/auth, Codehooks for webhooks and backend automation. --- > **Ready to try Codehooks?** Deploy your first webhook handler in under a minute: > ```bash > npm install -g codehooks > coho create --template stripe-webhook-handler > coho deploy > ``` --- ## Supabase vs Codehooks for Agent Development: Technical Comparison (2026) *(Note: This content contains MDX/JSX code)* import FAQSection from '@site/src/components/FAQSection'; # Supabase vs Codehooks for Agent Development: Technical Comparison 2026 > **Looking for pricing information?** Check out our [Supabase Pricing vs Codehooks guide](./supabase-pricing-comparison) for cost analysis and quick decision-making. ## Development Experience Comparison ### Codehooks: Agent-Native Development Platform - **CLI-First Workflow**: Every operation is a terminal command — `coho create`, `coho deploy`, `coho log -f` - **Sub-5-Second Deploys**: No build steps, no container images. Just `coho deploy` and it's live - **Agent Context Tools**: `coho prompt` generates platform context for any AI agent; `coho describe` summarizes your project - **Agent Diagnostics**: `coho doctor` checks project health; `coho log -f` streams logs in real time - **Workflow Support**: Built-in automation and integration workflow API - **Simple Data Management**: NoSQL-focused tools for quick data operations - **Claude Code Plugin**: Auto-detection, templates, and full API context for Claude Code users - **MCP Server**: Direct agent integration for database operations, code deployment, and file management - **Minimal Configuration**: Zero provisioning, zero wiring. One project, one deploy, everything works ### Supabase: Comprehensive Application Development Platform - **Rich Dashboard**: Comprehensive web interface for database management - **SQL Editor**: Advanced online query editor with syntax highlighting - **Table Editor**: Visual interface for managing complex data structures - **Database Schema Tools**: Visualization and relationship management - **Authentication Dashboard**: Complete user and role management - **Real-time Inspector**: Monitor subscriptions and database changes - **API Documentation**: Auto-generated, comprehensive API docs - **Enterprise Tooling**: Advanced monitoring and analytics ## Agent Development Workflow This is where the two platforms diverge most sharply. Codehooks is designed for the agent deployment loop — the rapid cycle of prompt, scaffold, deploy, inspect, iterate that AI agents excel at. ### The Codehooks Agent Loop ``` 1. Agent prompts → coho prompt (generates platform context) 2. Agent scaffolds → coho create myproject (project ready) 3. Agent codes → writes index.js with codehooks-js 4. Agent deploys → coho deploy (live in ~5 seconds) 5. Agent inspects → coho log -f (stream logs) 6. Agent iterates → edit code → coho deploy (5 seconds again) ``` **Full loop: ~90 seconds from prompt to production.** Your agent can deploy 50 iterations in the time it takes other platforms to deploy once. ### Key CLI Commands for Agents | Command | What it does | |---------|-------------| | `coho create` | Scaffold a new project | | `coho deploy` | Deploy in ~5 seconds | | `coho prompt` | Generate platform context for any AI agent | | `coho describe` | Summarize project structure and endpoints | | `coho doctor` | Diagnose project health and configuration | | `coho log -f` | Stream logs in real time | | `coho set-env` | Set environment variables | | `coho info` | Show project metadata | ### The Supabase Workflow Supabase's primary interface is the web dashboard. While Supabase has a CLI, the core workflow involves: 1. Create project via dashboard 2. Design schema in the Table Editor 3. Configure Row Level Security policies in the dashboard 4. Write Edge Functions locally 5. Deploy via `supabase functions deploy` (longer deployment cycle) 6. Check logs in the dashboard This GUI-first approach works well for human developers who prefer visual tools, but creates friction for AI agents that operate through terminal commands. ## Feature Comparison ### Codehooks Strengths - **Agent-Native Platform**: CLI-first design built for AI agent workflows - **Sub-5-Second Deploys**: Instant deployment for rapid iteration — critical for agent development loops - **Workflow Automation**: Built-in workflow support for integration tasks - **Simplified Development**: Less code needed for automation and integration scenarios - **Unified NoSQL Database**: Easy access to both document and key-value databases - **Integration-First Design**: - Native serverless functions for APIs and webhooks - Job scheduling & worker queues for background tasks - Automatic CRUD for rapid prototyping - **Agent CLI Tools**: `coho prompt`, `coho doctor`, `coho describe` for agent context and diagnostics - **Simple Authentication**: Quick auth setup for integration scenarios - **Real-time Capabilities**: Server-Sent Events (SSE) for lightweight real-time features - **Predictable Costs**: **All compute costs included** — no surprise bills or usage-based compute charges ### Supabase Strengths - **Comprehensive Application Platform**: Full-featured dashboard and tooling - **PostgreSQL Database**: Enterprise-grade SQL database with advanced features - **Advanced Online Tools**: - Rich dashboard for database management - SQL editor with syntax highlighting and query optimization - Visual table editor for complex data relationships - Real-time inspector for monitoring subscriptions - **Enterprise Security**: Row Level Security (RLS) and advanced compliance features - **Open Source**: Full access to source code and self-hosting options - **Real-time Subscriptions**: WebSocket-based real-time with advanced filtering - **Extensive Documentation**: Large community and comprehensive resources - **Database Extensions**: Rich ecosystem of PostgreSQL extensions ## AI Integration & Agent Tooling ### Codehooks: Built for Agents Codehooks provides multiple integration points for AI agents: **Claude Code Plugin** ([GitHub](https://github.com/RestDB/codehooks-claude-plugin)) - Auto-detects Codehooks projects - Provides templates and full API context - Enables end-to-end development without leaving the terminal **MCP Server** ([GitHub](https://github.com/RestDB/codehooks-mcp-server)) - Database operations: Query, update, and manage collections - Code deployment: Deploy JavaScript serverless functions - File management: Upload, browse, and manage cloud storage - Key-value store: Real-time data storage with TTL support - System operations: Access logs and documentation **CLI Context Commands** - `coho prompt` — generates platform context for any AI agent - `coho describe` — summarizes your project structure and endpoints - [llms.txt](https://codehooks.io/llms.txt) — LLM-optimized documentation **What agents build with Codehooks:** - Stripe webhook handlers with queue-based processing - Scheduled automations (inventory sync, digest emails, data cleanup) - Multi-step workflows (Slack alerts after Shopify orders) - Data ingestion pipelines for IoT and real-time events - Full CRUD APIs with authentication ### Supabase: MCP Integration Supabase supports MCP connections, allowing AI agents to interact with PostgreSQL databases, authentication systems, and storage services through the Model Context Protocol. The primary development experience remains dashboard-oriented, with the CLI as a secondary interface. ## Use Cases ### Codehooks is Ideal For: - **Quick API Development**: REST endpoints with built-in database, auth, and automatic CRUD — deploy in seconds - **Agent-Built Backend Projects**: AI agents that need to autonomously create, deploy, and iterate on backends - **Automation & Integration Projects**: Workflow automation, data pipelines, API integrations - **Microservices & APIs**: Lightweight backend services and webhook processing - **Rapid Prototyping**: 90-second prompt-to-production loop for quick MVP development - **Background Processing**: Job scheduling, worker queues, and automated tasks - **Simple CRUD Applications**: Quick database operations without complex relationships - **CLI-Driven Workflows**: Teams and agents that prefer terminal-first development ### Supabase is Ideal For: - **Full-Stack Applications**: Comprehensive web and mobile applications - **SaaS Platforms**: Multi-tenant applications with complex user management - **Enterprise Applications**: Applications requiring advanced security and compliance - **Data-Intensive Applications**: Complex queries, relationships, and analytics - **Team Collaboration Apps**: Applications requiring advanced real-time features - **Open Source Projects**: Teams needing full control and customization - **Traditional Web Applications**: Applications benefiting from SQL database patterns ## Key Differentiators ### Codehooks 1. **Agent-Native Platform**: CLI-first design with `coho prompt`, `coho doctor`, Claude Code plugin, and MCP server 2. **Sub-5-Second Deploys**: 90-second prompt-to-production loop — your agent can deploy 50 iterations while other platforms deploy once 3. **Zero Config**: No provisioning, no wiring, no YAML. One project, one deploy, everything works 4. **Workflow Automation**: Built-in support for integration workflows and background processing 5. **Predictable Costs**: **All-inclusive pricing with unlimited compute included** — flat-rate means your agent can iterate aggressively ### Supabase 1. **Comprehensive Platform**: Full-featured application development environment with rich dashboard 2. **Advanced Database Tools**: Rich SQL editor, schema visualization, and database management 3. **Enterprise Features**: Row-level security, advanced compliance, and audit capabilities 4. **Open Source Flexibility**: Self-hosting options and full source code access 5. **Community Ecosystem**: Large community, extensive documentation, and third-party integrations ## Migration Considerations ### Migrating to Codehooks - Simple migration path for NoSQL-based applications - Quick setup process — your agent can run `coho create` and start migrating immediately - Minimal configuration required - Built-in tools for data management - `coho import` for bulk data loading ### Migrating to Supabase - More complex migration for SQL-based applications - Requires database schema planning - More configuration options - Extensive documentation available --- ## Conclusion Both platforms are strong in their respective domains: **Codehooks** is the **agent-native** choice for teams and AI agents prioritizing speed, simplicity, and rapid iteration. CLI-first design, sub-5-second deploys, and agent tooling (`coho prompt`, `coho doctor`, Claude Code plugin, MCP server) make it the natural platform for agent-driven development. It's particularly strong for integration projects, APIs, and automation workflows where quick deployment and zero configuration are key. **Supabase** provides a comprehensive solution for traditional application development, offering enterprise-grade features, SQL power, a rich dashboard, and extensive customization options. The choice depends on your workflow: if your AI agent or team operates through the terminal and values rapid iteration, choose Codehooks. If you prefer visual tools and need enterprise SQL features, choose Supabase. Both offer clear upgrade paths as your needs evolve. > **Need help with pricing decisions?** Check our [Supabase Pricing Comparison](./supabase-pricing-comparison) for detailed cost analysis and business considerations. --- ## Supabase vs Codehooks for AI Agents: Pricing Comparison (2026) *(Note: This content contains MDX/JSX code)* # Supabase vs Codehooks for AI Agents: Pricing Comparison 2026 ## Why Compare Supabase and Codehooks? AI agents need a backend they can autonomously create, deploy, and iterate on — without waiting for a human to click through a dashboard. That changes what "good pricing" means: it's not just about base cost, it's about how much iteration your agent can do before the bill becomes unpredictable. Supabase and Codehooks are both modern backend platforms with generous free tiers and affordable paid plans. They overlap in some areas, but they're designed for different workflows: - **Supabase** is a full-stack backend built on PostgreSQL, great for web and mobile apps with complex relational data. Its primary interface is a rich web dashboard. - **Codehooks** is an agent-native backend platform for quick APIs, automation, and integrations. Its primary interface is the CLI — every operation your agent needs is a terminal command. Where pricing really differs is compute and iteration cost: 👉 **Supabase charges extra for functions (compute), while Codehooks includes unlimited compute in every paid plan. Your agent can deploy 50 iterations while debugging — same $19/month.** This doesn't make one platform strictly better — Supabase remains a strong choice for SQL-heavy applications, while Codehooks appeals to developers and AI agents who value predictable costs and rapid iteration. ![Supabase vs Codehooks Comparison](./img/supabase-vs-codehooks.webp) ## Different Solutions for Different Needs While Supabase and Codehooks are both powerful backend platforms, they serve fundamentally different purposes and excel in different scenarios. Think of them as different tools in your development toolkit — each optimized for specific workflows. ### Supabase: The Full-Stack Backend Platform Supabase is ideal when you need: - Traditional SQL database with complex relationships - Enterprise-grade security features - Advanced authentication with multiple providers - Complex data modeling with strict schemas - Full-stack application development - Large-scale deployments with custom requirements - AI agent integration via Model Context Protocol (MCP) ### Codehooks: The Agent-Native Backend Platform Codehooks shines when you or your agent needs: - **Quick API development** — REST endpoints with built-in database, deploy in seconds - **CLI-first deployment** — `coho create` → write code → `coho deploy` (5 seconds) - **Agent-friendly tooling** — `coho prompt`, `coho doctor`, `coho describe`, `coho log -f` - Webhook endpoints and integrations - Scheduled tasks and automation workflows - Simple data ingestion and transformation - Rapid prototyping and MVP development - AI agent integration via [Claude Code plugin](https://github.com/RestDB/codehooks-claude-plugin) and [MCP server](https://github.com/RestDB/codehooks-mcp-server) ### AI Integration Support Both platforms support **Model Context Protocol (MCP)**, enabling integration with AI agents and LLM applications. Codehooks goes further with a dedicated **Claude Code plugin** for auto-detection, templates, and full API context — plus CLI commands like `coho prompt` that generate platform context for any agent. The entire platform is designed so an AI agent can create, deploy, inspect, and iterate on a complete backend without ever leaving the terminal. While there is some overlap in capabilities, choosing between them often depends on your primary use case rather than just comparing features side by side. The following comparison will help you understand their differences in detail, but remember that the "better" choice depends entirely on your specific needs. Looking at Supabase pricing in 2026? Supabase's base plans look affordable, but compute (functions) is billed separately after limited credits. Codehooks includes unlimited compute in every paid plan, making its $19 Pro and $99 Team tiers far more predictable and cost-effective compared to Supabase Pro ($25 + compute) and Team ($599 + compute). ## Quick Pricing Overview | Feature | Supabase Free | Codehooks Free | Supabase Pro ($25) | Codehooks Pro ($19) | Supabase Team ($599) | Codehooks Team ($99) | | ------------ | ------------- | -------------- | ------------------ | ------------------- | -------------------- | -------------------- | | Developers | Unlimited | 1 | Unlimited | 3 | Unlimited | 6 | | API Calls | Unlimited | 60/min | Unlimited | 3600/min | Unlimited | Full-speed | | Database | 500MB | 150MB\* | 8GB | 15GB\* | Custom | 25GB\* | | Storage | 1GB | 1GB | 100GB | 25GB | Custom | 100GB | | Compute | None | **Included** | $10 credits | **Included** | Custom | **Included** | | Support | Community | Chat | Email | Chat + Email | Priority | Premium | | Backups | No | CLI only | Daily | Daily | Daily | Daily | | Environments | 1 Project | 1 Space | Multiple Projects | 1 Space | Multiple Projects | 3 Spaces | \* NoSQL databases typically use significantly less storage space than SQL databases due to their schema-less nature and efficient storage patterns. The listed storage limits for Codehooks may effectively store more data than equivalent SQL storage limits. 1GB of JSON data with 1KB per document is approximately 1 million documents. **Note on Compute Costs**: Codehooks includes **unlimited compute/execution time** in all paid tiers, while Supabase charges additional fees for compute usage beyond included credits. Your agent can deploy 47 iterations while debugging a webhook handler — same $19/month. On Supabase, each function invocation counts against your compute credits. ## Ideal Use Cases: Where Each Platform Shines ### Codehooks: The Agent-Native Backend Platform #### Perfect for Agent-Built Projects - **Quick APIs**: REST endpoints with built-in database and auth — your agent creates a full CRUD API in one deploy - **Webhook Handlers**: Your agent scaffolds a Stripe webhook handler, deploys it in 5 seconds, tests it, iterates - **Scheduled Automations**: CRON jobs for inventory sync, digest emails, data cleanup — agent writes and deploys in one loop - **Multi-step Workflows**: Chain API calls, queue tasks, orchestrate services — all from a single `coho deploy` - **Data Ingestion APIs**: Quick setup for data collection pipelines your agent can iterate on rapidly #### Ideal for Automation - **Scheduled Tasks**: Built-in job scheduling - **Worker Queues**: Background job processing - **Event-driven Workflows**: Automated responses - **Data Synchronization**: Multi-source data sync - **IoT Data Processing**: Real-time sensor data handling #### Great for Rapid Development - **90 Seconds Prompt to Production**: Agent prompts, scaffolds, deploys, verifies — full loop - **Sub-5-Second Deploys**: No build steps, no container images, no CI/CD pipelines - **CLI-first Workflow**: Every operation is a terminal command your agent can execute - **Zero Config**: No provisioning, no wiring, no YAML. One project, one deploy, everything works ### Supabase: The Full-Stack Backend Platform #### Perfect for Traditional Applications - **Web Applications**: Full-stack development - **Mobile Apps**: Complete backend solution - **Enterprise Systems**: Advanced security features - **Complex Data Models**: SQL relationships - **Large-scale Applications**: Enterprise-grade features ## When to Choose Each Platform ### Choose Codehooks When You Need: - **Agent-Native Development** - CLI-first — your agent doesn't need a browser or dashboard - Sub-5-second deploys for rapid iteration - `coho prompt` for instant platform context - Flat-rate pricing that doesn't penalize iteration - **Quick Integration Development** - Fast API creation - Simple data ingestion - Webhook endpoints - Data transformation - **Automation Projects** - Scheduled tasks - Background jobs - Event processing - Data synchronization - **Rapid Prototyping** - Quick MVPs - Feature testing - Internal tools - Microservices ### Choose Supabase When You Need: - **Complex Data Relationships** - SQL databases - Advanced queries - Data integrity - Complex schemas - **Enterprise Features** - Advanced security - Compliance - Custom extensions - Large-scale deployments - **Full-Stack Applications** - Complete backend - Complex auth - Advanced real-time - Custom solutions ## Detailed Pricing Breakdown ### Supabase Pricing Tiers #### Free Tier - Perfect for Hobby Projects - **Cost**: $0/month - **Best for**: Students, hobbyists, and small projects - **Key Features**: - Unlimited API requests - 50,000 monthly active users - 500MB database space - 5GB bandwidth - 1GB file storage - Community support #### Pro Tier - For Growing Projects - **Cost**: $25/month - **Best for**: Small to medium businesses - **Key Features**: - 100,000 monthly active users - 8GB database space - 250GB bandwidth - 100GB file storage - Email support - Daily backups - 7-day log retention - $10 in compute credits #### Team Tier - For Professional Teams - **Cost**: $599/month - **Best for**: Professional teams and businesses - **Key Features**: - SOC2 compliance - HIPAA compliance (add-on) - Read-only and billing member roles - SSO for dashboard - Priority email support and SLAs - 14-day backup retention - 28-day log retention ### Codehooks Pricing Tiers #### Development (Free) - For Small Projects - **Cost**: $0/month - **Best for**: Learning, prototyping, and agent experimentation - **Key Features**: - 1 Developer - 60 API and Function calls/minute - 150MB Database Storage - 1GB File Storage - codehooks.io subdomain only - 1 Space - CLI export backups only - Chat Support - No Credit Card Required #### Pro - For Production Projects - **Cost**: $19/month - **Best for**: Production projects, small teams, and active agent development - **Key Features**: - 3 Developers - API/Functions: up to 3,600 calls/minute - 15GB Database Storage - 25GB File Storage - 1 Custom Domain with SSL - 1 Space - Daily Backups - Chat and Email Support - Extra resources available: - Additional developers: $5 each - Additional database storage: $0.1/GB - Additional file storage: $0.02/GB - Additional domains: $10 each - Additional spaces: $10 each #### Team - For Scaling Teams - **Cost**: $99/month - **Best for**: Scaling teams and growing businesses - **Key Features**: - 6 Developers - Full-speed API and Functions - 25GB Database Storage - 100GB File Storage - 3 Custom Domains with SSL - 3 Spaces - Daily Backups - Premium Support - Extra resources available: - Additional developers: $5 each - Additional database storage: $0.1/GB - Additional file storage: $0.02/GB - Additional domains: $10 each - Additional spaces: $10 each ## Cost Comparison: Which is More Affordable? ### For Small Projects - **Supabase Free**: Unlimited API calls, 500MB database, 1GB storage - **Codehooks Free**: 60 API calls/min, 150MB database, 1GB storage - **Winner**: Supabase offers more generous API and database limits, with equal storage ### For Growing Projects - **Supabase Pro**: $25/month, 8GB database, 100GB storage, $10 compute credits (additional compute costs) - **Codehooks Pro**: $19/month, 15GB database, 25GB storage, **unlimited compute included** - **Winner**: Codehooks offers significantly better value with more database storage, lower base price, and no additional compute costs. For agent workflows with frequent deploys and iterations, the flat rate is especially valuable. ### For Professional Teams - **Supabase Team**: $599/month, custom limits (additional compute costs apply) - **Codehooks Team**: $99/month, 25GB database, 100GB storage, **unlimited compute included** - **Winner**: Codehooks offers significantly lower pricing with generous storage limits and no surprise compute bills, making it highly cost-effective for most teams ## Additional Costs to Consider ### Supabase Additional Costs - Extra storage: Pay for additional storage beyond included limits - Compute credits: Additional compute resources beyond included credits - HIPAA compliance: Additional cost for healthcare projects - Enterprise features: Custom pricing for advanced features ### Codehooks Additional Costs - Developers: $5 each - Database Storage: $0.1/GB - File Storage: $0.02/GB - Domains: $10 each - Spaces: $10 each import FAQSection from '@site/src/components/FAQSection'; ## Codehooks Spaces: The Power of Separate Environments Codehooks has a unique feature called Spaces. Spaces are totally self-contained environments with their own storage, database, code, settings and domains. It's well suited to use for having development, staging and production environments in the same project, but can also be used for different backend / API functionality in the same project. While Supabase uses separate projects for different environments, Codehooks Spaces allow you to manage multiple environments within a single project, making it easier to maintain and deploy across different stages of development. ### Space Management Across Tiers - **Free Tier**: 1 Space for small projects - **Pro Tier**: 1 Space for growing projects - **Team Tier**: 2 Spaces for scaling teams - **Additional Spaces**: $10/month each ## Conclusion While both platforms offer robust solutions, they serve different workflows: - **Codehooks** is an **agent-native backend platform** that excels in rapid development, integration, and automation with **predictable, all-inclusive pricing**: - CLI-first — your agent deploys with `coho deploy`, no dashboard needed - Sub-5-second deploys for rapid iteration (50 iterations while other platforms deploy once) - Unlimited compute included — flat-rate pricing that doesn't penalize agent iteration - Built-in database, queues, cron, and auth — zero config, zero ops - Agent tooling: `coho prompt`, `coho doctor`, Claude Code plugin, MCP server - **Supabase** shines in traditional application development, making it suitable for: - Full-stack applications with complex SQL relationships - Enterprise requirements (SOC2, HIPAA) - Advanced authentication and security - Large-scale deployments Choose Codehooks if your AI agent or team values speed, simplicity, and rapid iteration for backend projects. Choose Supabase if you need enterprise-grade features, complex data relationships, or advanced security features. > **Need detailed technical comparison?** Check out our [features and development experience guide](./supabase-features-comparison) for development workflows, architecture details, and migration considerations. --- ## API Cheat Sheet *(Note: This content contains MDX/JSX code)* **Everything you need to build a serverless backend in one place.** This cheat sheet contains all essential backend APIs for creating complete serverless applications with Codehooks.io. Quick reference for routing, authentication, NoSQL databases, workflows, queues, key-value stores, and real-time features. Each API includes direct links to detailed documentation with examples and usage patterns. **→ [Authentication & Security](#authentication--security)** - Secure your APIs with tokens, JWT, OAuth, or custom auth methods. ## **HTTP Routing APIs** Create REST API endpoints using Express-style route handlers for all standard HTTP methods. Route handlers receive request and response objects for processing incoming HTTP requests. - **`post(path, ...middleware?, function)`** - Register POST route handlers → [Details](/docs/appeventapi#apppostroute-middleware-workerfunction) - **`get(path, ...middleware?, function)`** - Register GET route handlers → [Details](/docs/appeventapi#appgetroute-middleware-workerfunction) - **`put(path, ...middleware?, function)`** - Register PUT route handlers → [Details](/docs/appeventapi#appputroute-middleware-workerfunction) - **`patch(path, ...middleware?, function)`** - Register PATCH route handlers → [Details](/docs/appeventapi#apppatchroute-middleware-workerfunction) - **`delete(path, ...middleware?, function)`** - Register DELETE route handlers → [Details](/docs/appeventapi#appdeleteroute-middleware-workerfunction) - **`all(path, ...middleware?, function)`** - Register handlers for all HTTP methods → [Details](/docs/appeventapi#appallroute-middleware-workerfunction) ## **Middleware & Authentication APIs** Add middleware functions to process requests before they reach route handlers, and serve static or uploaded files. Authentication middleware intercepts requests without valid access tokens. - **`use(function)`** - Register global middleware → [Details](/docs/appeventapi#appuseworkerfunction) - **`auth(path, function)`** - Register authentication middleware → [Details](/docs/appeventapi#appauthroute-workerfunction) - **`static(options, function?)`** - Serve static files from source code → [Details](/docs/appeventapi#appstaticoptions-callback) - **`storage(options, function?)`** - Serve files from blob storage → [Details](/docs/appeventapi#appstorageoptions-callback) ## **Database/NoSQL APIs** Complete NoSQL document database with CRUD operations, schema validation, and MongoDB-style queries. All data is stored as JSON documents organized in collections. ### **Connection & Setup** - **`Datastore.open()`** - Connect to datastore and return API interface → [Details](/docs/nosql-database-api#datastoreopen) ### **Document Operations** - **`insertOne(collection, document)`** - Insert new document → [Details](/docs/nosql-database-api#insertonecollection-document) - **`getOne(collection, query)`** - Get single document by ID/query (throws if not found) → [Details](/docs/nosql-database-api#getonecollection-id--query) - **`findOne(collection, query)`** - Alias for getOne → [Details](/docs/nosql-database-api#findonecollection-id--query) - **`findOneOrNull(collection, query)`** - Get single document, returns null if not found → [Details](/docs/nosql-database-api#findoneornullcollection-id--query) - **`getMany(collection, query?, options?)`** - Get stream of documents → [Details](/docs/nosql-database-api#getmanycollection-query-options) - **`find(collection, query?, options?)`** - Alias for getMany → [Details](/docs/nosql-database-api#findcollection-query-options) - **`toArray(collection, query?, options?)`** - Get documents as array → [Details](/docs/nosql-database-api#toarraycollection-query-options) - **`updateOne(collection, query, document, operators?, options?)`** - Update document (patch) → [Details](/docs/nosql-database-api#updateonecollection-id--query-updateoperators-options) - **`updateMany(collection, query, document, operators?, options?)`** - Update multiple documents → [Details](/docs/nosql-database-api#updatemanycollection-query-document-options) - **`replaceOne(collection, query, document, options?)`** - Replace document completely → [Details](/docs/nosql-database-api#replaceonecollection-id--query-document-options) - **`replaceMany(collection, query, document, options?)`** - Replace multiple documents → [Details](/docs/nosql-database-api#replacemanycollection-query-document-options) - **`removeOne(collection, query)`** - Remove document → [Details](/docs/nosql-database-api#removeonecollection-id--query) - **`removeMany(collection, query, options?)`** - Remove multiple documents → [Details](/docs/nosql-database-api#removemanycollection-query-options) ### **Schema Management** - **`setSchema(collection, schema)`** - Add JSON-Schema validation → [Details](/docs/nosql-database-api#setschemacollection-schema) - **`getSchema(collection)`** - Get collection schema → [Details](/docs/nosql-database-api#getschemacollection) - **`removeSchema(collection)`** - Remove JSON-Schema validation → [Details](/docs/nosql-database-api#removeschemacollection) ### **Collection Management** - **`createCollection(collection, options?)`** - Create new collection → [Details](/docs/nosql-database-api#createcollectioncollection-options) - **`dropCollection(collection)`** - Delete collection → [Details](/docs/nosql-database-api#dropcollectioncollection) ## **Key-Value Store APIs** Fast in-memory key-value storage for caching, session management, and temporary data. Supports optional TTL (time-to-live) for automatic expiration. - **`set(key, value, options?)`** - Set key-string value pair with optional TTL → [Details](/docs/key-value-database-api#setkey-value-options) - **`setObj(key, value, options?)`** - Set key-object pair with optional TTL → [Details](/docs/key-value-database-api#setobjkey-value-options) - **`get(key, options?)`** - Get string value by key → [Details](/docs/key-value-database-api#getkey-options) - **`getObj(key, options?)`** - Get object value by key → [Details](/docs/key-value-database-api#getobjkey-options) - **`getAll(keyPattern, options?)`** - Get all key-value pairs matching pattern → [Details](/docs/key-value-database-api#getallkeypattern-options) - **`del(key, options?)`** - Delete key-value pair → [Details](/docs/key-value-database-api#delkey-options) - **`delAll(key, options?)`** - Delete all key-value pairs matching pattern → [Details](/docs/key-value-database-api#delallkey-options) - **`incr(key, value, options?)`** - Increment numeric value → [Details](/docs/key-value-database-api#incrkey-number-options) - **`decr(key, value, options?)`** - Decrement numeric value → [Details](/docs/key-value-database-api#decrkey-number-options) ## **Queue & Background Processing APIs** Offload time-consuming tasks to background worker queues for asynchronous processing. Workers can process jobs in parallel based on your plan limits. - **`worker(name, function, options?)`** - Register worker functions → [Details](/docs/appeventapi#appworkername-workerfunction-options) - **`enqueue(topic, document, options?)`** - Add job to queue → [Details](/docs/queueapi#enqueuetopic-payload-options) - **`enqueueFromQuery(collection, query, topic, options?)`** - Queue items from database query → [Details](/docs/queueapi#enqueuefromquerycollection-query-topic-options) ## **Job Scheduling APIs** Schedule recurring tasks with cron expressions or run one-time delayed jobs at specific times. Perfect for periodic maintenance, reports, and scheduled notifications. - **`job(cronExpression, worker)`** - Register scheduled cron jobs → [Details](/docs/jobhooks#cron-jobs) - **`schedule.runAt(when, data, worker)`** - Schedule one-time delayed job → [Details](/docs/jobhooks#schedule-delayed-worker-function-dynamically-with-runat) - **`schedule.run(data, worker)`** - Execute worker immediately → [Details](/docs/jobhooks#schedule-worker-function-to-run-immediate) ## **Workflow APIs** Build reliable multi-step workflows with persistent state management and automatic error recovery. Workflows can pause, resume, and handle timeouts across long-running business processes. ### **Workflow Management** - **`createWorkflow(name, description, steps, config?)`** - Create new workflow definition → [Details](/docs/workflow-api#createworkflowname-description-steps-config) - **`start(initialState)`** - Start new workflow instance → [Details](/docs/workflow-api#startinitialstate) - **`updateState(instanceId, state, options?)`** - Update workflow state → [Details](/docs/workflow-api#updatestateinstanceid-state-options) - **`setState(instanceId, stateData)`** - Set complete workflow state → [Details](/docs/workflow-api#setstateinstanceid-statedata) - **`continue(instanceId, reset?)`** - Continue paused workflow → [Details](/docs/workflow-api#continueinstanceid-reset) - **`getWorkflowStatus(id)`** - Get workflow status → [Details](/docs/workflow-api#getworkflowstatusid) - **`getInstances(filter)`** - List workflow instances → [Details](/docs/workflow-api#getinstancesfilter) - **`cancelWorkflow(id)`** - Cancel workflow instance → [Details](/docs/workflow-api#cancelworkflowid) ### **Error Recovery & Monitoring** - **`continueAllTimedOut()`** - Continue all timed out workflows → [Details](/docs/workflow-api#continuealltimedout) - **`findTimedOutSteps(filter?)`** - Find timed out workflow steps → [Details](/docs/workflow-api#findtimedoutstepsfilter) ### **Event Management** - **`on(event, listener)`** - Register event listener → [Details](/docs/workflow-api#onevent-listener) - **`once(event, listener)`** - Register one-time event listener → [Details](/docs/workflow-api#onceevent-listener) - **`off(event, listener)`** - Remove event listener → [Details](/docs/workflow-api#offevent-listener) - **`emit(event, data)`** - Emit custom event → [Details](/docs/workflow-api#emitevent-data) ### **Configuration** - Pass configuration options as the last argument to `createWorkflow()` → [Details](/docs/workflow-api#workflow-configuration) ## **File Management APIs** Store and retrieve files in blob storage with support for text, binary, and streaming operations. Ideal for user uploads, images, documents, and media files. - **`filestore.readFile(path)`** - Read file content as text → [Details](/docs/fileapi#readfilepath) - **`filestore.readFileAsBuffer(path)`** - Read file content as buffer → [Details](/docs/fileapi#readfileasbufferpath) - **`filestore.getReadStream(path)`** - Read file as binary stream → [Details](/docs/fileapi#getreadstreampath) - **`filestore.saveFile(path, filestream)`** - Write binary stream to file → [Details](/docs/fileapi#savefilepath-filestream) - **`filestore.deleteFile(path)`** - Delete file → [Details](/docs/fileapi#deletefilepath) - **`filestore.list(path)`** - List files in directory → [Details](/docs/fileapi#listpath-options) ## **Real-time Communication APIs** Push live updates to connected clients using server-sent events (SSE). Build real-time features like chat, notifications, and live data feeds. - **`realtime.createChannel(channel)`** - Create real-time channel → [Details](/docs/realtimeapi#realtimecreatechannelchannel) - **`realtime.publishEvent(channel, data, query?)`** - Publish event to channel → [Details](/docs/realtimeapi#realtimepublisheventchannel-data-query) - **`realtime.createListener(channel, data)`** - Create listener for channel → [Details](/docs/realtimeapi#realtimecreatelistenerchannel-data) - **`realtime.getListener(channel, listenerID)`** - Get specific listener → [Details](/docs/realtimeapi#realtimegetlistenerchannel-listenerid) - **`realtime.getListeners(channel)`** - Get all listeners for channel → [Details](/docs/realtimeapi#realtimegetlistenerschannel) - **`realtime.removeListener(channel, listenerID)`** - Remove listener → [Details](/docs/realtimeapi#realtimeremovelistenerchannel-listenerid) ## **Inter-Service Communication APIs** Make high-speed authenticated requests between Codehooks applications for microservices architecture. Optimized for internal app-to-app communication within the platform. - **`internalFetch(url, options?)`** - Make authenticated requests to other Codehooks APIs → [Details](/docs/appeventapi#appinternalfetchurl-options) ## **Template & Configuration APIs** Configure application settings and auto-generate complete CRUD REST APIs with a single function call. Use crudlify to instantly create database endpoints without writing route handlers. - **`set(key, val)`** - Set application configuration → [Details](/docs/appeventapi#appsetkey-val) - **`crudlify(schema?, options?)`** - Auto-generate CRUD REST API → [Details](/docs/appeventapi#appcrudlifyschema-options) ## **OpenAPI Documentation APIs** Auto-generate OpenAPI 3.0 specifications and interactive Swagger UI from your schemas and routes. Supports Zod, Yup, and JSON Schema for automatic schema conversion. - **`app.openapi(config, swaggerPath?)`** - Enable OpenAPI docs generation and Swagger UI → [Details](/docs/openapi-swagger-docs#appopenapiconfig-swaggerpath) - **`openapi(spec)`** - Middleware to add OpenAPI metadata to custom routes → [Details](/docs/openapi-swagger-docs#openapispec-middleware) ## **Application Lifecycle APIs** Initialize and bind your application to the serverless runtime. Must be called and exported as the default export from your main application file. - **`init(function?)`** - Initialize application and return manifest → [Details](/docs/appeventapi#appinit) - **`start(function?)`** - Alias for init() method → [Details](/docs/appeventapi#appinit) ## **Database REST API Endpoints** Auto-generated RESTful endpoints for database operations when using crudlify(). All standard CRUD operations are available with query support and pagination. When using `crudlify()`, these endpoints are automatically created: | Operation | Method | Endpoint | Description | | --------- | -------- | -------------------- | ---------------------------------------------------------------------------------- | | List all | `GET` | `/:collection` | Get all documents → [Details](/docs/database-rest-api#get-list-of-documents) | | Query | `GET` | `/:collection?query` | Get documents by query → [Details](/docs/database-rest-api#get-documents-by-query) | | Get by ID | `GET` | `/:collection/:id` | Get single document → [Details](/docs/database-rest-api#get-document-by-id) | | Create | `POST` | `/:collection` | Create new document → [Details](/docs/database-rest-api#create-a-new-document) | | Update | `PATCH` | `/:collection/:id` | Update document → [Details](/docs/database-rest-api#update-a-document-by-id) | | Replace | `PUT` | `/:collection/:id` | Replace document → [Details](/docs/database-rest-api#replace-a-document-by-id) | | Delete | `DELETE` | `/:collection/:id` | Delete document → [Details](/docs/database-rest-api#delete-a-document-by-id) | ## **Authentication & Security** Secure your API endpoints and authenticate users with multiple authentication methods. Choose the approach that fits your use case: API tokens for service-to-service calls, JWT for user authentication, or custom authentication logic. ### **Authentication Methods** - **API Tokens** - Add `x-apikey` header for app-to-app authentication. Create tokens with `coho add-token` or via web console → [Details](/docs/authentication#app-to-app-authentication-with-api-tokens) - **JWT/JWKS** - User authentication via Auth0, Clerk, or Stack-Auth. Configure JWKS endpoint with `coho jwks ` → [Details](/docs/authentication#authenticate-users-with-jwt-using-jwks) - **Admin Tokens** - For CLI automation and CI/CD. Create with `coho add-admintoken` and use with `--admintoken` flag → [Details](/docs/authentication#use-admin-tokens-to-authenticate-with-the-cli) - **IP Whitelisting** - Restrict API access by IP address using `coho whitelist ` or web console → [Details](/docs/authentication#use-ip-address-to-limit-api-access) - **Custom Auth** - Full control with `codehooks-auth` package. Includes OAuth (Google, GitHub), email verification, and OTP → [Details](/docs/authentication#custom-authentication-with-codehooks-auth) ### **Quick Examples** ```bash # Create read-only API token coho add-token --readonly # Configure JWT authentication coho jwks https://your-tenant.auth0.com/.well-known/jwks.json # Use API token in requests curl https://your-app.api.codehooks.io/api \ -H 'x-apikey: your-token-here' ``` ## **Query Syntax Examples** MongoDB-style query syntax for filtering database documents with comparison, logical, and array operators. Queries can be written in JavaScript objects or URL parameters. ### NoSQL Queries ```javascript // Simple equality { status: 'active'; } // Comparison operators { age: { $gt: 18; } } { price: { $lte: 100; } } // Logical operators { $and: [{ status: 'active' }, { age: { $gte: 18 } }]; } // Array operations { tags: { $in: ['javascript', 'nodejs']; } } // Regular expressions { name: { $regex: /john/i; } } ``` ### URL Query Parameters ```bash # Simple query ?status=active&limit=10 # Advanced query ?q={"age":{"$gt":18}}&sort=name&fields=name,email ``` ## **Common Usage Patterns** Real-world code examples showing typical patterns for building APIs, database operations, background processing, and workflows. Copy and adapt these patterns for your own applications. ### Basic API Setup ```javascript import { app } from 'codehooks-js'; // Auto-generate CRUD API app.crudlify(); // Custom routes app.get('/hello', (req, res) => { res.json({ message: 'Hello World' }); }); export default app.init(); ``` ### Database Operations ```javascript import { Datastore } from 'codehooks-js'; async function myFunc() { const conn = await Datastore.open(); // Insert document const doc = await conn.insertOne('users', { name: 'John' }); // Query documents const users = await conn.getMany('users', { active: true }).toArray(); // Key-value operations await conn.set('session:123', { userId: 'abc' }, { ttl: 3600000 }); } ... ``` ### Background Processing ```javascript // worker options, use 5 parallel workers const workerOpt = {workers: 5}; // Register worker app.worker('emailWorker', async (req, res) => { console.log('Processing email:', req.body.payload); res.end(); }, workerOpt); // Queue job async function myFunc() { const conn = await Datastore.open(); await conn.enqueue('emailWorker', { email: 'user@example.com' }); } ... ``` ### Workflow Processing ```javascript // Create a workflow with persistent state const workflow = app.createWorkflow( 'orderProcessing', 'Process customer orders', { start: async (state, goto) => { state.orderId = state.orderId || generateOrderId(); goto('validatePayment', state); }, validatePayment: async (state, goto) => { // Validate payment logic here if (state.paymentValid) { goto('fulfillOrder', state); } else { goto('handlePaymentError', state); } }, fulfillOrder: async (state, goto) => { state.status = 'fulfilled'; goto(null, state); // Complete workflow }, } ); // Start workflow instance const orderWorkflow = await workflow.start({ customerId: '123', amount: 99.99, }); ``` --- **Links:** - [Complete Documentation](/docs) - [Getting Started Guide](/docs/quickstart-cli) - [Webhook Templates](/docs/examples/examples-overview) - [CLI Reference](/docs/cli) - [Prompt for development using ChatGPT, Claude, Cursor etc](/docs/ai-agent-setup) - [Claude Code Plugin](https://github.com/RestDB/codehooks-claude-plugin) — Auto-detection, templates, and `/codehooks:backend` command for Claude Code - [MCP Server for Agent based development](https://github.com/RestDB/codehooks-mcp-server) --- ## The Application Object *(Note: This content contains MDX/JSX code)* import TOCInline from '@theme/TOCInline'; The Application object creates events that lets you hook your serverless functions accordingly. ```mermaid flowchart LR event("REST API events") & job("Background events") --> hook("App hook") --> func("Function") -.- db[(Database)] ``` Available event types are: - REST API route events - Background Job cron and scheduled events - Worker Queue events - Authentication events The event hooks is an Node.js Express style JavaScript library in a [public npm library](https://www.npmjs.com/package/codehooks-js) that you install as a npm package in your project directory. You can check for updates in the npm version. Install the CLI. ```bash npm i codehooks-js ``` Deploy your app with the CLI command. ```bash coho deploy ``` **API quick overview** ## Complete code example Import the `app` library in your application and hook into the various events. ```js import { app, openapi } from 'codehooks-js'; // generic serverless function function fooFunc(req, res) { res.end(); } // generic middleware function function fooMiddleware(req, res, next) { next(); } /* * Hook into events */ app.use(fooMiddleware); // global middleware app.use('/foo', fooMiddleware); // global middleware on routes app.get('/foo', fooFunc); // GET app.post('/foo', fooFunc); // POST app.put('/foo', fooFunc); // PUT app.patch('/foo', fooFunc); // PATCH app.delete('/foo', fooFunc); // DELETE app.all('/foo', fooFunc); // GET, POST, PUT, PATCH, DELETE app.auth('/*', fooMiddleware); // Called before outher routes without access tokens app.job('* * * * *', fooFunc); // subscribe to cron events app.worker('workername', fooFunc); // subscribe to worker events app.static(options); // serve deployed static files and content app.storage(options); // serve dynamic uploaded files and content app.openapi(config); // OpenAPI docs and Swagger UI app.crudlify(options); // Database CRUD REST API // Bind events and functions to the serverless runtime export default app.init(); ``` ## app.init() Mandatory call to bind your functions to events from the serverless runtime. **Parameters** none ## app.use(workerFunction) Adds global middleware to all route API events (get, put, post, patch, delete). **Parameters** - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object), [next](rest-api-app-routes#middleware-functions)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; import cookieParser from 'cookie-parser'; // external npm lib middleware app.use(cookieParser()); app.get('/foo', function (req, res) { // Cookies that have not been signed console.log('Cookies: ', req.cookies); // Cookies that have been signed console.log('Signed Cookies: ', req.signedCookies); // Custom middleware result console.log('Foo: ', req.foo); res.send('Done'); }); export default app.init(); ``` ## app.use(route, workerFunction) Adds global middleware to a specific route API events (get, put, post, patch, delete). **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object), [next](rest-api-app-routes#middleware-functions)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; // custom app.use('/foo', (req, res, next) => { console.log('Called before any other route handlers'); req.foo = 'Foo was here!'; next(); // must be called to continue }); app.get('/foo', function (req, res) { // Custom middleware result console.log('Foo: ', req.foo); res.send('Done'); }); export default app.init(); ``` ## app.get(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP GET method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.get('/foo', function (req, res) { res.send('Done'); }); export default app.init(); ``` ## app.post(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP POST method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.post('/foo', function (req, res) { console.log('Data from client is', req.body); res.send('Done'); }); export default app.init(); ``` ## app.put(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP PUT method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.put('/foo', function (req, res) { console.log('Data from client is', req.body); res.send('Done'); }); export default app.init(); ``` ## app.patch(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP PATCH method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.patch('/foo', function (req, res) { console.log('Data from client is', req.body); res.send('Done'); }); export default app.init(); ``` ## app.delete(route, ...middleware?, workerFunction) Execute workerFunction function on HTTP DELETE method and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.delete('/foo/:ID', function (req, res) { const { ID } = req.params; console.log('Delete this', ID); res.send('Done'); }); export default app.init(); ``` ## app.all(route, ...middleware?, workerFunction) If no other routes matched before this, then execute the workerFunction function on all HTTP methods and route match. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **middleware**: (optional) One or more middleware functions, e.g. `openapi(spec)`, that process the request before the handler - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** ```js import { app } from 'codehooks-js'; app.get('/foo', function (req, res) { res.send('Done GET'); }); app.all('/*', function (req, res) { res.send( `None of the above routes matched but this ${req.method} ${req.originalUrl}` ); }); export default app.init(); ``` ## app.auth(route, workerFunction) Execute workerFunction function when no authentication token (x-apikey or JWT) is present. Works for all HTTP methods and route matches. **Parameters** - **route**: String or RegExp that [matches route](rest-api-app-routes#rest-api-route-matching) - **workerFunction**: function([requestObject](rest-api-app-routes#the-rest-api-request-object), [responseObject](rest-api-app-routes#the-rest-api-response-object)) **Returns** void **Code example** [See Auth hooks docs here.](authhooks) ## app.worker(name, workerFunction, options) Register a named workerFunction function that can be used to process queued events or scheduled events. **Parameters** - **name**: unique worker name - **workerFunction**: function(queueData, responseFunction) - **options**: (optional) Object with configuration settings - **workers**: number - Number of parallel workers to process queue items concurrently. Limited by your plan quota. - **timeout**: number (milliseconds) - Paid plans can set the worker function execution time up to 10 minutes. **Returns** void **Code example for worker queues** [See Queue docs here.](queuehooks) **Code example for scheduled workers** [See scheduled jobs docs here.](jobhooks#schedule-delayed-worker-function-dynamically-with-runat) ## app.job(cronExpression, workerFunction) Execute workerFunction function when cron job are scheduled. **Parameters** - **cronExpression**: a valid cron expression string - **workerFunction**: function(jobData, responseFunction) [See Job hook docs here.](jobhooks) ## app.static(options, callback) Serve static files from deployed source directory. For example: `app.static({ route: '/', directory: '/dist', default: 'index.html', notFound: '/index.html' })` **Options** - **route**: URL sub route for clients - **directory**: path to file upload directory - **default**: (optional) default file to serve when requesting a directory (e.g., 'index.html') - **notFound**: (optional) file to serve for 404 errors (essential for client-side routing in SPAs) **callback(req, res, next)** Provide a middleware function to control the request. For example, add client side cache headers instructions for static assets. ```js // serve static assets/images, and cache for 1 hour app.static({ route: '/assets', directory: '/assets' }, (req, res, next) => { console.log( 'If you see this, the client cache is invalidated or called for the first time' ); const ONE_HOUR = 1000 * 60 * 60; res.set('Cache-Control', `public, max-age=${ONE_HOUR}, s-maxage=${ONE_HOUR}`); res.setHeader('Expires', new Date(Date.now() + ONE_HOUR).toUTCString()); res.removeHeader('Pragma'); next(); }); ``` ## app.storage(options, callback) Serve files from the blob storage file system. You can upload files to the blob storage with the CLI. Also see the [Filesystem API docs](/docs/fileapi). ```bash $ coho file-upload --projectname 'myproject-fafc' --space 'dev' --src '/mylocaldir/' --target '/mydocuments' ``` For example: `app.storage({ route: '/docs', directory: '/mydocuments' })` **Options** - **directory**: path to file upload directory - **route**: URL sub route for clients **callback(req, res, next)** Provide a middleware function to control the request. ## app.crudlify(schema, options) Creates a complete Database REST API ([detailed docs here](/docs/database-rest-api)). Example: REST API for two collection `customer` and `product`, any schema is allowed: ```js app.crudlify({ customer: {}, product: {} }, { prefix: '/crudapi' }); // Example valid URL's // https://myproject-ff00.api.codehooks.io/dev/customer?name=Bill // https://myproject-ff00.api.codehooks.io/dev/product?price>42 ``` **Schema** -- **collection**: schema, for specific collections, e.g. `{"collection": someSchema}` **Options** - **prefix**: serve REST routes under another route. `{"prefix": "/api"}` **Returns** Promise to `before` `after` functions. Example: ```js crudlify(app, {product, customer}).then((hooks) => { hooks.beforePOST('customer', async (data) => { // do something here data.foo = 'Was here before saving!' }) hooks.afterPOST('product', async (data) => { console.log("Data as saved to the database", data) // do something here }) ``` ## app.openapi(config, swaggerPath?) Enable automatic OpenAPI 3.0 documentation generation and serve an interactive Swagger UI. When combined with `crudlify`, schemas are automatically converted to OpenAPI specifications. See the [complete OpenAPI documentation](/docs/openapi-swagger-docs) for full details. **Parameters** - **config** (object): OpenAPI configuration object - **info** (object, required): API metadata with `title`, `version`, and optional `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` - **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** ```js import { app } from 'codehooks-js'; import { z } from 'zod'; const todoSchema = z.object({ title: z.string().min(1).max(200), completed: z.boolean().default(false) }); // Enable OpenAPI documentation app.openapi({ info: { title: 'Todo API', version: '1.0.0', description: 'Task management API' }, tags: [ { name: 'Todos', description: 'Todo operations' } ], filter: (op) => op.method !== 'delete' // Hide DELETE endpoints from docs }, '/api-docs'); // Swagger UI at /api-docs app.crudlify({ todos: todoSchema }); export default app.init(); ``` After deployment, visit: - `/docs` (or custom path) — Interactive Swagger UI with authentication support - `/openapi.json` — Raw OpenAPI 3.0 specification ## openapi(spec) middleware Add OpenAPI metadata to custom routes. Import the `openapi` middleware helper and use it to document individual endpoints. **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 schemas directly) - **parameters** (array, optional): Query/path parameters - **responses** (object): Response definitions - **security** (array, optional): Security requirements (use `[]` for public endpoints) **Returns** Middleware function **Code example** ```js import { app, openapi } from 'codehooks-js'; import { z } from 'zod'; const UserSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email() }); app.openapi({ info: { title: 'User API', version: '1.0.0' } }); // Document a custom POST endpoint with Zod schema app.post('/users', openapi({ summary: 'Create a new 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) => { // Handler logic res.status(201).json({ id: '123', ...req.body }); } ); // Document a GET endpoint with query parameters app.get('/users', openapi({ summary: 'List users', tags: ['Users'], parameters: [ { name: 'limit', in: 'query', schema: { type: 'integer', default: 20 } }, { name: 'role', in: 'query', schema: { type: 'string', enum: ['admin', 'user'] } } ], responses: { 200: { description: 'List of users' } } }), async (req, res) => { res.json([]); } ); export default app.init(); ``` ## app.createWorkflow(name, desc, workflowJSON, config?) Create reliable stateful workflows using plain JavaScript. **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 including `collectionName`, `queuePrefix`, `timeout`, `maxStepCount`, `workers`, and step-specific settings **Returns**: Workflow instance for managing the workflow **Code example**: ```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(); ``` ## app.set(key, val) Set configuration settings for the application. This allows you to configure various aspects of your application behavior. **Parameters** - **key**: String - the configuration key - **val**: any - the configuration value **Returns** void **Code example** ```js import { app } from 'codehooks-js'; import handlebars from 'handlebars'; // Set views directory app.set('views', '/views'); // Set view engine app.set('view engine', { hbs: handlebars }); // Set other configuration options app.set('foo', 'bar'); export default app.init(); ``` ## app.internalFetch(url, options) Fetch data from another Codehooks API. This method is designed for high speed app-to-app communication within the Codehooks platform, allowing you to make authenticated API calls to other Codehooks applications. **Authentication**: Must include `x-apikey` header for authentication if the target API is not public. **Parameters** - **url**: String - URL to fetch from (e.g., `http://myapi-ffee.codehooks.io/dev/api/myroute`) - **options**: Object (optional) - Fetch options including method, headers, body, etc. **Returns** `Promise` - Promise that resolves with the fetched data **Code example** ```js import { app } from 'codehooks-js'; app.get('/fetch-data', async (req, res) => { try { // Fetch data from another Codehooks API const data = await app.internalFetch( 'http://myapi-ffee.codehooks.io/dev/api/users', { method: 'GET', headers: { 'Content-Type': 'application/json', 'x-apikey': 'your-target-api-key-here' } } ); res.json({ success: true, data }); } catch (error) { res.status(500).json({ error: 'Failed to fetch data' }); } }); app.post('/sync-user', async (req, res) => { try { // POST data to another Codehooks API const result = await app.internalFetch( 'http://userservice-1234.codehooks.io/dev/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-apikey': 'your-target-api-key-here' }, body: JSON.stringify(req.body) } ); res.json({ success: true, result }); } catch (error) { res.status(500).json({ error: 'Failed to sync user' }); } }); export default app.init(); ``` Read more in the [detailed workflow API docs](/docs/workflow-api) --- ## Authentication *(Note: This content contains MDX/JSX code)* Application APIs and data can be accessed with a secure token. Tokens can be JWT tokens created from a provider such as Auth0.com or API tokens created by yourself (or any project user with ADMIN rights). In addition, some CLI commands can use a special _admin token_ (se below). ## App-to-app authentication with API tokens ### Create an API token with the CLI API tokens can be created i two ways. 1. Using the CLI command `coho add-token` 2. Using the admin application at https://account.codehooks.io The example below shows how to use the CLI to create a new `READ` access API token. ```bash coho add-token --readonly Created new token 5f1ce782-5798-4295-bdd0-51568390a59c with access READ ``` The API token can now be used to access your application API. Add the `x-apikey` as a HTTP header in the request. ```bash curl https://myproject-ff00.api.codehooks.io/dev/myapi \ -H 'x-apikey: 5f1ce782-5798-4295-bdd0-51568390a59c' ``` ### Using the API key as a URL parameter Some integration platforms (e.g. Zapier, Make, Power Automate) only support URL-based API calls and don't allow setting custom HTTP headers. For these cases, you can pass the API key as a query parameter using `apikey`. ```bash curl 'https://myproject-ff00.api.codehooks.io/dev/myapi?apikey=5f1ce782-5798-4295-bdd0-51568390a59c' ``` :::tip Using with crudlify When using the `apikey` URL parameter together with [crudlify](/docs/appeventapi#appcrudlifyschema-options), you should use the `q` query parameter for your database queries. This prevents the `apikey` parameter from being interpreted as a database query filter. ```bash # Correct: use q parameter for the query, apikey for authentication curl 'https://myproject-ff00.api.codehooks.io/dev/customers?q={"city":"New York"}&apikey=5f1ce782-5798-4295-bdd0-51568390a59c' ``` ::: :::warning note The secure token should not be used from web pages. We recommend that you only use it server side. ::: ### Using the web application to add an API token In your account at https://account.codehooks.io, find the project and the space settings to add a new token. ![add API token using web](/img/api-token-using-ui.png) ## Authenticate users with JWT (using JWKS) ### Set up JWKS with the CLI If you have set up JWT authentication using [Auth0.com (Okta)](https://auth0.com), [Clerk](https://clerk.com/), [Stack-Auth](https://stack-auth.com) or similar services, you can easily set up a JWKS endpoint in the space settings. All API calls will then be verified using this mechanism. ```bash # add JWKS endpoint $ coho jwks https://.auth0.com/.well-known/jwks.json Set JWKS url to space 'dev' ``` | Auth provider | JWKS URL | | -------------- | ----------------------------------------------------------------------------------------------- | | Auth0.com | `https://.auth0.com/.well-known/jwks.json` | | Clerk.com | `https:///.well-known/jwks.json` | | Stack-auth.com | `https://api.stack-auth.com/api/v1/projects//.well-known/jwks.json` | ### Using the web application to set a JWKS endpoint In your account at https://account.codehooks.io, find the project and the space settings to set the JWKS endpoint URL. ![set JWKS endpoint using web](/img/add-jwks-endpoint.png) :::tip You can also roll your own authentication using code. Read more about using [authhooks](/docs/authhooks) for this. ::: :::info Verifying JWT tokens - RS256 vs HS256 By default, to verify login tokens (JWT), Auth0 example applications now use the asymmetric RS256 algorithm instead of the HS256 symmetric one. What is the difference? HS256 requires you to use a secret key to verify the token but RS256 let you use a public key which can be fetched from an URL on the web (JWKS URL). You can read more about it in [this blogpost by Auth0.](https://auth0.com/blog/navigating-rs256-and-jwks/) ::: ## Use admin tokens to authenticate with the CLI For use with the CLI, you can create admin tokens instead of having to log in to your account as a user with ADMIN rights. Admin tokens belong to the personal account or a team and applies to the following codehooks CLI commands: deploy, undeploy, createindex, removeindex, backup, restore, import, export, query. Admin tokens are ideal for use in shell scripts and for CI purposes. ```bash # add admin token $ coho add-admintoken ? Select personal account or team to add admin token to Personal New admin token added: 0882e570d8fc7fe97ae958ae8df3d7ba-7f5c5eb7177c Please copy this now and keep it secret. It will never be shown in full again. ``` ```bash # use the admin token $ coho deploy --admintoken 0882e570d8fc7fe97ae958ae8df3d7ba-7f5c5eb7177c Deploying to Project: students-3bfe Space: dev Deployed Codehook successfully! 🙌 ``` ## Use IP address to limit API access From your serverless functions you can inspect the client IP address from the request header fields. The example headers listen below shows that the client IP address is `11.22.33.44`. ```js headers: { host: 'myapp-f0gt.api.codehooks.io', 'x-request-id': 'ba5582e3bafd12602b3f70a5af5ab20f', 'x-real-ip': '11.22.33.44', 'x-forwarded-for': '11.22.33.44', 'x-forwarded-host': 'myapp-f0gt.api.codehooks.io', 'x-forwarded-port': '443', 'x-forwarded-proto': 'https', 'x-forwarded-scheme': 'https', 'x-scheme': 'https', accept: '*/*', 'user-agent': 'Thunder Client (https://www.thunderclient.com)', 'x-api-key': '07ee0bba-xxxx-yyy-zzzz-dfd0b991fa0a' } ``` Use the [CLI command `coho whitelist`](/docs/cli.md#whitelist) or the admin web application to give exclusive IP addresses API access, e.g. `coho whitelist 11.22.33.44` You can use a [authentication hook](/docs/authhooks) to intercept API requests. ## Custom Authentication with codehooks-auth For complete control over authentication, you can use the open-source [`codehooks-auth`](https://github.com/RestDB/codehooks-auth) package. This solution provides: - JWT-based authentication with access and refresh tokens - Built-in user management with OAuth support (Google and GitHub) - Email verification and OTP (One-Time Password) authentication - Customizable templates and email notifications ![codehooks-auth](https://raw.githubusercontent.com/RestDB/codehooks-auth/refs/heads/main/examples/images/auth-lock-screen.png) ### Setup Instructions 1. Install the required packages: ```bash npm install codehooks-auth codehooks-js ``` 2. Set up JWT secrets: ```bash coho set-env JWT_ACCESS_TOKEN_SECRET 'your_access_token_secret' --encrypted coho set-env JWT_REFRESH_TOKEN_SECRET 'your_refresh_token_secret' --encrypted ``` 3. Configure authentication in your backend code: ```js import { app } from 'codehooks-js'; import { initAuth } from 'codehooks-auth'; const settings = { JWT_ACCESS_TOKEN_SECRET: process.env.JWT_ACCESS_TOKEN_SECRET, JWT_REFRESH_TOKEN_SECRET: process.env.JWT_REFRESH_TOKEN_SECRET, redirectSuccessUrl: '/dashboard.html', // where to redirect after successful login baseAPIRoutes: '/api', // protected routes // Optional: Configure OAuth providers google: { CLIENT_ID: process.env.CLIENT_ID, CLIENT_SECRET: process.env.CLIENT_SECRET, REDIRECT_URI: 'https://{YOUR_APP_URL}.codehooks.io/auth/oauthcallback/google', }, github: { CLIENT_ID: process.env.GITHUB_CLIENT_ID, CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET, REDIRECT_URI: 'https://{YOUR_APP_URL}.codehooks.io/auth/oauthcallback/github', }, }; // Initialize auth with your settings initAuth(app, settings); export default app.init(); ``` 4. Access protected routes in your client code: ```js fetch('/api/protected-route', { credentials: 'include', headers: { 'Content-Type': 'application/json', }, }); ``` The authentication system will automatically handle: - User registration and login flows - JWT token management (access and refresh tokens) - OAuth authentication with supported providers - Email verification and password reset functionality - Protected API route authorization See the [codehooks-auth documentation](https://github.com/RestDB/codehooks-auth) for complete setup instructions, template customization options, and advanced configuration. --- ## Auth hooks *(Note: This content contains MDX/JSX code)* Auth hooks lets you override the default security behaviour on specific routes. If an auth hook route matches a route hook, and no authentication token is present in the headers, the auth function is called as a last resort to allow or dismiss the request. The auth hook function logic calls `next()` to allow, or `response.end()` to block a client request. Use the auth hook to create public routes and custom rules and overrides to fit any use case. ## Example auth hook ```javascript {9} title="index.js" import app from 'codehooks-js'; // Standard JS lib for express style code // REST API routes app.get('/specialroute/frags', (req, res) => { res.end('You have the correct secret header value'); }); // Auth hook app.auth('/specialroute/*', (req, res, next) => { // call some auth function here, e.g. myLookup myLookup(req.headers['X-challenge'], (err, data) => { if (err) { res.status(401); // Unauthorized res.end(); } else { // allow API call next(); } }); }); function myLookup(challenge, callback) { if (challenge === 'SOMESECRET') { callback(null); } else { callback('Sorry'); } } export default app.init(); // Bind functions to the serverless runtime ``` [See docs for route matching](rest-api-app-routes#route-matching) [See docs for middleware functions and the next() function](rest-api-app-routes#middleware-functions) --- ## Codehooks CLI tool *(Note: This content contains MDX/JSX code)* # Codehooks CLI Documentation **Version**: `1.3.2` > Command-line interface for [Codehooks.io](https://codehooks.io) - A serverless backend platform ## Quick Start ### Installation ```bash npm install -g codehooks ``` ### Basic Usage ```bash # Login to your account codehooks login # Create a new project codehooks create my-project # Deploy your code codehooks deploy ``` --- ## Commands by Category ### Getting Started | Command | Description | |---------|-------------| | [docs](#docs) | Open the API cheatsheet - routes, database, key-value, queues, jobs, workflows, and more | | [prompt](#prompt) | Display the Codehooks.io development prompt for AI assistants | | [login](#login) | Authenticate CLI - sign up and/or sign in | | [logout](#logout) | Log out of the CLI | | [account](#account) | Show info about account, projects and invitations | ### Project Management | Command | Description | |---------|-------------| | [create](#create) | Create a new codehooks project | | [init](#init) | Link an existing Codehooks project to the current folder | | [info](#info) | Show info about project and spaces | | [doctor](#doctor) | Show comprehensive project status (agent-friendly) | | [describe](#describe) | Describe the deployed app (collections, schemas, queues, files) | | [invite](#invite) | Invite user to project | | [join](#join) | Join project | | [leave](#leave) | Leave project | | [remove-project](#remove-project) | Remove the project | ### Space Management | Command | Description | |---------|-------------| | [add](#add) | Add new space to project | | [use](#use) | Set active space | | [remove-space](#remove-space) | Remove space and data | ### Deployment | Command | Description | |---------|-------------| | [verify](#verify) (compile, ver, comp) | Compile code (defaults to current dir) | | [deploy](#deploy) (de, dep) | Deploys current codehook folder | | [undeploy](#undeploy) (unde, undep) | Undeploy current codehook folder | | [install](#install) (inst, i) | Install a template application | ### Database & Collections | Command | Description | |---------|-------------| | [collection](#collection) (coll, col, ls) | Show collections for space | | [createcollection](#createcollection) (createcoll, add-collection) | Create a new collection | | [dropcollection](#dropcollection) (dropcoll, rmcoll, deletecoll) | Delete all data in collection and remove collection name | | [add-schema](#add-schema) (schema, create-schema) | Add a JSON schema to a collection | | [remove-schema](#remove-schema) (delete-schema, del-schema) | Remove JSON schema for a collection | | [cap-collection](#cap-collection) (cap, cap-coll, capcoll) | Cap a collection | | [uncap-collection](#uncap-collection) (uncap) | Remove cap on a collection | ### Data Operations | Command | Description | |---------|-------------| | [query](#query) (q) | Run query on collection in current space | | [count](#count) (co) | Count objects in a collection in current space | | [import](#import) (imp) | Import JSON or CSV data from file | | [export](#export) | Export JSON or CSV data | | [createindex](#createindex) (index, idx, create-index) | Add field(s) to a query index | | [dropindex](#dropindex) (removeindex, remove-index, rmindex, delete-index) | Remove field(s) from a query index | ### Key-Value Store | Command | Description | |---------|-------------| | [get](#get) | Retrieve key-value pair(s) from a space | | [set](#set) | Set key-value pair in a space | | [del](#del) | Delete key-value pair in a space | ### File Management | Command | Description | |---------|-------------| | [file-upload](#file-upload) (up, upload) | Upload files to server | | [file-delete](#file-delete) (delete) | Delete a file from server | | [file-list](#file-list) (files) | List files from server | ### Monitoring & Logs | Command | Description | |---------|-------------| | [log](#log) (logs) | Show system logs for a space. | | [stats](#stats) | Show usage metrics for project spaces | | [workflow-status](#workflow-status) (wfstat) | Visualize workflow data from workflowdata collection | | [queue-status](#queue-status) (qs, queues) | Visualize queue status with item counts and workers | ### Security & Access | Command | Description | |---------|-------------| | [add-token](#add-token) | Add token to space | | [remove-token](#remove-token) | Remove token from space | | [set-env](#set-env) | Set environment variable for space | | [remove-env](#remove-env) | Remove environment variable from space | | [jwks](#jwks) | Set/replace JWKS endpoint for OAuth2 authentication. Set to "" (empty string) to remove. | | [whitelist](#whitelist) | Add host to whitelist (use to restrict space access) | | [whitelist-remove](#whitelist-remove) | Remove host from whitelist | ### Admin Tools | Command | Description | |---------|-------------| | [admin](#admin) | Open the codehooks.io admin account UI at account.codehooks.io | | [admintokens](#admintokens) | Show active admintokens for account or team | | [add-admintoken](#add-admintoken) | Add admin token to account or team (for use with CI) | | [remove-admintoken](#remove-admintoken) | Remove admin token from account or team | ### Shell Integration | Command | Description | |---------|-------------| | [completion](#completion) | Generate command completion script. Just add this to your .bashrc, .bash_profile, .zshrc (or similar) on *nix machines | --- ## Global Options These options are available for all commands: | Option | Description | |--------|-------------| | -d, --debug | Show debug (verbose) information | | -h, --help | Show help | | -v, --version | Show version number | --- ## Command Reference ### Getting Started ### docs Open the API cheatsheet - routes, database, key-value, queues, jobs, workflows, and more --- ### prompt Display the Codehooks.io development prompt for AI assistants --- ### login Authenticate CLI - sign up and/or sign in --- ### logout Log out of the CLI **Options:** | Option | Description | |--------|-------------| | `--dir` | [default: "."] | | `--json` | Output as JSON [boolean] | --- ### account Show info about account, projects and invitations **Options:** | Option | Description | |--------|-------------| | `--json` | Output as JSON [boolean] | --- ### Project Management ### create Create a new codehooks project **Options:** | Option | Description | |--------|-------------| | `-n, --projectname` | Project name | | `--description` | A few words about this project | | `-t, --teamid` | Add project to team with this id | | `--template` | Template to use (e.g., "slack-memory-bot") or "default" for CRUD template [string] | | `--here` | Create project in current folder instead of new subfolder [boolean] [default: false] | | `-e, --empty` | Only create config.json, skip generating code files [boolean] [default: false] | | `--no-deploy` | Skip automatic deployment of default template [boolean] [default: false] | | `-g, --ga-deploy` | Add Github Action for automatic deploy [boolean] | | `-y, --yes` | Skip all prompts and use defaults (non-interactive mode) [boolean] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks create myapp Create a new project interactively codehooks create myapp --yes Create with defaults, no prompts codehooks create myapp --here Create in current folder codehooks create myapp --empty Create config only, no code files codehooks create myapp --here --empty Create config in current folder codehooks create myapp --template slack-memory-bot Create with a specific template codehooks create myapp --teamid abc123 --yes Create in team, non-interactive codehooks create myapp --no-deploy Create without deploying ``` --- ### init Link an existing Codehooks project to the current folder **Options:** | Option | Description | |--------|-------------| | `-n, --projectname` | Project name | | `-s, --space` | Space (environment) name [default: "dev"] | | `-e, --empty` | Only create the project config file [boolean] | | `--download` | Download source code from project [boolean] [default: true] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks init codehooks init --empty codehooks init --download ``` --- ### info Show info about project and spaces **Options:** | Option | Description | |--------|-------------| | `--projectname` | Active project [required] | | `--json` | Output info as JSON (not table) [boolean] | | `--space` | Only show info for this space [string] | | `-e, --examples` | Include cURL examples [boolean] | --- ### doctor Show comprehensive project status (agent-friendly) **Options:** | Option | Description | |--------|-------------| | `-p, --projectname` | Select which project name to use [required] | | `-s, --space` | Select which space to inspect | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON (default: true) [boolean] [default: true] | | `--collections` | Include collections info [boolean] [default: true] | | `--stats` | Include usage stats [boolean] [default: true] | | `--deploys` | Include deployment history [boolean] [default: true] | | `--logs` | Include recent error logs [boolean] [default: true] | **Examples:** ```bash codehooks doctor Full project status as JSON codehooks doctor --no-json Human-readable summary codehooks doctor --no-logs --no-stats Skip logs and stats codehooks doctor --admintoken CI/CD usage ``` --- ### describe Describe the deployed app (collections, schemas, queues, files) **Options:** | Option | Description | |--------|-------------| | `-p, --projectname` | Select which project name to use [required] | | `-s, --space` | Select which space to inspect | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON (default: true) [boolean] [default: true] | | `--files` | Include uploaded files list [boolean] [default: true] | | `--schemas` | Include full schema objects (use --no-schemas for summary only) [boolean] [default: true] | **Examples:** ```bash codehooks describe Full app description as JSON codehooks describe --no-json Human-readable summary codehooks describe --no-files Skip file listing codehooks describe --no-schemas Compact collection info without schemas ``` --- ### invite Invite user to project **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `-t, --email` | [string] [required] | | `--role` | [string] [default: "ADMIN"] | | `--remove` | remove invitation [boolean] | | `--json` | Output as JSON [boolean] | --- ### join Join project **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--json` | Output as JSON [boolean] | --- ### leave Leave project **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--json` | Output as JSON [boolean] | --- ### remove-project Remove the project **Options:** | Option | Description | |--------|-------------| | `-p, --projectname` | Project name [string] [required] | --- ### Space Management ### add Add new space to project **Options:** | Option | Description | |--------|-------------| | `--projectname` | [required] | | `-n, --space` | space name [required] | | `--restricted` | only team admins or owner can use if restricted [boolean] | | `--json` | Output as JSON [boolean] | --- ### use Set active space **Options:** | Option | Description | |--------|-------------| | `--projectname` | [required] | | `-n, --name` | | | `--json` | Output as JSON [boolean] | --- ### remove-space Remove space and data **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name | | `--space` | A space in your project [required] | | `--json` | Output as JSON [boolean] | --- ### Deployment ### verify **Aliases:** compile, ver, comp Compile code (defaults to current dir) **Options:** | Option | Description | |--------|-------------| | `--dir` | [default: "."] | | `--space` | Select which space to access | | `-p, --projectname` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `-e, --main` | Application main file, default is codehooks [default: "index"] | | `--tofile` | Output file path, writes to specified file when provided | --- ### deploy **Aliases:** de, dep Deploys current codehook folder **Options:** | Option | Description | |--------|-------------| | `--dir` | [default: "."] | | `-s, --space` | Select which space to access | | `-p, --projectname` | Select which project name to use | | `--history` | Show deployment history | | `--rollback` | Undo last deployment, set previous as active | | `--json` | Output JSON format | | `--admintoken` | Use admin token authentication (use for CI) | | `--template` | Deploy a pre defined code template | | `--vibemode` | Fast deploy, skip source code upload [boolean] [default: false] | | `--upload` | Upload source code assets to codehooks.io this projects environment [boolean] [default: true] | | `--minify` | Minify source code assets before upload [boolean] [default: true] | | `--compress` | Upload source as tar.gz archive instead of file-by-file [boolean] [default: false] | | `-e, --main` | Application main file, default is codehooks [default: "index"] | --- ### undeploy **Aliases:** unde, undep Undeploy current codehook folder **Options:** | Option | Description | |--------|-------------| | `--dir` | [default: "."] | | `-s, --space` | Select which space to access | | `-p, --projectname` | Select which project name to use | | `--json` | Output JSON format | | `--admintoken` | Use admin token authentication (use for CI) | --- ### install **Aliases:** inst, i Install a template application **Options:** | Option | Description | |--------|-------------| | `-t, --template` | Template directory from Github repo with templates [string] | | `--space` | Select which space (environment) to access | | `-p, --projectname` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks install Interactive template selection codehooks install static-website-tailwindcss Install specific template codehooks install --template slack-memory-bot Install with --template flag ``` --- ### Database & Collections ### collection **Aliases:** coll, col, ls Show collections for space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `--json` | JSON output format | | `--sys` | Show system collections [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | --- ### createcollection **Aliases:** createcoll, add-collection Create a new collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--cap` | Cap collection to max documents | | `--capdelay` | Delay capping to millis | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks createcollection pets codehooks createcollection logs --cap 5000 ``` --- ### dropcollection **Aliases:** dropcoll, rmcoll, deletecoll Delete all data in collection and remove collection name **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks dropcollection pets ``` --- ### add-schema **Aliases:** schema, create-schema Add a JSON schema to a collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--schema` | Path to file with JSON schema for collection [required] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks add-schema --collection 'person' --schema './personSchema.json' ``` --- ### remove-schema **Aliases:** delete-schema, del-schema Remove JSON schema for a collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks remove-schema --collection 'person' ``` --- ### cap-collection **Aliases:** cap, cap-coll, capcoll Cap a collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--cap` | Cap collection to max documents [default: 1000] | | `--capdelay` | Delay capping to max millis [default: 1000] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks capcollection --collection 'temperature' --cap 10000 ``` --- ### uncap-collection **Aliases:** uncap Remove cap on a collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks uncap-collection --collection 'temperature' ``` --- ### Data Operations ### query **Aliases:** q Run query on collection in current space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `-q, --query` | Limit selection with a query expression or JSON string '\{...\}' [default: ""] | | `-n, --count` | Count query results | | `-c, --collection` | Collection name [required] | | `--delete` | Delete all items from query result | | `--update` | Patch all items from query result with JSON string '\{...\}' [string] | | `--replace` | Replace all items from query result with JSON string '\{...\}' [string] | | `--useindex` | Use an indexed field to scan data in query [string] | | `--start` | Start value for index scan [string] | | `--end` | End value for index scan [string] | | `--limit` | Limit query result [number] | | `--fields` | Comma separated list of fields to include [string] | | `--sort` | Comma separated list of fields to sort by [string] | | `--offset` | Skip items before returning data in query result [number] | | `--enqueue` | Add query result to queue topic [string] | | `--pretty` | Output data with formatting and colors | | `--reverse` | Scan index in reverse order | | `--table` | Output data as formatted table (not JSON) [boolean] | | `--csv` | Output data in CSV format [boolean] | | `--jsonl` | Output as JSON Lines (one JSON object per line) [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | **Examples:** ```bash codehooks query pets name=Polly codehooks query --collection pets --query 'name=Polly&type=Parrot' codehooks query --collection pets --query 'name=/^po/' codehooks query --collection pets --query 'name=/^po/' --sort 'age,name' codehooks query pets 'name=Polly' --useindex name --fields 'name,type' codehooks q pets 'name=Polly&type=Parrot' --update '{"name": "Zilla"}' codehooks q pets 'type=Fish' --delete codehooks q pets 'type=Snake' --enqueue 'mytopic' codehooks q pets --jsonl codehooks query pets --q '{"type":"Dog"}' --project petsdb-5544 --space dev ``` --- ### count **Aliases:** co Count objects in a collection in current space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `-c, --collection` | Collection name [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | --- ### import **Aliases:** imp Import JSON or CSV data from file **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Project name [string] [required] | | `-s, --space` | A space in your project [string] | | `-c, --collection` | A collection in a Space [string] [required] | | `-f, --filepath` | File to import [string] [required] | | `--separator` | Field separator character, default is ',', also normal with '\t' or ';' [string] [default: ","] | | `--dryrun` | Test only, will not import any data [boolean] [default: false] | | `--rowcount` | Add a row count field to each imported record [boolean] [default: false] | | `--admintoken` | Use admin token authentication (use for CI) | | `--local` | Import data to local development server on port parameter [number] | | `--json` | Output as JSON [boolean] | | `--encoding` | String encoding to use, latin1, utf8, ascii, hex, ucs2 [string] [default: "utf8"] | **Examples:** ```bash codehooks import ./myfile.csv mycollection codehooks import --filepath ./myfile.csv --collection mycollection codehooks import -f ./myfile.json -c mycollection codehooks import -f ./myfile.csv -c mycollection --separator ';' --encoding 'latin1' ``` --- ### export Export JSON or CSV data **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Project name [string] [required] | | `-s, --space` | a space name in your project [string] | | `--collection` | A collection in the space [string] [required] | | `-f, --filepath` | Filename to save export data [string] | | `--csv` | Export to CSV [boolean] | | `--jsonl` | Export to JSONL (JSON Lines) [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | --- ### createindex **Aliases:** index, idx, create-index Add field(s) to a query index **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `-c, --collection` | Collection with indexes [required] | | `-i, --index` | Field to index [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks createindex --collection pets --index name codehooks idx pets name codehooks idx pets -i name -i type -i 'price-in-dollar' ``` --- ### dropindex **Aliases:** removeindex, remove-index, rmindex, delete-index Remove field(s) from a query index **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `-c, --collection` | Collection with indexes [required] | | `-i, --index` | Field to remove index for [required] | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks removeindex pets name codehooks removeindex --collection pets --index 'name' --index 'type' ``` --- ### Key-Value Store ### get Retrieve key-value pair(s) from a space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to query | | `-k, --key` | Key to match, or key* to fetch list [default: "*"] | | `--keyspace` | Keyspace to scan | | `--text` | Output info as text line [boolean] | | `--jsonl` | Output as JSON Lines (one JSON object per line) [boolean] | | `--onlycount` | Output only the count of keys [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | **Examples:** ```bash codehooks get 'my-value-one' codehooks get 'my*' codehooks get '*' --keyspace spacex codehooks get 'my*' --text codehooks get 'my*' --jsonl codehooks get 'my*' --onlycount ``` --- ### set Set key-value pair in a space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use [string] | | `-s, --space` | Select which space to use [string] | | `--key` | Key to set [string] [required] | | `--val` | Value to set [string] [required] | | `--keyspace` | Keyspace to use [string] | | `--ttl` | Time to live in millis for value [number] | | `--json` | Output info as JSON [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | **Examples:** ```bash codehooks set 'my-value-one' 'foo' codehooks set 'my-value-two' 'bar' codehooks set 'session-4f51-9bed' 'OK' --keyspace 'spacex' --ttl 60000 ``` --- ### del Delete key-value pair in a space **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to use | | `-s, --space` | Select which space to use | | `--key` | Key to delete [required] | | `--keyspace` | Keyspace to use | | `--json` | Output info as JSON [boolean] | | `--admintoken` | Use admin token authentication (use for CI) | **Examples:** ```bash codehooks del 'my-value-one' codehooks del 'my-value-two' --keyspace 'spacex' ``` --- ### File Management ### file-upload **Aliases:** up, upload Upload files to server **Options:** | Option | Description | |--------|-------------| | `-f, --src` | Path to directory or file [default: "."] | | `-t, --target` | Target directory on server, same as local if not set | | `-e, --ext` | Include files matching the extension [string] | | `--space` | Select which space (environment) to access | | `-p, --projectname` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `--dryrun` | Output files to upload without performing the action [boolean] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks file-upload './static' codehooks file-upload --src './local' --target '/remote' codehooks file-upload --src './local' --target '/remote' --ext 'png' codehooks file-upload --src './local' --target '/remote' --ext 'png|jpg|jpeg|gif' codehooks file-upload afile.txt ``` --- ### file-delete **Aliases:** delete Delete a file from server **Options:** | Option | Description | |--------|-------------| | `-f, --filename` | Delete file with match on absolute path/filename | | `-r, --match` | Delete multiple file that matches regular expression to a file path | | `--space` | Select which space (environment) to access | | `-p, --projectname` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `--dryrun` | Output files to upload without performing the action [boolean] | | `--json` | Output as JSON [boolean] | **Examples:** ```bash codehooks file-delete --filename '/static/myfile.txt' codehooks file-delete --match '/static/*' codehooks file-delete --match '.css$' ``` --- ### file-list **Aliases:** files List files from server **Options:** | Option | Description | |--------|-------------| | `-f, --path` | Path to directory or file [default: "."] | | `--space` | Select which space (environment) to access | | `-p, --project` | Select which project name to use | | `--admintoken` | Use admin token authentication (use for CI) | | `--reverse` | Scan index in reverse order | | `--table` | Output data as formatted table (not JSON) [boolean] | | `--json` | Output data as formatted JSON [boolean] | **Examples:** ```bash codehooks file-list '/static/' ``` --- ### Monitoring & Logs ### log **Aliases:** logs Show system logs for a space. **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project name to use | | `-s, --space` | Select which space to log | | `-t, --tail` | Chop log to n lines [default: 100] | | `-f, --follow` | Keep log stream open | | `-c, --context` | Filter log on: jobhooks, queuehooks, routehooks, datahooks, auth | | `--admintoken` | Use admin token authentication (use for CI) | | `--json` | Output log lines as JSON [boolean] | **Examples:** ```bash codehooks log codehooks log -f codehooks log --tail 10 codehooks log --project 'pets-ff00' --space prod ``` --- ### stats Show usage metrics for project spaces **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `--space` | Only show info for this space [string] | | `--json` | Output info as json [boolean] | --- ### workflow-status **Aliases:** wfstat Visualize workflow data from workflowdata collection **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `-f, --format` | Output format [string] [choices: "json", "markdown", "html", "ascii"] [default: "ascii"] | | `-a, --active` | Show only active (running) workflows [boolean] [default: false] | | `-i, --detailed` | Show detailed Instance Status table (default is summary only) [boolean] [default: false] | | `-v, --verbose` | Show version number [boolean] [default: false] | | `--follow` | Continuously update workflow status (Ctrl+C to exit) [boolean] [default: false] | | `--interval` | Refresh interval in milliseconds when using --follow (min: 1000) [number] [default: 2000] | --- ### queue-status **Aliases:** qs, queues Visualize queue status with item counts and workers **Options:** | Option | Description | |--------|-------------| | `-p, --project` | Select which project to query | | `-s, --space` | Select which space to query | | `-f, --format` | Output format [string] [choices: "table", "compact", "json"] [default: "table"] | | `--follow` | Continuously update queue status (Ctrl+C to exit) [boolean] [default: false] | | `--interval` | Refresh interval in milliseconds when using --follow (min: 1000) [number] [default: 2000] | | `--admintoken` | Use admin token authentication (use for CI) | --- ### Security & Access ### add-token Add token to space **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--readonly` | [boolean] [default: false] | | `--description` | [string] [default: ""] | | `--json` | Output as JSON [boolean] | --- ### remove-token Remove token from space **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `-t, --token` | [string] [required] | | `--json` | Output as JSON [boolean] | --- ### set-env Set environment variable for space **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--encrypted` | [boolean] [default: false] | | `--key` | [string] [required] | | `--value` | [string] [required] | | `--json` | Output as JSON [boolean] | --- ### remove-env Remove environment variable from space **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `-k, --key` | [string] [required] | | `--json` | Output as JSON [boolean] | --- ### jwks Set/replace JWKS endpoint for OAuth2 authentication. Set to "" (empty string) to remove. **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--url` | URL of JWKS endpoint (must be https) [string] [required] | | `--json` | Output as JSON [boolean] | --- ### whitelist Add host to whitelist (use to restrict space access) **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--ip` | IP address which should be allowed access to space [string] [required] | | `--json` | Output as JSON [boolean] | --- ### whitelist-remove Remove host from whitelist **Options:** | Option | Description | |--------|-------------| | `--projectname` | Project name [required] | | `--space` | A space in your project [required] | | `--ip` | The IP address to remove from the whitelist. [required] | | `--json` | Output as JSON [boolean] | --- ### Admin Tools ### admin Open the codehooks.io admin account UI at account.codehooks.io --- ### admintokens Show active admintokens for account or team **Options:** | Option | Description | |--------|-------------| | `--json` | Output as JSON [boolean] | | `--team` | Team ID (for non-interactive use with --json, use "personal" for personal account) [string] | --- ### add-admintoken Add admin token to account or team (for use with CI) **Options:** | Option | Description | |--------|-------------| | `--json` | Output as JSON [boolean] | | `--team` | Team ID (for non-interactive use with --json, use "personal" for personal account) [string] | --- ### remove-admintoken Remove admin token from account or team **Options:** | Option | Description | |--------|-------------| | `--json` | Output as JSON [boolean] | | `--team` | Team ID (for non-interactive use with --json, use "personal" for personal account) [string] | | `--token` | Admin token ID to remove (for non-interactive use with --json) [string] | --- ### Shell Integration ### completion Generate command completion script. Just add this to your .bashrc, .bash_profile, .zshrc (or similar) on *nix machines --- ## Additional Resources - **Documentation:** [Overview](/docs/) - **API Reference:** [API Cheat Sheet](/docs/apicheatsheet) - **Examples:** [https://github.com/RestDB/codehooks-io-examples](https://github.com/RestDB/codehooks-io-examples) - **MCP Server:** [https://github.com/RestDB/codehooks-mcp-server](https://github.com/RestDB/codehooks-mcp-server) - **Templates:** [https://github.com/RestDB/codehooks-io-templates](https://github.com/RestDB/codehooks-io-templates) - **Support:** [info@codehooks.io](mailto:info@codehooks.io) ## License MIT License - Copyright © Codehooks AS --- ## Client code examples *(Note: This content contains MDX/JSX code)* import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; Client applications communicate with a Codehooks API and database via a secure REST API. You can interface with Codehooks from most popular programming languages and platforms, such as: cURL, JavaScript, Python, PHP, Java, C#, Kotlin, R and Swift. In the code examples shown below we use `mystore-fafb` as an example Codehooks project name and `dev` as a project data space. Replace this with your own project name, and use a valid API token (`x-apikey`) or a JWT/JWKS integration (`Bearer `). ## Code examples for popular programming languages `GET` documents from the salesorder collection. ```bash curl --location 'https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` `POST` a new document to the salesorder collection. ```bash curl --location 'https://mystore-fafb.api.codehooks.io/dev/salesorder' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "customerID": 1234, "productID": 21435465, "quantity": 3, "payment": "subscription" }' ``` `PATCH` (update) an existing document in the salesorder collection. ```bash curl --location --request PATCH 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "quantity": 2, "payment": "card" }' ``` `DELETE` a document in the salesorder collection. ```bash curl --location --request DELETE 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` `GET` documents from the salesorder collection. ```js var myHeaders = new Headers(); myHeaders.append('x-apikey', '3c932310-3fab-4ba3-8102-b75ba0f05149'); var requestOptions = { method: 'GET', headers: myHeaders, redirect: 'follow', }; fetch( 'https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234', requestOptions ) .then((response) => response.json()) .then((result) => console.log(result)) .catch((error) => console.log('error', error)); ``` `POST` a new document to the salesorder collection. ```js var myHeaders = new Headers(); myHeaders.append('x-apikey', '3c932310-3fab-4ba3-8102-b75ba0f05149'); myHeaders.append('Content-Type', 'application/json'); var raw = JSON.stringify({ customerID: 1234, productID: 21435465, quantity: 3, payment: 'subscription', }); var requestOptions = { method: 'POST', headers: myHeaders, body: raw, redirect: 'follow', }; fetch('https://mystore-fafb.api.codehooks.io/dev/salesorder', requestOptions) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.log('error', error)); ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```js var myHeaders = new Headers(); myHeaders.append('x-apikey', '3c932310-3fab-4ba3-8102-b75ba0f05149'); myHeaders.append('Content-Type', 'application/json'); var raw = JSON.stringify({ quantity: 2, payment: 'card', }); var requestOptions = { method: 'PATCH', headers: myHeaders, body: raw, redirect: 'follow', }; fetch( 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876', requestOptions ) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.log('error', error)); ``` `DELETE` a document in the salesorder collection. ```js var myHeaders = new Headers(); myHeaders.append('x-apikey', '3c932310-3fab-4ba3-8102-b75ba0f05149'); var requestOptions = { method: 'DELETE', headers: myHeaders, redirect: 'follow', }; fetch( 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876', requestOptions ) .then((response) => response.text()) .then((result) => console.log(result)) .catch((error) => console.log('error', error)); ``` `GET` documents from the salesorder collection. ```py title="Using http.client" import requests url = "https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234" payload = {} headers = { 'x-apikey': '3c932310-3fab-4ba3-8102-b75ba0f05149' } response = requests.request("GET", url, headers=headers, data=payload) print(response.text) ``` `POST` a new document to the salesorder collection. ```py import http.client import json conn = http.client.HTTPSConnection("mystore-fafb.api.codehooks.io") payload = json.dumps({ "customerID": 1234, "productID": 21435465, "quantity": 3, "payment": "subscription" }) headers = { 'x-apikey': '3c932310-3fab-4ba3-8102-b75ba0f05149', 'Content-Type': 'application/json' } conn.request("POST", "/dev/salesorder", payload, headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```py import http.client import json conn = http.client.HTTPSConnection("mystore-fafb.api.codehooks.io") payload = json.dumps({ "quantity": 2, "payment": "card" }) headers = { 'x-apikey': '3c932310-3fab-4ba3-8102-b75ba0f05149', 'Content-Type': 'application/json' } conn.request("PATCH", "/dev/salesorder/9876", payload, headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` `DELETE` a document in the salesorder collection. ```py import http.client conn = http.client.HTTPSConnection("mystore-fafb.api.codehooks.io") payload = '' headers = { 'x-apikey': '3c932310-3fab-4ba3-8102-b75ba0f05149' } conn.request("DELETE", "/dev/salesorder/9876", payload, headers) res = conn.getresponse() data = res.read() print(data.decode("utf-8")) ``` `GET` documents from the salesorder collection. ```cs title="Using HttpClient" var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Get, "https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234"); request.Headers.Add("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` `POST` (create) a new document to the salesorder collection. ```cs var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Post, "https://mystore-fafb.api.codehooks.io/dev/salesorder"); request.Headers.Add("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149"); var content = new StringContent("{\n \"customerID\": 1234,\n \"productID\": 21435465,\n \"quantity\": 3,\n \"payment\": \"subscription\"\n}", null, "application/json"); request.Content = content; var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```cs var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Patch, "https://mystore-fafb.api.codehooks.io/dev/salesorder/9876"); request.Headers.Add("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149"); var content = new StringContent("{\n \"quantity\": 2,\n \"payment\": \"card\"\n}", null, "application/json"); request.Content = content; var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` `DELETE` a document in the salesorder collection. ```cs var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Delete, "https://mystore-fafb.api.codehooks.io/dev/salesorder/9876"); request.Headers.Add("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); ``` `GET` documents from the salesorder collection. ```php title="Using Guzzle" '3c932310-3fab-4ba3-8102-b75ba0f05149' ]; $request = new Request('GET', 'https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234', $headers); $res = $client->sendAsync($request)->wait(); echo $res->getBody(); ``` `POST` (create) a new document to the salesorder collection. ```php '3c932310-3fab-4ba3-8102-b75ba0f05149', 'Content-Type' => 'application/json' ]; $body = '{ "customerID": 1234, "productID": 21435465, "quantity": 3, "payment": "subscription" }'; $request = new Request('POST', 'https://mystore-fafb.api.codehooks.io/dev/salesorder', $headers, $body); $res = $client->sendAsync($request)->wait(); echo $res->getBody(); ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```php '3c932310-3fab-4ba3-8102-b75ba0f05149', 'Content-Type' => 'application/json' ]; $body = '{ "quantity": 2, "payment": "card" }'; $request = new Request('PATCH', 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876', $headers, $body); $res = $client->sendAsync($request)->wait(); echo $res->getBody(); ``` `DELETE` a document in the salesorder collection. ```php '3c932310-3fab-4ba3-8102-b75ba0f05149' ]; $request = new Request('DELETE', 'https://mystore-fafb.api.codehooks.io/dev/salesorder/9876', $headers); $res = $client->sendAsync($request)->wait(); echo $res->getBody(); ``` `GET` documents from the salesorder collection. ```java title="Using Unirest" Unirest.setTimeouts(0, 0); HttpResponse response = Unirest.get("https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234") .header("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .asString(); ``` `POST` (create) a new document to the salesorder collection. ```java Unirest.setTimeouts(0, 0); HttpResponse response = Unirest.post("https://mystore-fafb.api.codehooks.io/dev/salesorder") .header("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .header("Content-Type", "application/json") .body("{\n \"customerID\": 1234,\n \"productID\": 21435465,\n \"quantity\": 3,\n \"payment\": \"subscription\"\n}") .asString(); ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```java Unirest.setTimeouts(0, 0); HttpResponse response = Unirest.patch("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876") .header("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .header("Content-Type", "application/json") .body("{\n \"quantity\": 2,\n \"payment\": \"card\"\n}") .asString(); ``` `DELETE` a document in the salesorder collection. ```java Unirest.setTimeouts(0, 0); HttpResponse response = Unirest.delete("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876") .header("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .asString(); ``` `GET` documents from the salesorder collection. ```swift title="Using URLSession" var request = URLRequest(url: URL(string: "https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234")!,timeoutInterval: Double.infinity) request.addValue("3c932310-3fab-4ba3-8102-b75ba0f05149", forHTTPHeaderField: "x-apikey") request.httpMethod = "GET" let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() ``` `POST` (create) a new document to the salesorder collection. ```swift let parameters = "{\n \"customerID\": 1234,\n \"productID\": 21435465,\n \"quantity\": 3,\n \"payment\": \"subscription\"\n}" let postData = parameters.data(using: .utf8) var request = URLRequest(url: URL(string: "https://mystore-fafb.api.codehooks.io/dev/salesorder")!,timeoutInterval: Double.infinity) request.addValue("3c932310-3fab-4ba3-8102-b75ba0f05149", forHTTPHeaderField: "x-apikey") request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" request.httpBody = postData let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```swift let parameters = "{\n \"quantity\": 2,\n \"payment\": \"card\"\n}" let postData = parameters.data(using: .utf8) var request = URLRequest(url: URL(string: "https://mystore-fafb.api.codehooks.io/dev/salesorder/9876")!,timeoutInterval: Double.infinity) request.addValue("3c932310-3fab-4ba3-8102-b75ba0f05149", forHTTPHeaderField: "x-apikey") request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.httpMethod = "PATCH" request.httpBody = postData let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() ``` `DELETE` a document in the salesorder collection. ```swift var request = URLRequest(url: URL(string: "https://mystore-fafb.api.codehooks.io/dev/salesorder/9876")!,timeoutInterval: Double.infinity) request.addValue("3c932310-3fab-4ba3-8102-b75ba0f05149", forHTTPHeaderField: "x-apikey") request.httpMethod = "DELETE" let task = URLSession.shared.dataTask(with: request) { data, response, error in guard let data = data else { print(String(describing: error)) return } print(String(data: data, encoding: .utf8)!) } task.resume() ``` `GET` documents from the salesorder collection. ```kotlin title="Using Okhttp" val client = OkHttpClient() val request = Request.Builder() .url("https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234") .addHeader("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .build() val response = client.newCall(request).execute() ``` `POST` (create) a new document to the salesorder collection. ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = "{\n \"customerID\": 1234,\n \"productID\": 21435465,\n \"quantity\": 3,\n \"payment\": \"subscription\"\n}".toRequestBody(mediaType) val request = Request.Builder() .url("https://mystore-fafb.api.codehooks.io/dev/salesorder") .post(body) .addHeader("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```kotlin val client = OkHttpClient() val mediaType = "application/json".toMediaType() val body = "{\n \"quantity\": 2,\n \"payment\": \"card\"\n}".toRequestBody(mediaType) val request = Request.Builder() .url("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876") .patch(body) .addHeader("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .addHeader("Content-Type", "application/json") .build() val response = client.newCall(request).execute() ``` `DELETE` a document in the salesorder collection. ```kotlin val client = OkHttpClient() val mediaType = "text/plain".toMediaType() val body = "".toRequestBody(mediaType) val request = Request.Builder() .url("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876") .method("DELETE", body) .addHeader("x-apikey", "3c932310-3fab-4ba3-8102-b75ba0f05149") .build() val response = client.newCall(request).execute() ``` `GET` documents from the salesorder collection. ```r title="Using RCurl" library(RCurl) headers = c( "x-apikey" = "3c932310-3fab-4ba3-8102-b75ba0f05149" ) res <- getURL("https://mystore-fafb.api.codehooks.io/dev/salesorder?customerID=1234", .opts=list(httpheader = headers, followlocation = TRUE)) cat(res) ``` `POST` (create) a new document to the salesorder collection. ```r library(RCurl) headers = c( "x-apikey" = "3c932310-3fab-4ba3-8102-b75ba0f05149", "Content-Type" = "application/json" ) params = "{ \"customerID\": 1234, \"productID\": 21435465, \"quantity\": 3, \"payment\": \"subscription\" }" res <- postForm("https://mystore-fafb.api.codehooks.io/dev/salesorder", .opts=list(postfields = params, httpheader = headers, followlocation = TRUE), style = "httppost") cat(res) ``` `PATCH` (update) an existing document (9876) in the salesorder collection. ```r library(RCurl) headers = c( "x-apikey" = "3c932310-3fab-4ba3-8102-b75ba0f05149", "Content-Type" = "application/json" ) params = "{ \"quantity\": 2, \"payment\": \"card\" }" res <- getURLContent("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876", customrequest = "PATCH", postfields = params, httpheader = headers, followlocation = TRUE) cat(res) ``` `DELETE` a document in the salesorder collection. ```r library(RCurl) headers = c( "x-apikey" = "3c932310-3fab-4ba3-8102-b75ba0f05149" ) res <- httpDELETE("https://mystore-fafb.api.codehooks.io/dev/salesorder/9876", httpheader = headers, followlocation = TRUE) cat(res) ``` ## Links for further reading - [Database REST API](/docs/database-rest-api) - [Database query language](/docs/nosql-database-query-language) - [Serverless function NoSQL Database API](/docs/nosql-database-api) --- ## Concepts overview *(Note: This content contains MDX/JSX code)* Codehooks.io is the agent-native backend platform. A complete backend — API routes, database, queues, workers, cron, auth, and frontend hosting — that deploys in seconds from the CLI. Your coding agent creates, deploys, verifies, and iterates without ever leaving the terminal. Forget about assembling Lambda + API Gateway + DynamoDB + SQS — just focus on your business logic. Codehooks includes these main features: - **Studio** - Developer friendly web based tools for managing both data and code - **Node.js and JavaScript** - Same language for frontend and backend development (ES6 and Typescript) - **NPM** - Node.js package manager (npm) integration - **Node.js Express** - API development with Express-like API - **NoSQL** - Document database with Mongodb-like API - **Key-Value** - Key-Value database with a Redis-like API (subset) - **Worker Queues** - Persistent queues with worker functions - **Background jobs** - Cron jobs with scheduled worker functions - **CLI** - Powerful Command Line Interface (CLI) for productivity and DevOps/CI support Read on to learn more about how Codehooks.io simplifies backend development — whether you're building directly or through a coding agent. > Unless you've already have, please read the [quick start](/docs) first to learn how to install the CLI, and how to sign up/login to create your first Codehooks project. ## Codehooks main concepts ### Project A project is the top level concept in Codehooks. A project is owned by an account (private or company) and contains one or multiple "spaces" with your deployed code, data and access credentials in an isolated and secure unit. You can create multiple projects within your account and you can join/invite others to a project. An example project structure is shown below: - jane@example.com (private or corporate account owner) - **customers** (project name) - **dev** (development space) - Database instance - Security settings (API tokens, environment secrets, IP filters ...) - Source code (deployed version 1.1.12) - Developers (joe@example.com, pete@example.com) - **prod** (production space) - Database instance - Security settings (API tokens, environment secrets, IP filters ...) - Source code (deployed version 1.1.0) - Developers (joe@example.com, pete@example.com) - **salesdb** (project name for another project) - ... Projects are created with the CLI command `coho create` ([CLI docs here](cli#create)) :::info note You can also manage your projects and spaces using the account user interface at [https://account.codehooks.io](https://account.codehooks.io). ::: ### The project application source code A Codehooks application follows the familiar principles and best practices of modern JavaScript development. A project directory typically contains: - An `index.js` file for the application main entry code - A `package.json` file with library dependencies - A `config.json` file with project information We also recommend that you add source control (GIT) to manage your versions and branches. Typically this will be a git repo with branches that are deployed to various spaces. ### A minimal example The following CLI commands create a project and deploy it to the Codehooks cloud. These are the same commands a coding agent like Claude Code or Cursor would run autonomously. Create and setup a new project application: ```bash coho create example cd example npm init --yes npm install codehooks-js --save ``` After running the commands your project directory contains these files: ```bash . ├── config.json ├── index.js ├── node_modules ├── package-lock.json └── package.json ``` If you inspect the source file index.js you'll see the default generated application code: ```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(); ``` We can now deploy the application (in our case example-4g9p) to the cloud with the CLI command `coho deploy` ([CLI docs here](cli#deploy)). ```bash coho deploy # Server output example Project: example-4g9p Space: dev Deployed Codehook successfully! 🙌 ``` After deployment we can inspect the application details with the `coho info` command, in this example showing our default space `dev` with its base endpoint URL and access token(s): ```bash coho info --examples Project name: example-4g9p Team: YOUR-NAME (personal account) API endpoint: https://example-4g9p.api.codehooks.io/dev/* Spaces: ┌──────────────┬────────────────────────────────────────────┬──────┬──────┐ │ Name │ Tokens │ Jwks │ Env │ ├──────────────┼────────────────────────────────────────────┼──────┼──────┤ │ dev (active) │ a77926ca-xxx-yyyy-zzzzz-14eee564f8d5 (RW) │ │ │ └──────────────┴────────────────────────────────────────────┴──────┴──────┘ ``` After successful deployment we can test our application endpoint with curl. This minimal example and test shows that our application is deployed to a secure endpoint and returns the expected result: ```bash curl -X GET 'https://example-4g9p.api.codehooks.io/dev' \ -H 'x-apikey: a77926ca-xxx-yyyy-zzzzz-14eee564f8d5' # API output example from server CRUD server ready ``` ### Project spaces A project space is a self-contained and isolated bundle of code, datastores and security settings within a specific project. All projects have a default space called `dev` which is created automatically when you add a project. You create new spaces with the `coho add` command ([CLI docs here](cli#add)). You can easily switch between different project spaces with the `coho use` command ([CLI docs here](cli#use)). Spaces can also have restricted access (team ADMINs only), which is convenient for production deployments. For example, in your project you want to have isolated development, testing and production spaces. ``` Project-X └── dev    ├── source code (git branch dev)    ├── security settings & env variables    └── data stores └── test    ├── source code (git branch test)    ├── security settings & env variables    └── data stores └── prod (restricted)    ├── source code (git branch prod)    ├── security settings & env variables    └── data stores ``` ### Deploy your application to the Codehooks serverless cloud Each project space is created as an isolated resource group in the Codehooks cloud. For example, the `coho add prod` command creates a project space called `prod`. By switching to the space with the `coho use prod` command, this space is now the active (changed in the config.json project file). On deployment with the `coho deploy` command, the current version of your application source code is packed and distributed as a serverless runtime to the **prod** space in the Codehook cloud. Similarily, switching to another space, e.g. by using the `coho use dev`, sets the **dev** space as the active, and `coho deploy` will deploy the application to the active **dev** space. This is a powerful feature, which effectively enables you to run multiple versions of your application with separate settings, environments and data stores. ### Built in Datastores Persisting, querying and manipulating data is essential in any application. Codehooks comes with a built in datastore that supports **streaming** NoSQL and Key-Value operations. No need to think about drivers and protocols, it's built directly into the Codehooks APIs. - NoSQL object data are accessed with the standard [Database API](nosql-database-api). - Key-Value data are accessed with the standard [Key-Value API](key-value-database-api) #### Data wrangling Using the CLI command `coho import`, you can easily import any CSV or JSON file into a datastore. Furthermore, the `coho query` command lets you query and transform data in advanced ways. ```bash title="Example: import and query of Stock data" coho import -c stocks -f ~/Downloads/all_stocks_5yr.csv coho query stocks --query 'Name=GOOG&open>10' --limit 2 --table ┌───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┐ │ date │ open │ high │ low │ close │ volume │ Name │ _id │ ├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 2013-03-… │ 15.98 │ 16.36 │ 15.93 │ 16.25 │ 8383300 │ GOOG │ 18007b5f… │ ├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 2013-03-… │ 14.7 │ 14.93 │ 14.5 │ 14.82 │ 9125300 │ GOOG │ 18007b5f… │ └───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┘ ``` ### Fast development flow The development process with Codehooks is fast — whether you're working in an editor or your coding agent is driving the CLI. A typical development flow: 1. Write or generate code in `index.js` and other source files 2. Set the active space, e.g. `coho use dev` 3. Deploy with `coho deploy` 4. Check logs with `coho log --follow` 5. Fix issues and redeploy with `coho deploy` Every step is a CLI command, which means coding agents can run the full create → deploy → verify → iterate loop without human intervention. ### Command Line Interface (CLI) The Codehooks CLI is how you — or your coding agent — manage projects and applications. Every operation is available as a CLI command: creating projects, deploying code, managing secrets, querying data, and checking logs. This CLI-first design is what makes Codehooks agent-native. Codehooks also has a complete set of features for teams to collaborate. The [account UI](https://account.codehooks.io) lets you create teams and invite team members. Read more about the CLI and the full set of commands and features [here](cli). :::info note You can also manage your projects and spaces using the account user interface at [https://account.codehooks.io](https://account.codehooks.io). ::: --- ## REST API query and Database CRUD API *(Note: This content contains MDX/JSX code)* A new Codehooks.io application has a complete and secure REST API for basic database CRUD (Create, Read, Update, Delete) operations using REST API queries. The CRUD REST API is implemented by bundling the deployed application with the NPM package [codehooks-js](https://www.npmjs.com/package/codehooks-js) and the [crudlify API](/docs/appeventapi#appcrudlifyschema-options). When you create a new project a default auto generated application is deployed, this application implements a full database CRUD REST API with built-in NoSQL query functionality. The code for this is shown below. ```js import { app } from 'codehooks-js'; app.crudlify(); // REST CRUD API export default app.init(); ``` All database URL endpoints are prefixed with the project ID, Codehooks API endpoint, and the database space name. For example, if your project ID is `myproject` and the database space is `dev`, then the full URL endpoint for the REST API is: `https://myproject-ff00.api.codehooks.io/dev/people?name=Ally`. ```mermaid graph TD A[REST API] --> B[CRUD Operations] B --> C[Create=POST] B --> D[Read/query=GET] B --> E[Update=PUT/PATCH] B --> F[Delete=DELETE] C --> G[(Database)] D --> G[(Database)] E --> G[(Database)] F --> G[(Database)] accTitle: Database REST API query and CRUD accDescr: You use the REST API to update the database using CRUD operations and REST API query ``` ## Overview of Database REST API operations and queries | API description | HTTP verb | Route | | :------------------------------------------------------------------------ | --------- | --------------------------- | | [Get list of documents](#get-list-of-documents) | `GET` | `/:collection` | | [Get documents by query](#get-documents-by-query) | `GET` | `/:collection?query` | | [Get document by ID](#get-document-by-id) | `GET` | `/:collection/:ID` | | [Create a new document](#create-a-new-document) | `POST` | `/:collection` | | [Update a document by ID](#update-a-document-by-id) | `PATCH` | `/:collection/:ID` | | [Replace a document by ID](#replace-a-document-by-id) | `PUT` | `/:collection/:ID` | | [Delete a document by ID](#delete-a-document-by-id) | `DELETE` | `/:collection/:ID` | | [Update multiple documents by query](#update-multiple-documents-by-query) | `PATCH` | `/:collection/:ID/_byquery` | | [Delete multiple documents by query](#delete-multiple-documents-by-query) | `DELETE` | `/:collection/:ID/_byquery` | :::note Database collections Create and manage collections with the Codehooks CLI command [`coho createcollection`](/docs/cli#createcollection) or using the Codehooks Studio [➕ Create collection] menu. ::: :::note Security All databases are protected with encrypted HTTPS protocol, secure API tokens or JWT tokens. You can also add IP filters and much more, read more about [Authentication here](/docs/authentication). **API tokens**: List your database API tokens with the CLI command [`coho info`](/docs/cli.md#info). Create new API tokens with the CLI command [`coho add-token`](/docs/cli.md#add-token). ::: ## Database REST API ### Get list of documents Retrieve all collection data. **URL:** `/:collection[?options]` **Method:** `GET` **Parameters:** - **collection**: a valid collection name, e.g. `people` **Options:** - **limit**: limit result, e.g. `?limit=2` - **offset**: skip forward in data result set - **fields**: comma separated list of fields to show, e.g. `?limit=2&fields=Last Name,Sex` - **sort**: comma separated list of fields to sort result by, e.g. `?limit=2&sort=-Sex,Last Name` **Returns** Array of JSON documents **Code example** ```bash curl 'https://myproject-ff00.api.codehooks.io/dev/people?limit=2' \ -H 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json [ { "Index": 54901, "User Id": "d088FCEC6EDEF20", "First Name": "Michaela", "Last Name": "Callahan", "Sex": "Male", "Email": "yesenia31@example.net", "Phone": "108-950-4850x6836", "Date of birth": "2010-11-07", "Job Title": "Research scientist (maths)", "_id": "64bb88b51ed9a3057ac99d9c" }, { "Index": 54902, "User Id": "40Fd72E5AaC5b67", "First Name": "Lacey", "Last Name": "Saunders", "Sex": "Male", "Email": "vcarey@example.net", "Phone": "623.506.2528x932", "Date of birth": "1946-06-28", "Job Title": "Insurance claims handler", "_id": "64bb88b51ed9a3057ac99d9d" } ] ``` **Error response** `401 no access` ```html 401 no access ``` ### REST API query - get documents by query Retrieve collection data filtered by a query parameter. :::tip For a complete overview of the NoSQL query language and how to perform the REST API queries, check out the [NoSQL and REST API Query language](nosql-database-query-language) documentation. ::: **URL:** `/:collection[?query&options]` **Method:** `GET` **Parameters:** - **collection**: a valid collection name, e.g. `people` **Query:** - Simple REST API queries using URL parameters: - **name=value** - e.g. `?First Name=Michaela` - Advanced MongoDB query using URL JSON: - **q=\{\.\.\.\}** - e.g. `?q={"First Name": {"$regex": "en"}, "Last Name": {"$in": ["Saunders", "Massey"]}}` **Options:** - **limit**: limit result, e.g. `?limit=2` - **offset**: skip forward in data result set - **fields**: comma separated list of fields to show, e.g. `?limit=2&fields=Last Name,Sex` - **sort**: comma separated list of fields to sort result by, e.g. `?limit=2&sort=-Sex,Last Name` **Returns** Array of JSON documents **Code example:** simple query ```bash curl --location 'https://myproject-ff00.api.codehooks.io/dev/people?First%20Name=Michaela&fields=First%20Name%2CEmail%2C_id' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json [ { "First Name": "Michaela", "Email": "yesenia31@example.net", "_id": "64bb88b51ed9a3057ac99d9c" } ] ``` **Code example:** advanced query :::tip URL encoding Notice that advanced query parameter `q={...}` should be programmatically URL encoded before sent to the server. E.g. the query: ```js let query = { 'First Name': { $regex: 'en' }, 'Last Name': { $in: ['Saunders', 'Massey'] }, }; ``` For example, you can use JavaScript's `query = encodeURIComponent(JSON.stringify(query))`: Then the encoded query equals: ```html {%22First%20Name%22%3A{%22%24regex%22%3A%22en%22}%2C%22Last%20Name%22%3A{%22%24in%22%3A[%22Saunders%22%2C%22Massey%22]}} ``` ::: ```bash curl --location --globoff 'https://myproject-ff00.api.codehooks.io/dev/people?q={%22First%20Name%22%3A{%22%24regex%22%3A%22en%22}%2C%22Last%20Name%22%3A{%22%24in%22%3A[%22Saunders%22%2C%22Massey%22]}}' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json [ { "Index": 54907, "User Id": "18E57F5a6aaC1ab", "First Name": "Darlene", "Last Name": "Saunders", "Sex": "Female", "Email": "noah55@example.net", "Phone": "001-158-700-3226", "Date of birth": "1970-08-23", "Job Title": "Scientist, forensic", "_id": "64bb88b51ed9a3057ac99da2" }, { "Index": 54933, "User Id": "aeD915eA429Fb02", "First Name": "Lauren", "Last Name": "Massey", "Sex": "Male", "Email": "stuart55@example.net", "Phone": "+1-716-581-5746x2442", "Date of birth": "2001-10-21", "Job Title": "Information systems manager", "_id": "64bb88b51ed9a3057ac99dbc" } ] ``` **Error response** `401 no access` ```html 401 no access ``` **Error response** `400 Bad Request` ```html Unexpected token } in JSON at position 5 ``` ### Get document by ID Retrieve a document from a collection. **URL:** `/:collection/:ID` **Method:** `GET` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **ID**: an existing document `_id` value **Returns** A JSON document **Code example** ```bash curl --location 'https://myproject-ff00.api.codehooks.io/dev/people/64bb88b51ed9a3057ac99d9c' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json { "Index": 54901, "User Id": "d088FCEC6EDEF20", "First Name": "Michaela", "Last Name": "Callahan", "Sex": "Male", "Email": "yesenia31@example.net", "Phone": "108-950-4850x6836", "Date of birth": "2010-11-07", "Job Title": "Research scientist (maths)", "_id": "64bb88b51ed9a3057ac99d9c" } ``` **Error response** `401 no access` ``` 401 no access ``` **Error response** `404 Not Found` ``` 3 INVALID_ARGUMENT: NotFoundError: Key not found in database [64bb88b51ed9a3057ac99d9cc] ``` ### Create a new document Insert a new JSON document in a collection. **URL:** `/:collection` **Method:** `POST` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **body**: a valid JSON document **Returns** The created JSON document **Code example** ```bash curl --location 'https://myproject-ff00.api.codehooks.io/dev/people' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data-raw '{ "First Name": "Jim", "Last Name": "Callahan", "Email": "jim@example.net", "Phone": "108-950-4850x6836", "Date of birth": "1995-11-07", "Job Title": "Software tester" }' ``` **Success response** `201 Created` ```js { "First Name": "Jim", "Last Name": "Callahan", "Email": "jim@example.net", "Phone": "108-950-4850x6836", "Date of birth": "1995-11-07", "Job Title": "Software tester", "_id": "64bbc861f13609074a5d981a" } ``` **Error response** `401 no access` ``` 401 no access ``` **Error response** `400 Bad Request` E.g. error when POST an invalid JSON document or a JSON-schema `schemaError` error occurs. ``` { "schemaError": [ { "instancePath": "", "schemaPath": "#/required", "keyword": "required", "params": { "missingProperty": "firstName" }, "message": "must have required property 'firstName'" } ] } ``` ### Update a document by ID Update a JSON document in a collection. **URL:** `/:collection/:ID` **Method:** `PATCH` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **ID**: a valid document `_id` - **body**: a valid JSON document **Returns** The updated JSON document **Code example** ```bash curl --location --request PATCH 'https://myproject-ff00.api.codehooks.io/dev/people/64bbc861f13609074a5d981a' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "Phone": "123-345-12332x6836", "Job Title": "Bug master" }' ``` **Success response** `200 OK` ```js { "First Name": "Jim", "Last Name": "Callahan", "Email": "jim@example.net", "Phone": "123-345-12332x6836", "Date of birth": "1995-11-07", "Job Title": "Bug master", "_id": "64bbc861f13609074a5d981a" } ``` **Error response** `401 no access` ``` 401 no access ``` **Error response** `400 Bad Request` E.g. invalid JSON document or `schemaError`. ``` { "schemaError": [ ... ] } ``` ### Replace a document by ID Replace a JSON document in a collection. **URL:** `/:collection/:ID` **Method:** `PUT` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **ID**: a valid document `_id` - **body**: a valid JSON document **Returns** The replaced JSON document **Code example** ```bash curl --location --request PUT 'https://myproject-ff00.api.codehooks.io/dev/people/64bbc861f13609074a5d981a' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "First Name": "Re", "Last Name": "Placed", "Job Title": "Replacer" }' ``` **Success response** `200 OK` ```json { "First Name": "Re", "Last Name": "Placed", "Job Title": "Replacer", "_id": "64bbc861f13609074a5d981a" } ``` **Error response** `401 no access` ``` 401 no access ``` E.g. invalid JSON document or `schemaError`. ``` { "schemaError": [ ... ] } ``` ### Delete a document by ID Delete a JSON document in a collection. **URL:** `/:collection/:ID` **Method:** `DELETE` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **ID**: a valid document `_id` **Returns** The deleted document \_id **Code example** ```bash curl --location --request DELETE 'https://myproject-ff00.api.codehooks.io/dev/people/64bbc861f13609074a5d981a' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json { "_id": "64bbc861f13609074a5d981a" } ``` **Error response** `401 no access` ``` 401 no access ``` ### Update multiple documents by query Update multiple JSON document by query in a collection. **URL:** `/:collection/_byquery[?query&options]` **Method:** `PATCH` **Parameters:** - **collection**: a valid collection name, e.g. `people` - **body**: a valid JSON document to update on all matches **Query:** - Simple REST API queries using URL parameters: - **name=value** - e.g. `?First Name=Michaela` - Advanced MongoDB query using URL JSON: - **q=\{\.\.\.\}** - e.g. `?q={"First Name": {"$regex": "en"}, "Last Name": {"$in": ["Saunders", "Massey"]}}` **Returns** Count of updated documents **Code example** ```bash curl --location --request PATCH 'https://myproject-ff00.api.codehooks.io/dev/people/_byquery?Job%20Title=%22Scientist%2C%20forensic%22' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' \ --header 'Content-Type: application/json' \ --data '{ "$set": { "salary": "high" }, "$inc": { "visited": 1 } }' ``` **Success response** `200 OK` ```json { "count": 2 } ``` **Error response** `401 no access` ``` 401 no access ``` **Error response** `400 Bad Request` E.g. invalid JSON document or `schemaError`. ``` { "schemaError": [ ... ] } ``` ### Delete multiple documents by query Delete multiple JSON document by query in a collection. :::warning Erases all matched documents by query Please ensure that the applied query is correct. An empty query parameter will erase all documents in collection. ::: **URL:** `/:collection/_byquery[?query&options]` **Method:** `DELETE` **Parameters:** - **collection**: a valid collection name, e.g. `people` **Query:** - Simple REST API queries using URL parameters: - **name=value** - e.g. `?First Name=Michaela` - Advanced MongoDB query using URL JSON: - **q=\{\.\.\.\}** - e.g. `?q={"First Name": {"$regex": "en"}, "Last Name": {"$in": ["Saunders", "Massey"]}}` **Returns** Count of deleted documents **Code example** ```bash curl --location --request DELETE 'http://myproject-ff00.api.codehooks.io/dev/people/_byquery?Job%20Title=%22Scientist%2C%20forensic%22' \ --header 'x-apikey: 3c932310-3fab-4ba3-8102-b75ba0f05149' ``` **Success response** `200 OK` ```json { "count": 2 } ``` **Error response** `401 no access` ``` 401 no access ``` ## Data Schema support We recommend to add JSON-schema 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. ![json-schema](./images/studio/json-schema.webp) However, you can also take full control over the data validation in your code using these popular technologies described in the following sections. Codehooks supports these popular data and validation schemas: - [Yup](https://www.npmjs.com/package/yup) - Dead simple Object schema validation - [Zod](https://www.npmjs.com/package/zod) - TypeScript-first schema validation with static type inference - [JSON.schema](https://www.npmjs.com/package/z-schema) - Standard declarative language that allows you to annotate and validate JSON documents. ### Data validation with Yup The code example below shows how easy it is to validate data with a [Yup](https://www.npmjs.com/package/yup) data schema. Run `npm i yup` in project folder. ```js import { app } from 'codehooks-js'; import { object, string, number, date } from 'yup'; // database schema const userSchema = object({ name: string().required(), age: number().required().positive().integer(), email: string().email(), website: string().url().nullable(), createdOn: date().default(() => new Date()), }); app.crudlify({ user: userSchema }); export default app.init(); // export app to a runtime server engine ``` ### Data validation with JSON-schema The next code example uses JSON-schema to validate data. Run `npm i z-schema` in project folder. ```js import { app } from 'codehooks-js'; import ZSchema from 'z-schema'; const userSchema = { id: 'personDetails', type: 'object', properties: { firstName: { type: 'string' }, lastName: { type: 'string' }, }, required: ['firstName', 'lastName'], }; app.crudlify({ user: userSchema }); // bind to serverless runtime export default app.init(); ``` ### Data validation with Zod Zod is another popular data validation library. Run `npm i zod` in project folder. A simple code example using Zod is show below. ```js import { app } from 'codehooks-js'; // Standard Codehooks.io lib import { z } from 'zod'; const userSchema = z .object({ username: z.string(), email: z.string().email(), status: z.boolean().default(true), }) .required({ username: true, email: true }); app.crudlify({ user: userSchema }); export default app.init(); // Bind functions to the serverless cloud ``` ## Database event hooks middleware To provide additional CRUD logic, events are triggered before and after a database operation. ```js hooks.before(, handlerFunction) hooks.after(, handlerFunction) ``` Example event hooks are shown in the code example below. ```js ... // collapsed code app.crudlify({user: userSchemaYup}, options).then((hooks) => { hooks.beforePOST('user', async (data) => { console.log("User data before saving", data) // abort operation with a throw, cases 404 status code // E.g. throw new Error(`BAD post for ${data}`) // mutate data before saved to the database data.foo = 'Was here!' }) hooks.afterPOST('user', async (data) => { console.log("User data after saved to the database", data) }) }) ... // collapsed code ``` ## Examples ### Insert a new `user` to the database POST a new user using curl. ```bash curl -X POST \ 'https://myproject-fef0.api.codehooks.io/dev/user' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Ally", "email": "ally@example.com" }' ``` ### Validate data against a Yup data schema Check that the data schema validates correctly by sending an invalid email address. ```bash curl -X POST \ 'https://myproject-fef0.api.codehooks.io/dev/user' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Sally", "email": "sally.example.com" }' ``` Validation error shows that Yup works. ```bash 400 Bad Request { "value": { "active": true, "email": "sally.example.com", "name": "Sally" }, "path": "email", "type": "email", "errors": [ "email must be a valid email" ], ... # chopped error message } ``` ### Run a database REST API query ```bash curl -X GET \ 'https://myproject-fef0.api.codehooks.io/dev/user?name=Ally' \ --header 'Content-Type: application/json' ``` Example output. ```json [ { "_id": "63fb97825f624f479034eb08", "active": true, "email": "ally@example.com", "name": "Ally" } ] ``` ### Update a record in the database ```bash curl -X PATCH \ 'https://myproject-fef0.api.codehooks.io/dev/user/63fb97825f624f479034eb08' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Ally Mc. Beal" }' ``` ### Querying the database using the REST API You can query the database REST API in two different ways. **Simple** to use and quick queries with the URL: ```url https://myproject-fef0.api.codehooks.io/dev/user?name=jane&age>23&limit=2&offset=5&sort=email&fields=name,age ``` Which actually produces this query object: ```js { name: 'Jane', age: { '$gt': 23 } }, { fields: { name: 1, age: 1 }, sort: { email: 1 }, skip: 5, limit: 2, projection: { name: 1, age: 1 } } ``` For **advanced** use, and programmatic approach, pass inn the full NoSQL/JSON REST API query and hints as URL parameters: ```url https://myproject-fef0.api.codehooks.io/dev/user?q={"name": "Jane", "age": {"$gt": 23}}&h={"fields": { "name": 1, "age": 1 },"sort": {"email": 1 },"skip": 5,"limit": 2,"projection": { "name": 1, "age": 1 }} ``` The last option would probably use `JSON.stringify(query)` etc. to produce a valid query in the URL. --- ## Data operators *(Note: This content contains MDX/JSX code)* import TOCInline from '@theme/TOCInline'; Data objects in a datastore can be manipulated in any update operation. To manipulate one or many data object(s) a range of special operators can be applied using the [`updateOne(...)`](nosql-database-api#updateonecollection-id-document) or [`updateMany(...)`](nosql-database-api#updatemanycollection-document-options) API. **Operators quick overview** ## **$inc** operator Increase or decrease a field value on an existing datastore object. **Syntax**: `{ $inc: { : , : , ... } }` Example: ```js // before {..., "followers": 42} const operator = {$inc: {"followers": 1}} // Call update API, e.g. updateMany or updateOne await conn.updateMany('mycollection', {"status": "ACTIVE"}, operator) // after {..., "followers": 43} ``` 👉 _Use negative values to decrement field._ ## **$push** operator Append an element to a sub array on an existing datastore object. **Syntax**: `{ $push: { : , ... } }` Example: ```js // before {"followers": 42, "comments": ["one", "three"]} const operator = {$push: {"comments": "two"}} // E.g.: await conn.updateOne('customer', ID, operator); // after {"followers": 42, "comments": ["one", "three", "two"]} ``` ## **$pull** operator Remove an element from a sub array on an existing datastore object. Pulls all matches from array. **Syntax**: `{ $pull: { : |, : |, ... } }` Example: ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$pull: {"comments": "two"}} // E.g.: await conn.updateOne('customer', ID, operator); // after {"followers": 42, "comments": ["one", "three"]} ``` ## **$pop** operator Removes last element of a sub array on an existing datastore object. **Syntax**: `{ $pop: { : -1 | 1, ... } }` -1 pop first item of array, 1 pop last item of array. Example: ```js // before {"followers": 42, "comments": ["one", "three", "two"]} const operator = {$pop: {"comments": 1} // E.g.: await conn.updateOne('customer', ID, operator); // after {"followers": 42, "comments": ["one", "three"]} ``` ## **$addToSet** operator Insert an unique element to a sub array on an existing datastore object. Nothing is inserted if item already exists in set. **Syntax**: `{ $addToSet: { : , ... } }` Example: ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$addToSet: {"items": {"name": "one", "price": 1}}} // E.g.: await conn.updateOne('customer', ID, operator); // after { "followers": 42, "comments": ["one", "two", "three"], "items": [{"name": "one", "price": 1}] } ``` ## **$set** operator Set any object value. **Syntax**: `{ $set: { : , ... } }` Example: ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$set: {"address": {"street": "Main street", "ZIP": 1123}}} // E.g.: await conn.updateOne('customer', ID, operator); // after { "followers": 42, "comments": ["one", "two", "three"], "address": {"street": "Main street", "ZIP": 1123} } ``` ## **$unset** operator Delete an object property. **Syntax**: `{ $unset: { : "", ... } }` ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$unset: {"comments": ""}} // E.g.: await conn.updateOne('customer', ID, operator); // after { "followers": 42 } ``` ## **$rename** operator Rename an object property. **Syntax**: `{$rename: { : , : , ... } }` ```js // before {"followers": 42, "comments": ["one", "two", "three"]} const operator = {$rename: {"comments": "claps"}} // E.g.: await conn.updateOne('customer', ID, operator); // after { "followers": 42, "claps": ["one", "two", "three"] } ``` --- ## AWS S3 integration *(Note: This content contains MDX/JSX code)* ![aws-s3](./aws-s3.png) In this example we'll create an API to upload and download binary files to AWS S3 (v3). You can read more in this blog post about [AWS S3 integration](https://dev.to/restdbjones/step-by-step-guide-uploading-and-downloading-binary-files-to-aws-s3-v3-using-nodejs-and-codehooksio-4olh) Also check out the full [example source code here](https://github.com/RestDB/codehooks-io-examples/tree/main/aws-s3). ## Initialise the project Create a new project and install all packages. ```bash coho create mys3project cd mys3project npm init es6 -y npm install codehooks-js npm install @aws-sdk/client-s3 ``` ## Add credentials and secrets Use the CLI or the Admin UI to add your secret environment variables. Note the `--encrypted` flag to prevent reading the actual data content of the variables. ```bash coho set-env AWS_ACCESS_KEY_ID 'YOUR_KEY' --encrypted coho set-env AWS_SECRET_ACCESS_KEY 'YOUR_SECRET' --encrypted coho set-env AWS_BUCKET 'YOUR_BUCKET' --encrypted coho set-env AWS_REGION 'YOUR_REGION' --encrypted ``` ## Example source code The example app shown below creates an API for upload and download of binary files to and from AWS S3. ```js /* * Codehooks (c) AWS S3 example */ import { app } from 'codehooks-js' import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3' import { PassThrough } from 'stream' const { AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_BUCKET } = process.env; const s3config = { region: AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } const s3client = new S3Client(s3config); // public access app.auth('/download/*', (req, res, next) => { if (req.method === 'GET') { next() } else { res.status(403).end('Not public') } }) // API to GET a binary data stream from AWS S3 app.get('/download/:file', async (req, res) => { try { // filename from route const { file } = req.params; const input = { "Bucket": AWS_BUCKET, "Key": `tmp/${decodeURI(file)}` // decode filename and get from /bucket/tmp/file }; // Create get command const command = new GetObjectCommand(input); // Send get command const response = await s3client.send(command); // set content-type res.set('content-type', response.ContentType) // stream data back to client response.Body.pipe(res.writable) } catch (error) { // Woops res.status(400).end(error.message) } }) // API to POST a binary data stream to AWS S3 app.post('/upload/single', async (req, res) => { try { // get size, type and filename from destructured header values const { 'content-length': ContentLength, 'content-type': ContentType, filename } = req.headers; const input = { "Bucket": AWS_BUCKET, "Key": `tmp/${filename}`, // emulate file system /bucketname/tmp/filename "Body": new PassThrough(), // stream to pipe data through "ContentLength": ContentLength, "ContentType": ContentType }; // pipe binary request data to S3 stream req.pipe(input.Body); // create put command const command = new PutObjectCommand(input); // execute put object command const response = await s3client.send(command); // return data to client res.json(response); } catch (error) { // some error occured, return 400 status to client res.status(400).end(error.message) } }) // bind to serverless runtime export default app.init(); ``` ## Testing the API Let's test the API with curl by uploading a test file from a local filesystem. Use `coho info --examples` to inspect your project API endpoint. **Upload a file** ```bash curl --location 'https://.api.codehooks.io/dev/upload/single' \ --header 'x-apikey: XXXXX' \ --header 'filename: myfile.png' \ --header 'Content-Type: image/png' \ --header 'x-apikey: YOUR_API_TOKEN' \ --data '@./myfile.png' ``` **Download a file** ```bash curl --location 'https://.api.codehooks.io/dev/download/myfile.png' \ --output myfile.png ``` --- ## ChatGPT REST API node.js *(Note: This content contains MDX/JSX code)* ![chatgpt codehooks](./chatgpt-codehooks.png) This is an example of a serverless Node.js application that uses codehooks.io to create a REST API for the OpenAI [ChatGPT](https://chat.openai.com) language model. The API allows REST API clients to send text prompts to the ChatGPT model and receive a response in return. The code uses the [codehooks-js](https://www.npmjs.com/package/codehooks-js) library to handle routing and middleware for the API, and the [node-fetch](https://www.npmjs.com/package/node-fetch) library to make HTTP requests to the OpenAI API. The code also uses the built-in codehooks.io Key-Value store to cache the response from the OpenAI API for 60 seconds, in order to reduce the number of requests made to the OpenAI API (If you want to create your own Node.js Express backend, you just need to replace this cache with [Redis](https://redis.io/) for example). Additionally, the code also implements a middleware to rate limit the number of requests coming from the same IP address to 10/min. :::info Documentation - Read more about the [OpenAI API](https://platform.openai.com/docs/api-reference/introduction) - Read more about the [Key-Value store ](/docs/key-value-database-api) ::: ## Create the OpenAI API key Navigate to your [Open AI account settings](https://platform.openai.com/settings/organization/api-keys) and create a new secret key. ![chatgpt-apikey](./chatgpt-apikey.png) ## Create a new project to host the GPT REST API ```bash coho create gptrestapi ``` This will create a new directory for your REST API backend application. ```bash cd gptrestapi ``` ## Copy the source code ```js title="index.js" import { app, Datastore } from 'codehooks-js'; import fetch from 'node-fetch'; // REST API routes app.post('/chat', async (req, res) => { if (!process.env.OPENAI_API_KEY) { return res.status(500).json({ error: 'Missing OPENAI_API_KEY' }); // CLI command: coho set-env OPENAI_API_KEY 'XXX' } const { ask } = req.body; if (!ask) { return res.status(400).json({ error: 'Missing ask parameter' }); } const db = await Datastore.open(); const cacheKey = `chatgpt_cache_${ask}`; // Check cache first const cachedAnswer = await db.get(cacheKey); if (cachedAnswer) { return res.json({ response: cachedAnswer }); } try { // Call OpenAI API const response = await callOpenAiApi(ask); const { choices } = response; const text = choices?.[0]?.message?.content || 'No response'; // Cache response for 1 minute await db.set(cacheKey, text, { ttl: 60 * 1000 }); res.json({ response: text }); } catch (error) { console.error('OpenAI API error:', error); res.status(500).json({ error: 'Failed to get response from OpenAI' }); } }); // Call OpenAI API async function callOpenAiApi(ask) { const requestBody = { model: 'gpt-4-turbo', messages: [ { role: 'system', content: 'You are a helpful AI assistant.' }, { role: 'user', content: ask }, ], temperature: 0.6, max_tokens: 1024, }; const requestOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify(requestBody), }; const response = await fetch( 'https://api.openai.com/v1/chat/completions', requestOptions ); return response.json(); } // Global middleware to IP rate limit traffic app.use(async (req, res, next) => { const db = await Datastore.open(); const ipAddress = req.headers['x-real-ip'] || req.ip; // Increase count for IP const count = await db.incr(`IP_count_${ipAddress}`, 1, { ttl: 60 * 1000 }); console.log('Rate limit:', ipAddress, count); if (count > 10) { return res.status(429).json({ error: 'Too many requests from this IP' }); } next(); }); // Export app to the serverless runtime export default app.init(); ``` Open the `index.js` file and replace the code with the source code. ## Install dependencies ```bash npm install codehooks-js node-fetch ``` ## Create a new secret environment variable Once you have created your Open AI API secret key, you need to add this to the serverless runtime. You can use the [CLI command](/docs/cli.md#set-env) for this. ```bash coho set-env OPENAI_API_KEY 'XXXXXXXX' --encrypted ``` ## Deploy the Node.js ChatGPT REST API to the codehooks.io backend From the project directory run the [CLI command](/docs/cli#deploy) `coho deploy`. The output from my test project is shown below: ```bash coho deploy Project: gptrestapi-pwnj Space: dev Deployed Codehook successfully! 🙌 ``` ## Test your ChatGPT REST API The following curl example demonstrates that we have a live REST API that takes a `POST` body with a text string `ask` and returns the response from the Open AI API. ```bash curl -X POST \ 'https://gptrestapi-pwnj.api.codehooks.io/dev/chat' \ --header 'x-apikey: cb17c77f-2821-489f-a6b4-fb0dae34251b' \ --header 'Content-Type: application/json' \ --data-raw '{ "ask": "Make a great title for a code example with ChatGPT and serverless node.js with codehooks.io" }' ``` Output from the ChatGPT REST API is shown below. ```bash "Building Conversational AI with ChatGPT and Serverless Node.js Using Codehooks.io" ``` Another example shows that we a actually talking to the Open AI API. ```bash curl -X POST \ 'https://gptrestapi-pwnj.api.codehooks.io/dev/chat' \ --header 'x-apikey: cb17c77f-2821-489f-a6b4-fb0dae34251b' \ --header 'Content-Type: application/json' \ --data-raw '{ "ask": "Tell a joke" }' ``` Output from the second example 😂: ```bash Q: What did the fish say when it hit the wall? A: Dam! ``` The source code of this and other examples are also available on our [GitHub example repo](https://github.com/RestDB/codehooks-io-examples/tree/main/chatgpt-rest-api). --- ## CRUD REST API Example with Node.js and codehooks.io | Step-by-Step Tutorial *(Note: This content contains MDX/JSX code)* # CRUD REST API Example - how to build a complete CRUD REST API with Node.js and codehooks.io This comprehensive tutorial shows you how to build a complete CRUD (Create, Read, Update, Delete) REST API from scratch. You'll learn how to implement all essential database operations through a RESTful interface, with working example code that you can run immediately. This CRUD REST API example demonstrates: - Full database CRUD operations (Create, Read, Update, Delete) - RESTful endpoint implementation - Query parameter handling - NoSQL database integration - Error handling - Production-ready code structure The CRUD REST API example lets clients access your data, e.g. - Search database with url query strings, e.g. `GET` `/dev/customer?name=Bill&city=Berlin` - Get object by ID, e.g. `GET` `/dev/customer/17f7990aa6d-w84jerq77u7w3d` - Create new objects from JSON data, e.g. `POST` `/dev/customer` `{JSON}` :::tip **Automate** the creating of a CRUD REST API with the open source library [Crudlify](https://www.npmjs.com/package/codehooks-crudlify). ::: ## Project Structure for the CRUD REST API Example The project has one main JavaScript file ([index.js](#main-function-indexjs)) and four JavaScript library files: [create.js](#create-function), [read.js](#read-function), [update.js](#update-function) and [delete.js](#delete-function). 1. Create a project with the `coho create` CLI command. 2. Change to the project directory and install `npm i query-to-mongo codehooks-js -s`. 3. Delete default index.js file and create new files `touch index.js create.js read.js delete.js update.js` 4. Open code files in your favorite code editor and enter the follwing code. ## CRUD REST API Example: Main function (index.js) The index.js file defines the HTTP routes (routehooks) for the serverless functions. The functions are implemented as separate code files for clarity and better organisation of the project. ```js title=index.js import app from 'codehooks-js'; // import libraries from separate files import { createFunc } from './create'; import { readOneFunc, readManyFunc } from './read'; import { deleteFunc } from './delete'; import { updateFunc } from './update'; // Codehooks API routes for the CRUD REST API example app.post('/:collection', createFunc); app.get('/:collection', readManyFunc); app.get('/:collection/:ID', readOneFunc); app.put('/:collection/:ID', updateFunc); app.delete('/:collection/:ID', deleteFunc); export default app.init(); ``` ## Create function (create.js) ```js title=create.js import { Datastore } from 'codehooks-js'; export async function createFunc(req, res) { const { collection } = req.params; const document = req.body; const conn = await Datastore.open(); const result = await conn.insertOne(collection, document); res.json(result); } ``` ## Read function (read.js) ```js title=read.js // use npm package to convert URL query string to MongoDB query filter const q2m = require('query-to-mongo'); import { Datastore } from 'codehooks-js'; export async function readManyFunc(req, res) { const { collection } = req.params; const conn = await Datastore.open(); const options = { filter: q2m(req.query).criteria, }; conn.getMany(collection, options).json(res); } export async function readOneFunc(req, res) { const { collection, ID } = req.params; const conn = await Datastore.open(); try { const result = await conn.getOne(collection, ID); res.json(result); } catch (e) { res .status(404) // not found .end(e); } } ``` ## Update function (update.js) ```js title=update.js import { Datastore } from 'codehooks-js'; export async function updateFunc(req, res) { const { collection, ID } = req.params; const document = req.body; const conn = await Datastore.open(); const result = await conn.updateOne(collection, ID, document, {}); res.json(result); } ``` ## Delete function (delete.js) ```js title=delete.js import { Datastore } from 'codehooks-js'; export async function deleteFunc(req, res) { const { collection, ID } = req.params; const conn = await Datastore.open(); const result = await conn.removeOne(collection, ID, {}); res.json(result); } ``` ## Deploying the CRUD REST API Example You can deploy the code with this [CLI command](/docs/cli#deploy): ```bash coho deploy ``` ## Testing the deployed CRUD REST API Example Test your deployed CRUD REST API with curl or [Postman](https://getpostman.com) (recommended). :::tip Use the CLI command `coho info` to see API tokens for the project datastore. ::: ### POST ```bash title="Shell command" curl --location --request POST 'https://.api.codehooks.io/dev/customer' \ --header 'x-apikey: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Bill", "email": "william@example.com" }' ``` ```bash title="Example output" {"name":"Billy Bob","email":"william@example.com","_id":"640f60e2b4fdc00015c4280f" ``` ### PUT ```bash title="Shell command" curl --location --request PUT 'https://.api.codehooks.io/dev/customer/640f60e2b4fdc00015c4280f' \ --header 'x-apikey: ' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Billybob" }' ``` ```bash title="Example output" { "name": "Billybob", "email": "william@example.com", "_id": "640f60e2b4fdc00015c4280f" } ``` ### GET (all) ```bash title="Shell command" curl --location --request GET 'https://.api.codehooks.io/dev/customer' \ --header 'x-apikey: ' ``` ```bash title="Example putput" { "name": "Billybob", "email": "william@example.com", "_id": "640f60e2b4fdc00015c4280f" } ... ``` ### GET (by query) ```bash title="Shell command" curl --location --request GET 'https://.api.codehooks.io/dev/customer?email=william@example.com' \ --header 'x-apikey: ' ``` ```bash title="Example putput" { "name": "Billybob", "email": "william@example.com", "_id": "640f60e2b4fdc00015c4280f" } ``` ### DELETE ```bash title="Shell command" curl --location --request DELETE 'https://.api.codehooks.io/dev/customer/640f60e2b4fdc00015c4280f' \ --header 'x-apikey: ' ``` ```bash title="Example putput" { "_id": "640f60e2b4fdc00015c4280f" } ``` --- ## Data import *(Note: This content contains MDX/JSX code)* Codehooks supports import of large data sets from CSV and JSON files. In this example we'll use a dataset from Kaggle machine learning data site. The data contains 5 years of stock trading results with 619040 records. You'll find the dataset here https://www.kaggle.com/datasets/camnugent/sandp500 After downloading the data we use the CLI to import the data file to a collection `stocks`. ```bash title="CLI command" coho import -c stocks -f ~/Downloads/all_stocks_5yr.csv Codehooks data import [==============================] 178673/bps 100% 0.0s Finished import of 619040 objects ``` We can inspect that the dataset is imported correctly with the `coho query` command. ```bash title="CLI command" coho query stocks --limit 2 ``` ```bash title="output" { date: '2013-03-14', open: 15.98, high: 16.36, low: 15.93, close: 16.25, volume: 8383300, Name: 'AAL', _id: '18007b5f8d7-000mnf5l50qd7a' } { date: '2013-03-07', open: 14.7, high: 14.93, low: 14.5, close: 14.82, volume: 9125300, Name: 'AAL', _id: '18007b5f8d7-058mo6lu276dq0' } ``` We can also perform complex queries on datasets. E.g. find all Google trades that closed above $110. ```bash title="CLI command" coho query stocks 'Name=GOOG&close>1100' ``` ```bash title="output" { date: '2018-01-23', open: 1159.85, high: 1171.63, low: 1158.75, close: 1169.97, volume: 1333056, Name: 'GOOG', _id: '640f60e2b4fdc00015c4280f' } { date: '2018-01-31', open: 1170.57, high: 1173, low: 1159.13, close: 1169.94, volume: 1538688, Name: 'GOOG', _id: '640f60f2b4fdc00015c42811' } ... ``` :::tip Use the `coho query` `--table` option to display data in tabular form. Use the `--raw` option to output data for further processing. ::: ```bash title="CLI command" coho query stocks --limit 2 --table ┌───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┬───────────┐ │ date │ open │ high │ low │ close │ volume │ Name │ _id │ ├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 2013-03-… │ 15.98 │ 16.36 │ 15.93 │ 16.25 │ 8383300 │ AAL │ 18007b5f… │ ├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ │ 2013-03-… │ 14.7 │ 14.93 │ 14.5 │ 14.82 │ 9125300 │ AAL │ 18007b5f… │ └───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┴───────────┘ ``` --- ## GitHub example code ![github](github-mark.png) Navigate to the [GitHub examples](https://github.com/RestDB/codehooks-io-examples) repository to learn more from fully working examples and how-to guides. The repository contains example code for various topics [from our blog](/blog), including the following: * Mailgun integration * React * ChatGpt * mongoDB and Express * CRUD Get started by cloning the repo to your local machine. ```bash git clone https://github.com/RestDB/codehooks-io-examples.git ``` --- ## GraphQL API with database *(Note: This content contains MDX/JSX code)* This serverless Codehooks example backend exposes a graphql endpoint for CRUD operations. * You can use [Postman](https://postman.com) to test the API. Postman automatically fetches the GraphQL schema and gives you auto-completion 🤠 * Data is persisted in the NoSQL document database. :::tip Visit [the official GraphQL site](https://www.graphql.com/) to learn more about GraphQL. ::: 1. Create a project with the `coho create` CLI command. 2. Change to the project directory and install using `npm i codehooks-js graphql graphql-tools`. 3. Open the index.js file in your favorite code editor and enter the following code. ## Main file (index.js) The index.js file defines the schema, the resolvers and the single GraphQL route we need. ```js title=index.js import { app, Datastore } from 'codehooks-js'; import { graphql } from 'graphql'; import { makeExecutableSchema } from 'graphql-tools'; // Construct a schema, using GraphQL schema language const typeDefs = ` type Member { id: ID name: String! age: Int active: Boolean } type Query { ping: String member(id: ID!): Member! allMembers: [Member] } type Mutation { addMember(name: String! age:Int active:Boolean): Member! updateMember(id: ID! name: String age:Int active:Boolean): Member! deleteMember(id: ID!): ID } `; // implement resolvers for queries and mutations const resolvers = { Query: { ping: () => { return 'Pong!'; }, member: async (obj, { id }, { db } ) => { const { _id, ...rest } = await db.getOne('members', id); return { id: _id, ...rest }; }, allMembers: async (obj, args, { db }) => { const members = await db.getArray('members'); return members.map(({_id, ...member }) => ({id: _id, ...member })); // map _id to id } }, Mutation: { addMember: async (obj, input, { db }) => { const {_id, ...rest } = await db.insertOne('members', input); return { id: _id, ...rest }; }, updateMember: async(obj, {id, ...data }, { db }) => { return db.updateOne('members', id, data); }, deleteMember: async(obj, {id}, { db }) => { const result = await db.removeOne('members', id); return result._id; }, } } const schema = makeExecutableSchema({ typeDefs, resolvers }) // Create the default graphql POST route app.post('/graphql', async (req, res) => { const conn = await Datastore.open(); const response = await graphql({ schema, source: req.body.query, contextValue: { db: conn } }); res.json(response); }); export default app.init(); // Bind functions to the serverless cloud ``` ## Deployment You can deploy the code with this [CLI command](/docs/cli#deploy): ```bash coho deploy ``` ## Test the deployed GraphQL endpoint Test your deployed code with [Postman using GraphQL support](https://www.postman.com/graphql) (recommended). Postman reads the schema from the codehooks.io `/graphql` endpoint and gives you code completion. :::tip Use the CLI command `coho info` to see the HTTP endpoint and the API tokens for the project space. ::: ## You can also use cURL to use the GraphQL API to add and read data ### Add one member mutation ```bash curl --location --request POST 'https://graphqltest-4pmq.api.codehooks.io/dev/graphql' \ --header 'x-apikey: 66e33428-45d3-4811-be21-58fa6d5d5e91' \ --header 'Content-Type: application/json' \ --data-raw '{"query":"mutation { addMember(name: \"Mr Codehook\", age: 33) { id name }}"}' ``` ### Get all members query ```bash curl --location --request POST 'https://graphqltest-4pmq.api.codehooks.io/dev/graphql' \ --header 'x-apikey: 66e33428-45d3-4811-be21-58fa6d5d5e91' \ --header 'Content-Type: application/json' \ --data-raw '{"query":"{allMembers {id name }}"}' ``` ## Use a local serverless server instead of deploying If you don't want to create an account yet, you can also test your codehooks.io GraphQL backend locally using this command: ```bash coho localserver ``` ## Conclusion We've demonstrated how you can set up a simple serverless GraphQL server using very little JavaScript code. We hope this has inspired you to check out what is possible to do with codehooks.io's compact backend and database. To learn more about creating backends with Node.js and codehooks.io, check out the documentation or the blog --- ## Hello world! The simple serverless backend example *(Note: This content contains MDX/JSX code)* ## 3 simple steps to create your first serverless backend example 1. Create a project with the `coho create` CLI command. 2. Change to the project directory and install `npm i codehooks-js -s`. 3. Open `index.js` in your favorite code editor. ```js title=index.js import app from 'codehooks-js' app.get('/hello', async (req, res) => { console.log("Inside the routehook"); res.json({"message": "Hello world!"}); }); export default app.init(); ``` Deploy the serverless backend example with the `coho deploy` CLI command. Run `coho info --examples` to see your API token and cURL examples. Test API with: ```js curl https://.api.codehooks.io/dev/hello -H 'x-apikey: ' ``` Test the serverless backend example: ```js curl https://mybackendproject-x23a.api.codehooks.io/dev/hello -H 'x-apikey: 6e111ea6-6b3a-412d-8f6b-061c816c67c8' ``` :::tip By clicking the "Import" button in [Postman](https://postman.com) and pasting in the cURL expression, you can test the serverless backend example there. ::: --- ## Mailgun integration example *(Note: This content contains MDX/JSX code)* ![mailgun-example](./maigun-example.png) In this Mailgun integration example we'll show how easy it is to create and deploy a complete serverless backend that integrates with the Mailgun API for sending email messages to your users. [Mailgun](https://mailgun.com) is a SaaS (Software as a Service) email delivery and management platform that allows users to send, receive, and track email communications. Mailgun is often used by developers and businesses to handle bulk email sending and improve email deliverability. Before you begin, you need to create a new Codehooks project, if you don't have one already. ## Create a new serverless backend project ```bash # create a new Codehooks project coho create mailgunexample # navigate into the directory for the new project cd mailgunexample # install dependent npm packages npm i codehooks-js form-data node-fetch --save ``` ## 4 simple steps to integrate with the Mailgun REST API 1. Get your Mailgun API credentials 2. Create secrets in your serverless backend 3. Create the REST API for sending email 4. Deploy your serverless backend ## 1. Get your Mailgun API credentials Navigate to your account security setting [directly here](https://app.mailgun.com/app/account/security/api_keys). Help information [is available here](https://help.mailgun.com/hc/en-us/articles/203380100-Where-Can-I-Find-My-API-Key-and-SMTP-Credentials-). You'll need two secret keys from your Mailgun account: `MAILGUN_APIKEY` and `MAILGUN_DOMAIN` After locating and inspecting your API-key and domain, protect their content by adding them as encrypted secret environment variables to your serverless backend. ## 2. Create secrets in your serverless backend It's easy to [add secrets](/docs/application-secrets) as environment variables using the CLI command `set-env`. The example below shows how to create the two secrets we need to access the Mailgun REST API: ```bash coho set-env "MAILGUN_APIKEY" "XXXXXXXXXXX" --encrypted coho set-env "MAILGUN_DOMAIN" "example.com" --encrypted ``` Once the secrets have been added to the serverless runtime we can access them as regular system variables, e.g `process.env.MAILGUN_APIKEY`. ## 3. Create the REST API for sending email The code example below shows a complete serverless backend application. The application exposes a REST API endpoint with a `POST` function that calls the Mailgun REST API to send an email to the `body.email` address. We also use the built-in [serverless NoSQL database](/docs/nosql-database-api) to store a log of all sent emails. ```js title="index.js" /* * Mailgun integration example. */ import {app, Datastore} from 'codehooks-js'; import FormData from 'form-data'; import fetch from 'node-fetch'; // Mailgun REST API endpoint address const MAILGUN_URL = 'api.eu.mailgun.net'; // or api.mailgun.net for US customers // REST API for sending email to list of recipients app.post('/sendmail', async function (req, res) { // pick from post const {email, name} = req.body; // create an email as form data const form = new FormData(); form.append('from', 'jones@codehooks.io'); form.append('to', email); form.append('subject', 'Testing Mailgun with Codehooks'); form.append('text', `Hello ${name}, hope you are ok. Lets meet soon.`); // Mailgun api endpoint const url = `https://${MAILGUN_URL}/v3/${process.env.MAILGUN_DOMAIN}/messages`; // Mailgun credentials must be base64 encoded for Basic authentication const credentials = Buffer.from(`api:${process.env.MAILGUN_APIKEY}`).toString('base64'); // POST REST API with the email form data const resp = await fetch(url, { method: 'POST', headers: { "Authorization": `Basic ${credentials}` }, body: form }); // handle response errors or OK data if (resp.status <= 201) { // Success, return Mailgun response to the REST API client const output = await resp.json(); console.log("Success", output); // insert log to the NoSQL database const db = await Datastore.open(); const doc = await db.insertOne('maillog', {email, name, output}); return res.status(201).json(output); } else { console.error(rest.status, resp.statusText); // pass the Mailgun error to the REST API client return res.status(resp.status).json({error: resp.statusText}); } }) // bind to serverless runtime export default app.init(); ``` ## 4. Deploy your serverless backend I just love to deploy serverless backends to the Codehooks.io cloud service, it's so easy and instant. From the project directory run the CLI command for deployment: ```bash coho deploy # example output Project: mailgunexample-8jto Space: dev Deployed Codehook successfully! 🙌 ``` The serverless backend API is now live and available for traffic in the cloud. It you run the CLI command [`coho info --examples`](/docs/cli#info) you will see your backend endpoint address and some curl examples to get you started with testing. ## Testing your Mailgun integration [Postman](https://www.postman.com/api-platform/api-testing/), [Thunder](https://marketplace.visualstudio.com/items?itemName=rangav.vscode-thunder-client) or [curl](https://curl.se) are great tools for API development and testing. In this example we'll keep it simple and use curl to test our new serverless API. ```bash curl --location --request POST 'https://mailgunexample-8jto.api.codehooks.io/dev/sendmail' \ --header 'x-apikey: bb00d714-c4f2-4df7-9ecb-ad2ce4b29362' \ --header 'Content-Type: application/json' \ --data-raw '{ "name": "Jane", "email": "jones@restdb.io" }' # Mailgun response on success {"id":"<20230114154601.5d0bb6cd74064224@example.com>","message":"Queued. Thank you."} ``` :::tip Use the [CLI command](/docs/cli#logs) `coho logs` to see errors and generally what's going on inside of your runtime application. ::: Now that our API has processed some traffic, we can [query the NoSQL database](/docs/cli#query) to see what data has been written into the sendmail log collection. ```bash coho query maillog --pretty # Database output { email: 'jones@restdb.io', name: 'Jane', output: { id: '<20230114161849.9231a6192d685842@mg.codehooks.io>', message: 'Queued. Thank you.' }, _id: '185b1139873-p6yxdvkww5jz31' } ... ``` Voilá, we have a complete serverless backend for sending emails via the Mailgun REST API, including a full database log of all requests and results. The full source code from this Mailgun integration example is available at [GitHub](https://github.com/RestDB/codehooks-io-examples/tree/main/mailgunexample). Happy coding! --- ## Pet store API *(Note: This content contains MDX/JSX code)* A simple Pet store API with logging and background jobs and queue processing of complete collection data. 1. Create a project with the `coho create` CLI command. 2. Change to the project directory and install `npm i codehooks-js -s`. 3. Open `index.js` in your favorite code editor and enter the code shown below. 4. Deploy the project with `coho deploy` CLI command 🙌 ```js title=index.js /* * Pet store API * Codehooks (c) example code. * Deploy with CLI command: coho deploy */ import {app, Datastore} from 'codehooks-js' // API endpoint, jobs and queues app.get('/pet', logStats, getPets); app.get('/pet/:petid', getPet); app.post('/pet', logStats, postPet); app.put('/pet/:petid', putPet); app.delete('/pet/:petid', deletePet); app.get('/stats', petStats); app.job('*/10 * * * * *', cleanUpJob); app.worker('cleanParrots', cleanParrots); // Get all pets async function getPets(req, res) { console.debug("Get all Pets from datastore"); const conn = await Datastore.open(); conn.getMany('pets').json(res); } // Find Pet by ID async function getPet(req, res) { const {petid} = req.params; const conn = await Datastore.open(); const data = await conn.getOne('pets', petid); res.json(data); } // Create a new Pet async function postPet(req, res) { const conn = await Datastore.open(); req.body._created = new Date().toISOString(); const doc = await conn.insertOne('pets', req.body); res.status(201).json(doc); } // Update a Pet by ID async function putPet(req, res) { const {petid} = req.params; const conn = await Datastore.open(); const data = await conn.updateOne('pets', petid, req.body); res.json(data); } // Delete a Pet by ID async function deletePet(req, res) { const {petid} = req.params; const conn = await Datastore.open(); const data = await conn.removeOne('pets', petid); res.json(data); } // Add all Parrot pets to a clean up queue async function cleanUpJob(req, res) { const conn = await Datastore.open(); const query = {type: "Parrot"}; // all Parrots const topic = 'cleanParrots'; const job = await conn.enqueueFromQuery('pets', query, topic); console.log(job); res.end(); } // Process queue to clean up Parrot names async function cleanParrots(req, res) { let {payload} = req.body; if (payload.type === 'Parrot' && payload.name !== 'Polly') { console.log("Job", payload); payload.formerlyKnownAs = payload.name; payload.name = "Polly"; const conn = await Datastore.open(); const data = await conn.updateOne('pets', payload._id, payload); } res.end(); } // Middleware to log stats async function logStats(req, res, next) { const conn = await Datastore.open(); const kval = await conn.incr(`pet_${req.method}`, 1); next(); } // List all pet keys async function petStats(req, res) { const conn = await Datastore.open(); const stat = []; conn.getAll('pet_') .on('data', (data) => { res.json(stat); }).on('end', () => { res.json(stat); }) } export default app.init(); ``` --- ## React backend example *(Note: This content contains MDX/JSX code)* How do you set up an easy backend for [React](https://reactjs.org/)? In this example we'll create a simple React app front-end with a Node.js codehooks.io backend API and database. The main objective of this example is to learn how to use [codehooks.io](https://codehooks.io) as an API backend for your React app. The output of the React example app is shown in the screen shot below. The **"Hello React world!"** text and the visitor count is fetched by calling the backend REST API: ![react-backend-example](react-backend-example.png) Read on to learn how to create a simple React app front-end and backend application. ## Create the React example app We'll be using the standard [create-react-app](https://create-react-app.dev/) to kick-start our React example app development. Run the following commands from your command line: ```bash npx create-react-app react-backend-example cd react-backend-example ``` > You can also clone the GitHub repo `git clone https://github.com/RestDB/codehooks-io-examples.git` The boilerplate React example app is now generated in your application directory. Then open the `src/App.js` file in your favorite code editor (some prefer [VS Code](https://code.visualstudio.com/) and others like [Vim](https://www.vim.org/) 🥔 🍅) and replace the default generated code with the React code example shown below. If you read the code comments you'll get a pretty good idea of what's going on. ```js title="src/App.js" import React, { useState, useEffect } from 'react'; import logo from './logo.svg'; import './App.css'; // React backend API endpoint and API token // REPLACE WITH YOURS const API_ENDPOINT = 'https://reactback-nyly.api.codehooks.io/dev/hello'; const API_KEY = 'a4679c85-b4c8-49fb-b8ac-63230b269dd7'; function App() { // Application state variables const [visits, setVisits] = useState(null); const [message, setMessage] = useState(null); useEffect(()=>{ // Call Codehooks backend API const fetchData = async () => { const response = await fetch(API_ENDPOINT, { method: "GET", headers: { "x-apikey": API_KEY } }); const data = await response.json(); // Change application state and reload setMessage(data.message); setVisits(data.visits); } fetchData(); },[]) return (
logo

React backend with Codehooks.io

{message || ''}

Visitors: {visits || '---'}

); } 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 ![upload](./images/fileapi/upload.png) ### Download a PDF file ![download](./images/fileapi/download.png) --- ## 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. ![json-schema](./images/studio/json-schema.webp) ### 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. ![real-time web chat](./images/webchat.webp) [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: ![Workflow data in Codehooks Studio](images/workflowdata.webp) 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}

{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 (
{filters.map((filter) => ( ))}
); }; export const TemplateCard = ({ image, title, description, command, templateUrl, tutorialUrl, category, activeFilter, }) => { const isVisible = activeFilter === 'All' || activeFilter === category; if (!isVisible) return null; return (
{image && {title}}

{title}

{description}

            {command}
          
View Template → {tutorialUrl && ( Read Tutorial → )}
); }; 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}

{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
[**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. ![Stripe webhooks integration with Codehooks.io](./img/stripe-webhooks.webp) **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 =
body text here
, index = -1, }) => { return (
{icon} {title}
{index > -1 ? (
{' '}
) : null}
{content}
); }; export const Feature = ({ icon, title = 'No title', content =
body text here
, nextsteps = [], }) => { return (
{icon}

{title}

{content}
{nextsteps.length > 0 && (

Learn more

    {nextsteps.map((step, i) => (
  • {step}
  • ))}
)}
); }; ## 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) :::
} title="API & Webhook Development" content="REST, CRUD, custom APIs & webhooks" /> } title="Datastore" content="NoSQL & Key-Value" /> } title="Scheduled Jobs & Events" content="CRON schedules & event triggers" /> } title="Queues" content="Process webhooks asynchronously" /> } title="Workflows" content="Chain webhooks into reliable processes" /> } title="File System" content="Serve static files" /> } title="Data Aggregation" content="Groups & sums" /> } title="Hosting" content="Let codehooks serve your frontend" />
:::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, ]} />
} title="Auto-generate OpenAPI documentation" content={
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.
{`import { app } from 'codehooks-js'; import { z } from 'zod'; const todoSchema = z.object({ title: z.string().min(1).max(200), completed: z.boolean().default(false) }); // Enable OpenAPI docs at /docs app.openapi({ info: { title: 'Todo API', version: '1.0.0' } }); app.crudlify({ todos: todoSchema }); export default app.init();`}
After deploying, visit /docs for interactive Swagger UI with full authentication support, or /openapi.json for the raw specification.
} nextsteps={[ OpenAPI documentation guide, Document custom routes, Schema library support, ]} />
## NoSQL and Key-Value Datastores
} 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.

{codex5}
} nextsteps={[ Job API, Crontab guru - interactive CRON expression builder , ]} />
## Queues
} 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.
{codex6}
} nextsteps={[Queue API]} />
## Reliable Workflows
} title="Build reliable stateful workflows" content={
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:
$ coho file-upload --projectname 'myproject-fafc' --space 'dev' --src '/somedir/' --target '/myblobs'
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. ![socialcard](./img/alpine-js-rest-api-illustration.webp) ## 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**. ![new project](img/new-project.png) ### 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. ![code hooks studio alpine.js database](img/studio-fooball.png) 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). ![alpine.js example](img/alpine-js-example.png) 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. ![socialcard](./img/json-schema.webp) ### 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. ![codehoooks studio json-schema](./img/json-schema.webp) ### 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. ![socialcard](./img/sql-vs-nosql.webp) ## 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. ![socialcard](./img/api-integration.webp) ### 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. ![socialcard](./img/workflow-card.webp) ## 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. ![Vibe coding tools concept illustration](./img/vibe-coding-tools.webp) 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 | ![API vs REST API diagram](img/api-integration.webp) ## 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 ``` ![LLM workflow architecture](./img/llm-workflows-with-javascript.webp) ## 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 backend for OpenClaw agents](./img/codehooks-openclaw.webp) ## 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. ![Vibe coding a todo app with AI](./img/vibe-coding-todo-app.webp) ## 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. ![The final todo app with React frontend and auto-generated Swagger API documentation](./img/vibetodo.webp) ## 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? ![screenshot admin panel](img/stop-building-admin-apps.webp) See the below example screenshots of the default admin app generated from a json datamodel: ![screenshot of the datamodel editor](https://camo.githubusercontent.com/31e2365bdc584564c6357d9329d4872f5c93cafe792710eaa6b490a2a9109e8f/68747470733a2f2f636f6465686f6f6b732e696f2f696d672f72656163742d61646d696e2d64617368626f6172642f64657461696c2d766965772e77656270) ## 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. ![webhook delivery](img/webhook.delivery.webp) ## 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. ::: ![Secure automation webhooks](img/secure-automation-webhooks.webp) ## 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. ![Swagger UI showing the Todo API with auto-generated endpoints](./img/swagger-ui-todo-api.png) *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