Documentation

FastAnn is a production-ready UI annotation library. Add contextual comments, badges, and highlights to any DOM element in minutes.

Installation

Install the framework-agnostic core and the wrapper for your stack:

# Angular
npm install @fastann/core @fastann/angular

# React
npm install @fastann/core @fastann/react

# Vue
npm install @fastann/core @fastann/vue

# jQuery
npm install @fastann/core @fastann/jquery

Quick Start

Bootstrap the library in your Angular application:

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideAnnotation }  from '@fastann/angular';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAnnotation({
      apiUrl:     'https://api.yourapp.com',
      licenseKey: 'YOUR_PRO_LICENSE_KEY', // Pro only
    }),
  ],
};

Configuration

OptionTypeRequiredDescription
apiUrlstringYesBase URL for the annotation API.
licenseKeystringProRSA-signed domain-locked key.
theme'light' | 'dark'NoDefault: 'light'
localestringNoDefault: 'en'

Cell Keys

A cell key is a unique string that identifies an annotatable element. It follows the format rowId:columnId:

<td ann-cell-key="row-42:revenue">{{ "{{" }} row.revenue {{ "}}" }}</td>

Keys must be stable across page loads — use your data's primary key, not a loop index.

Permissions

FastAnn uses a granular permission model. Assign permissions per user:

type Permission =
  | 'view_annotation'   // see annotation badges
  | 'create_annotation' // add new annotations
  | 'edit_annotation'   // edit own annotations
  | 'delete_annotation' // delete own annotations
  | 'view_comment'      // read comments
  | 'create_comment'    // post comments
  | 'delete_comment'    // delete any comment (moderator)
  | 'view_logs';        // view change history

Angular Guide

Import AnnCellDirective into your standalone component:

// sales-table.component.ts
import { AnnCellDirective } from '@fastann/angular';

@Component({
  standalone: true,
  imports: [AnnCellDirective],
  template: `
    <table>
      <tr *ngFor="let row of rows">
        <td [ann-cell-key]="'r' + row.id + ':name'">{{ "{{" }} row.name {{ "}}" }}</td>
        <td [ann-cell-key]="'r' + row.id + ':sales'">{{ "{{" }} row.sales {{ "}}" }}</td>
      </tr>
    </table>
  `
})
export class SalesTableComponent {
  rows = [...];
}

React Guide

// SalesTable.tsx
import { AnnCell, AnnProvider } from '@fastann/react';

export function SalesTable() {
  return (
    <AnnProvider apiUrl="https://api.yourapp.com" licenseKey={LICENSE_KEY}>
      <table>
        {rows.map(row => (
          <tr key={row.id}>
            <AnnCell cellKey={`r${row.id}:name`}>{row.name}</AnnCell>
            <AnnCell cellKey={`r${row.id}:sales`}>{row.sales}</AnnCell>
          </tr>
        ))}
      </table>
    </AnnProvider>
  );
}

Vue Guide

<!-- SalesTable.vue -->
<script setup>
import { AnnCell } from '@fastann/vue';
</script>

<template>
  <table>
    <tr v-for="row in rows" :key="row.id">
      <ann-cell :cell-key="`r${row.id}:name`">{{ "{{" }} row.name {{ "}}" }}</ann-cell>
    </tr>
  </table>
</template>

License Key Format

A Pro license key is structured as:

Base64(UTF8(payloadJson)) . Base64(RSA_PKCS1_SHA256_signature)

Where the payload JSON is:

{ "dom": "yoursite.com", "plan": "Pro", "uid": "a1b2c3d4" }

Frontend Verification

Use the Web Crypto API to verify licenses entirely offline:

export async function verifyLicense(licenseKey: string): Promise<boolean> {
  const [payloadB64, sigB64] = licenseKey.split('.');
  if (!payloadB64 || !sigB64) return false;

  const payloadBytes = Uint8Array.from(atob(payloadB64), c => c.charCodeAt(0));
  const sigBytes     = Uint8Array.from(atob(sigB64),     c => c.charCodeAt(0));

  const cryptoKey = await crypto.subtle.importKey(
    'spki', pemToArrayBuffer(PUBLIC_KEY_PEM),
    { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
    false, ['verify']
  );

  const valid = await crypto.subtle.verify(
    'RSASSA-PKCS1-v1_5', cryptoKey, sigBytes, payloadBytes
  );
  if (!valid) return false;

  const payload = JSON.parse(new TextDecoder().decode(payloadBytes));
  return payload.dom === window.location.hostname.toLowerCase();
}