diff --git a/node/censor-with-redact/.gitignore b/node/censor-with-redact/.gitignore
new file mode 100644
index 00000000..6a7d6d8e
--- /dev/null
+++ b/node/censor-with-redact/.gitignore
@@ -0,0 +1,130 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
\ No newline at end of file
diff --git a/node/censor-with-redact/.prettierrc.json b/node/censor-with-redact/.prettierrc.json
new file mode 100644
index 00000000..0a725205
--- /dev/null
+++ b/node/censor-with-redact/.prettierrc.json
@@ -0,0 +1,6 @@
+{
+  "trailingComma": "es5",
+  "tabWidth": 2,
+  "semi": true,
+  "singleQuote": true
+}
diff --git a/node/censor-with-redact/README.md b/node/censor-with-redact/README.md
new file mode 100644
index 00000000..ddc724db
--- /dev/null
+++ b/node/censor-with-redact/README.md
@@ -0,0 +1,65 @@
+# 🤐 Node.js Censor with Redact Function
+
+Cesnor sensitive information from a provided text string using Redact API by Pangea.
+
+## 🧰 Usage
+
+### `GET`
+
+HTML form for interacting with the model.
+
+### `POST`
+
+Returns the supplied text string with sensitive information redacted.
+
+**Parameters**
+
+| Name         | Description                 | Location | Type               | Sample Value                                 |
+| ------------ | --------------------------- | -------- | ------------------ | -------------------------------------------- |
+| Content-Type | Content type of the request | Header   | `application/json` | N/A                                          |
+| text         | Text to redact              | Body     | String             | `My email address is myname2000@gmail.com`   |
+
+**Response**
+
+Sample `200` Response:
+
+Content-Type: ``
+
+```json
+{
+    "ok": true,
+    "redacted": "My email address is <EMAIL_ADDRESS>"
+}
+```
+
+Sample `400` Response:
+
+```json
+{
+    "ok": false,
+    "error": "Missing required field: text."
+}
+
+```
+
+## ⚙️ Configuration
+
+| Setting           | Value         |
+| ----------------- | ------------- |
+| Runtime           | Node (18.0)   |
+| Entrypoint        | `src/main.js` |
+| Build Commands    | `npm install` |
+| Permissions       | `any`         |
+| Timeout (Seconds) | 15            |
+
+## 🔒 Environment Variables
+
+### PANGEA_REDACT_TOKEN
+
+Access token for the Pangea Redact API
+
+| Question      | Answer                                                                                  |
+| ------------- | --------------------------------------------------------------------------------------- |
+| Required      | Yes                                                                                     |
+| Sample Value  | `pts_7p4...5wl4`                                                                        |
+| Documentation | [Pangea: Configuration](https://pangea.cloud/docs/redact/getting-started/configuration) |
diff --git a/node/censor-with-redact/package-lock.json b/node/censor-with-redact/package-lock.json
new file mode 100644
index 00000000..11170539
--- /dev/null
+++ b/node/censor-with-redact/package-lock.json
@@ -0,0 +1,64 @@
+{
+  "name": "censor-with-redact",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "censor-with-redact",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "undici": "^5.22.1"
+      },
+      "devDependencies": {
+        "prettier": "^3.0.0"
+      }
+    },
+    "node_modules/busboy": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
+      "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
+      "dependencies": {
+        "streamsearch": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=10.16.0"
+      }
+    },
+    "node_modules/prettier": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz",
+      "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==",
+      "dev": true,
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "node_modules/streamsearch": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
+      "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/undici": {
+      "version": "5.22.1",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz",
+      "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==",
+      "dependencies": {
+        "busboy": "^1.6.0"
+      },
+      "engines": {
+        "node": ">=14.0"
+      }
+    }
+  }
+}
diff --git a/node/censor-with-redact/package.json b/node/censor-with-redact/package.json
new file mode 100644
index 00000000..c18e9ebe
--- /dev/null
+++ b/node/censor-with-redact/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "censor-with-redact",
+  "version": "1.0.0",
+  "description": "",
+  "main": "src/main.js",
+  "type": "module",
+  "scripts": {
+    "format": "prettier --write ."
+  },
+  "keywords": [],
+  "devDependencies": {
+    "prettier": "^3.0.0"
+  },
+  "dependencies": {
+    "undici": "^5.22.1"
+  }
+}
diff --git a/node/censor-with-redact/src/main.js b/node/censor-with-redact/src/main.js
new file mode 100644
index 00000000..e39ae297
--- /dev/null
+++ b/node/censor-with-redact/src/main.js
@@ -0,0 +1,32 @@
+import { fetch } from 'undici';
+import { getStaticFile, throwIfMissing } from './utils.js';
+
+export default async ({ req, res }) => {
+  throwIfMissing(process.env, ['PANGEA_REDACT_TOKEN']);
+
+  if (req.method === 'GET') {
+    return res.send(getStaticFile('index.html'), 200, {
+      'Content-Type': 'text/html; charset=utf-8',
+    });
+  }
+
+  try {
+    throwIfMissing(req.body, ['text']);
+  } catch (err) {
+    return res.json({ ok: false, error: err.message }, 400);
+  }
+
+  const response = await fetch(`https://redact.aws.eu.pangea.cloud/v1/redact`, {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+      Authorization: `Bearer ${process.env.PANGEA_REDACT_TOKEN}`,
+    },
+    body: JSON.stringify({
+      text: req.body.text,
+    }),
+  });
+
+  const data = /** @type {*} */ (await response.json());
+  return res.json({ ok: true, redacted: data.result.redacted_text });
+};
diff --git a/node/censor-with-redact/src/utils.js b/node/censor-with-redact/src/utils.js
new file mode 100644
index 00000000..5b93ba58
--- /dev/null
+++ b/node/censor-with-redact/src/utils.js
@@ -0,0 +1,34 @@
+import path from 'path';
+import { fileURLToPath } from 'url';
+import fs from 'fs';
+
+/**
+ * Throws an error if any of the keys are missing from the object
+ * @param {*} obj
+ * @param {string[]} keys
+ * @throws {Error}
+ */
+export function throwIfMissing(obj, keys) {
+  const missing = [];
+  for (let key of keys) {
+    if (!(key in obj) || !obj[key]) {
+      missing.push(key);
+    }
+  }
+  if (missing.length > 0) {
+    throw new Error(`Missing required fields: ${missing.join(', ')}`);
+  }
+}
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const staticFolder = path.join(__dirname, '../static');
+
+/**
+ * Returns the contents of a file in the static folder
+ * @param {string} fileName
+ * @returns {string} Contents of static/{fileName}
+ */
+export function getStaticFile(fileName) {
+  return fs.readFileSync(path.join(staticFolder, fileName)).toString();
+}
diff --git a/node/censor-with-redact/static/index.html b/node/censor-with-redact/static/index.html
new file mode 100644
index 00000000..af43985f
--- /dev/null
+++ b/node/censor-with-redact/static/index.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Censor with Redact API Demo</title>
+
+    <script>
+      async function onSubmit(text) {
+        const response = await fetch('/', {
+          method: "POST",
+          body: JSON.stringify({ text }),
+          headers: {
+            'Content-Type': 'application/json',
+          },
+        });
+
+        const json = await response.json();
+
+        if (!json.ok || json.error) {
+          alert(json.error);
+        }
+
+        return json.redacted;
+      }
+    </script>
+
+    <script src="//unpkg.com/alpinejs" defer></script>
+
+    <link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink" />
+    <link rel="stylesheet" href="https://unpkg.com/@appwrite.io/pink-icons" />
+  </head>
+  <body>
+    <main class="main-content">
+      <div class="top-cover u-padding-block-end-56">
+        <div class="container">
+          <div
+            class="u-flex u-gap-16 u-flex-justify-center u-margin-block-start-16"
+          >
+            <h1 class="heading-level-1">Censor with Redact API Demo</h1>
+            <code class="u-un-break-text"></code>
+          </div>
+          <p
+            class="body-text-1 u-normal u-margin-block-start-8"
+            style="max-width: 50rem"
+          >
+            This is demo application. You can ue this app to ensure
+            implementation with Redact API works properly. Use input below to
+            enter text and get censored message as response.
+          </p>
+        </div>
+      </div>
+      <div
+        class="container u-margin-block-start-negative-56"
+        x-data="{ message: '', censoredMessage: '', loading: false }"
+      >
+        <div class="card u-flex u-gap-24 u-flex-vertical">
+          <div class="u-flex u-cross-center u-gap-8">
+            <div
+              class="input-text-wrapper is-with-end-button u-width-full-line"
+            >
+              <input x-model="message" type="search" placeholder="Message" />
+              <div class="icon-search" aria-hidden="true"></div>
+            </div>
+
+            <button
+              class="button"
+              x-bind:disabled="loading"
+              x-on:click="async () => { loading = true; censoredMessage = ''; try { censoredMessage = await onSubmit(message) } catch(err) { console.error(err); } finally { loading = false; } }"
+            >
+              <span class="text">Censor</span>
+            </button>
+          </div>
+          <template x-if="censoredMessage">
+            <div class="u-flex u-flex-vertical u-gap-12">
+              <div class="u-flex u-flex-vertical u-gap-12 card">
+                <div class="u-flex u-gap-12">
+                  <h5 class="eyebrow-heading-2">Redact API:</h5>
+                </div>
+
+                <div style="overflow-x: hidden; line-break: anywhere">
+                  <p class="u-color-text-gray" x-text="censoredMessage"></p>
+                </div>
+              </div>
+            </div>
+          </template>
+        </div>
+      </div>
+    </main>
+  </body>
+</html>
