Skip to content

Repository files navigation

Airside

Embeddable Commenting Tool

A self-contained, open-source commenting overlay you host yourself. Drop the widget into any web page — Next.js, plain HTML, or anything in between — and reviewers can leave threaded, DOM-anchored comments without touching your page design or sending data to a third-party service.

  • Client-side: a light-DOM widget with its own bundled React; no iframe, no Shadow DOM.
  • Server-side: a Web-standard Request → Response handler you mount inside your own app.
  • Database: MongoDB Atlas or PostgreSQL — you choose; the driver only enters builds that import the matching adapter.
  • Storage: Vercel Blob, Amazon S3 / Cloudflare R2, or local filesystem for image attachments.

Airside demo — log in, drop a pin to leave a comment, and triage threads from the side panel


Quick start (Next.js App Router)

1. Install

pnpm add @airnauts/airside-integration-next @airnauts/airside-client \
  @airnauts/airside-adapter-mongo @airnauts/airside-storage-vercel-blob
# React is required in your Next.js app already; no extra peer to install.

2. Mount the API route

Create app/api/airside/[...path]/route.ts:

import { createAirsideAppRoute } from '@airnauts/airside-integration-next'
import { mongoRepository } from '@airnauts/airside-adapter-mongo'
import { createVercelBlobStorage } from '@airnauts/airside-storage-vercel-blob'

export const { GET, POST, PATCH, DELETE, OPTIONS } = createAirsideAppRoute({
  secretKey: process.env.AIRSIDE_SECRET!,
  projectId: 'my-app',
  allowedOrigins: ['https://my-app.example.com'],
  repository: mongoRepository({ uri: process.env.MONGODB_URI! }),
  storage: createVercelBlobStorage({ token: process.env.BLOB_READ_WRITE_TOKEN! }),
})

3. Mount the widget

In your root layout:

'use client'
import { AirsideLayer } from '@airnauts/airside-integration-next/client'

export function AirsideMount() {
  return <AirsideLayer airsideKey={process.env.NEXT_PUBLIC_AIRSIDE_KEY!} endpoint="/api/airside" />
}

The widget is inert until a page is opened with ?airside-key=<your-secret-key> in the URL. After that, the key is persisted to localStorage so it stays active on subsequent visits.

Local development (no database)

Swap in the in-memory adapter:

import { createMemoryRepository } from '@airnauts/airside-adapter-memory'
import { createFileSystemStorage } from '@airnauts/airside-storage-fs'

export const { GET, POST, PATCH, DELETE, OPTIONS } = createAirsideAppRoute({
  secretKey: 'dev-key',
  projectId: 'my-app',
  allowedOrigins: ['http://localhost:3000'],
  repository: createMemoryRepository(),
  storage: createFileSystemStorage({ rootDir: './public/uploads', baseUrl: '/uploads' }),
  rateLimit: false,
})

Alternative setups

The Quick start wires both halves on the Next.js App Router. Each half swaps independently — pick one server mount and one widget mount:

  • Server — App Router (above), Pages Router (below), or, on any other host, the Web-standard server.handle(request) directly: a Fetch-native framework like Hono passes its Request straight in; a classic Node host (Express, http) bridges req/res via @airnauts/airside-server/node. Node-compatible runtimes only (the server uses node:crypto, Buffer, and Node database drivers).
  • WidgetAirsideLayer for React (below), or airside.init() for vanilla JS (below).

The widget only needs an endpoint pointing at a mounted server; the server only needs the widget's origin in its allowedOrigins.

Server — Next.js Pages Router

On the Pages Router, mount a catch-all API route with createAirsidePagesRoute:

// pages/api/airside/[...path].ts
import { createAirsidePagesRoute } from '@airnauts/airside-integration-next'
import { createMemoryRepository } from '@airnauts/airside-adapter-memory'

// REQUIRED: Next reads this statically, so the helper can't set it. The comments
// API parses JSON/multipart itself, so the raw body must reach it unparsed.
export const config = { api: { bodyParser: false } }

export default createAirsidePagesRoute({
  secretKey: process.env.AIRSIDE_SECRET ?? 'dev-key',
  projectId: 'my-app',
  allowedOrigins: ['http://localhost:3000'],
  repository: createMemoryRepository(),
  storage: { async put(blob) { return { url: `mem://${blob.name}`, key: blob.name, size: 0 } } },
  rateLimit: false,
})

A single default export handles every method — server.handle answers the CORS preflight (OPTIONS) internally. Keep this on the Node runtime (the default): the server uses node:crypto, Buffer, and Node-only database drivers, so it cannot run on the Edge runtime. For production, swap createMemoryRepository() and the storage stub for mongoRepository({ uri }) + createVercelBlobStorage({ token }) (or createFileSystemStorage), exactly as in the App Router Quick start.

Widget — React (any framework)

AirsideLayer is a plain React component — it works in any React app (Vite, CRA, Remix…), not just Next.js. Install the React integration package:

pnpm add @airnauts/airside-integration-react

Render it once near your app root and point endpoint at the mounted server (use an absolute URL when the API is on another origin, and add that origin to the server's allowedOrigins):

import { AirsideLayer } from '@airnauts/airside-integration-react'

export function App() {
  return (
    <>
      {/* your app */}
      <AirsideLayer
        airsideKey={import.meta.env.VITE_AIRSIDE_KEY}
        endpoint="https://api.example.com/api/airside"
      />
    </>
  )
}

@airnauts/airside-integration-react needs react as a peer — already present in your React app, so there's nothing extra to install beyond the package above.

Widget — Vanilla JS (no framework)

Without React, call airside.init() directly. It returns a handle you can destroy() to tear the widget down again:

import { airside } from '@airnauts/airside-client'

const handle = await airside.init({
  key: 'your-secret-key',
  endpoint: '/api/airside', // or an absolute URL to a server on another origin
})

// later, to remove the widget:
// handle.destroy()

As with the React wrapper, the widget stays inert until the page is opened with ?airside-key=<key> (after which the key is persisted and the param stripped from the URL). Use it from any bundler, or from a <script type="module"> on a plain HTML page.


Packages

This is a pnpm monorepo. The packages below are published to npm under the @airnauts scope.

Package Description
@airnauts/airside-core Isomorphic: Zod schemas, HTTP contract types, pageKey normalization, anchor scoring/threshold policy, OpenAPI generator
@airnauts/airside-client Widget engine (init()), light-DOM anchoring runtime
@airnauts/airside-integration-react React host wrapper (AirsideLayer) — calls init() in an effect
@airnauts/airside-server Web-standard HTTP handler, use cases, CORS/security, adapter interfaces, generic Node bridge, dev server
@airnauts/airside-integration-next One-call Next.js App and Pages Router integration (createAirsideAppRoute / createAirsidePagesRoute)
@airnauts/airside-adapter-mongo MongoDB Atlas / self-hosted repository adapter
@airnauts/airside-adapter-postgres PostgreSQL repository adapter (hybrid columns + jsonb; driver-agnostic)
@airnauts/airside-adapter-memory In-memory repository for local development and tests
@airnauts/airside-storage-vercel-blob Vercel Blob image-attachment storage
@airnauts/airside-storage-s3 Amazon S3 / Cloudflare R2 image-attachment storage
@airnauts/airside-storage-fs Filesystem image-attachment storage
@airnauts/airside-extension-slack Slack Incoming Webhook notification extension
@airnauts/airside-extension-email Email notification extension (SMTP via nodemailer or Resend HTTP API)
@airnauts/airside-extension-jira "Create Jira issue" thread-action extension for Jira Cloud
@airnauts/airside-extension-github "Create GitHub issue" thread-action extension for GitHub Issues

Examples

Example Description
examples/nextjs-host Full Next.js App Router integration — MongoDB, Vercel Blob, Slack and email notifications, Jira and GitHub Issues thread-action extensions, Playwright e2e tests
examples/playground Minimal Vite + in-memory server sandbox for widget development

Roadmap

None of these are committed releases — they're the directions we're considering.

Widget & UX

  • Per-comment overflow menu — edit / delete / copy a comment (needs new PATCH/DELETE comment endpoints).
  • Emoji reactions on comments (new Comment field + add/remove-reaction endpoints across both adapters).
  • Smooth, document-anchored pin positioning — drop the per-scroll-frame layout work for jank-free pins (parking lot; a positioning-basis change that would get its own ADR).
  • In-widget changelog popup surfacing recent user-facing changes to reviewers (parking lot).
  • Page-level / unanchored comments — start a thread without placing a pin, for general page feedback (parking lot; schema seam already designed in the architecture).
  • Rich-text / Markdown comment bodies.
  • @mentions and thread assignment.
  • Accessibility & keyboard-navigation pass; widget UI localization (i18n).

Real-time & collaboration

  • Live updates — push new comments and threads to open widgets (SSE or WebSocket) instead of refetch-on-focus.
  • Authenticated reviewer identity — map commenters to real user accounts / SSO instead of a typed-in name.

Integrations & extensions

  • Jira comment sync — mirror later thread replies into a linked Jira issue (parking lot; needs externalLinks on the notification event).
  • More thread-action integrations — Linear.
  • More notifiers — Discord, Microsoft Teams, generic outbound webhook.

Adapters & hosts

  • More persistence adapters — SQLite, MySQL.
  • More host-framework glue beyond Next.js — Remix, SvelteKit, Astro, and a generic Request-based handler for Hono / Express.

Managed cloud

  • Hosted cloud version — a subscription-based, fully-managed offering for teams that want the review workflow without standing up their own server: we run the comment server, database, and attachment storage; you drop in the widget. Self-hosting the open-source packages stays free and first-class.

Bug fixes & known rough edges

  • A pin on a plain structural element can silently migrate to the wrong surviving sibling after the original is removed (correctness bug; TDD fix deferred).
  • Signal-less elements (no id, class, or data-* attribute) cannot clear the re-anchor score threshold under structural mutations and always orphan (known v1 scoring limitation).
  • MongoDB adapter emits a cosmetic webpack aws4 warning when bundled by Next.js — build succeeds and runtime is unaffected (deferred; fix is a lazy-import change in the adapter).

Want one of these sooner, or have a use case we haven't listed? Open an issue or reach out to Airnauts.


Developing in this monorepo

Prerequisites: Node.js ≥ 18, pnpm ≥ 9.

# Install all dependencies
pnpm install

# Build all packages (required before running examples or tests)
pnpm build

# Run all tests
pnpm test

# Typecheck all packages
pnpm typecheck

Branching: development happens directly on main until the beta release.

Releases are managed by Changesets. Publishing is automatic on every push to main (after CI passes). See RELEASING.md for the full release procedure.


License

MIT


About Airnauts

Airnauts

This tool is built and maintained by Airnauts — a digital product studio that designs and engineers web and mobile products end to end, from early concept and UX through to production software.

We built Airside to solve a recurring problem in our own client work: gathering precise, in-context feedback on live web pages without bolting on a heavyweight third-party SaaS. We open-sourced it so other teams can host the same review workflow on their own infrastructure, keep their data in their own database, and adapt the widget to their own product.

If you'd like help integrating it, or you're looking for a partner to design and build your next product, get in touch at airnauts.com.

About

Self-hosted, embeddable commenting overlay for any web page — threaded, DOM-anchored review comments backed by your own database and storage. No iframe, no third-party SaaS.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages